d7c83d719eb1e7510166fa8f06ad341e6875686f

Author: yandod

Date: 2009-03-21 20:13:19 +0900

initial commit.

diff --git a/app/.htaccess b/app/.htaccess new file mode 100755 index 0000000..0ed8662 --- /dev/null +++ b/app/.htaccess @@ -0,0 +1,5 @@ +<IfModule mod_rewrite.c> + RewriteEngine on + RewriteRule ^$ webroot/ [L] + RewriteRule (.*) webroot/$1 [L] + </IfModule> \ No newline at end of file diff --git a/app/config/acl.ini.php b/app/config/acl.ini.php new file mode 100755 index 0000000..94a9a9a --- /dev/null +++ b/app/config/acl.ini.php @@ -0,0 +1,74 @@ +;<?php die() ?> +; SVN FILE: $Id: acl.ini.php 7945 2008-12-19 02:16:01Z gwoo $ +;/** +; * Short description for file. +; * +; * +; * PHP versions 4 and 5 +; * +; * CakePHP(tm) : Rapid Development Framework http://www.cakephp.org/ +; * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) +; * +; * Licensed under The MIT License +; * Redistributions of files must retain the above copyright notice. +; * +; * @filesource +; * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) +; * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project +; * @package cake +; * @subpackage cake.app.config +; * @since CakePHP(tm) v 0.10.0.1076 +; * @version $Revision: 7945 $ +; * @modifiedby $LastChangedBy: gwoo $ +; * @lastmodified $Date: 2008-12-18 18:16:01 -0800 (Thu, 18 Dec 2008) $ +; * @license http://www.opensource.org/licenses/mit-license.php The MIT License +; */ + +; acl.ini.php - Cake ACL Configuration +; --------------------------------------------------------------------- +; Use this file to specify user permissions. +; aco = access control object (something in your application) +; aro = access request object (something requesting access) +; +; User records are added as follows: +; +; [uid] +; groups = group1, group2, group3 +; allow = aco1, aco2, aco3 +; deny = aco4, aco5, aco6 +; +; Group records are added in a similar manner: +; +; [gid] +; allow = aco1, aco2, aco3 +; deny = aco4, aco5, aco6 +; +; The allow, deny, and groups sections are all optional. +; NOTE: groups names *cannot* ever be the same as usernames! +; +; ACL permissions are checked in the following order: +; 1. Check for user denies (and DENY if specified) +; 2. Check for user allows (and ALLOW if specified) +; 3. Gather user's groups +; 4. Check group denies (and DENY if specified) +; 5. Check group allows (and ALLOW if specified) +; 6. If no aro, aco, or group information is found, DENY +; +; --------------------------------------------------------------------- + +;------------------------------------- +;Users +;------------------------------------- + +[username-goes-here] +groups = group1, group2 +deny = aco1, aco2 +allow = aco3, aco4 + +;------------------------------------- +;Groups +;------------------------------------- + +[groupname-goes-here] +deny = aco5, aco6 +allow = aco7, aco8 \ No newline at end of file diff --git a/app/config/bootstrap.php b/app/config/bootstrap.php new file mode 100755 index 0000000..3e9cdac --- /dev/null +++ b/app/config/bootstrap.php @@ -0,0 +1,45 @@ +<?php +/* SVN FILE: $Id: bootstrap.php 7945 2008-12-19 02:16:01Z gwoo $ */ +/** + * Short description for file. + * + * Long description for file + * + * PHP versions 4 and 5 + * + * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org) + * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project + * @package cake + * @subpackage cake.app.config + * @since CakePHP(tm) v 0.10.8.2117 + * @version $Revision: 7945 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2008-12-18 18:16:01 -0800 (Thu, 18 Dec 2008) $ + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ +/** + * + * This file is loaded automatically by the app/webroot/index.php file after the core bootstrap.php is loaded + * This is an application wide file to load any function that is not used within a class define. + * You can also use this to include or require any files in your application. + * + */ +/** + * The settings below can be used to set additional paths to models, views and controllers. + * This is related to Ticket #470 (https://trac.cakephp.org/ticket/470) + * + * $modelPaths = array('full path to models', 'second full path to models', 'etc...'); + * $viewPaths = array('this path to views', 'second full path to views', 'etc...'); + * $controllerPaths = array('this path to controllers', 'second full path to controllers', 'etc...'); + * + */ +//EOF +Configure::write('app_title', 'Candycane'); +?> \ No newline at end of file diff --git a/app/config/core.php b/app/config/core.php new file mode 100755 index 0000000..523676b --- /dev/null +++ b/app/config/core.php @@ -0,0 +1,227 @@ +<?php +/* SVN FILE: $Id: core.php 7945 2008-12-19 02:16:01Z gwoo $ */ +/** + * This is core configuration file. + * + * Use it to configure core behavior of Cake. + * + * PHP versions 4 and 5 + * + * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org) + * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project + * @package cake + * @subpackage cake.app.config + * @since CakePHP(tm) v 0.2.9 + * @version $Revision: 7945 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2008-12-18 18:16:01 -0800 (Thu, 18 Dec 2008) $ + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ +/** + * CakePHP Debug Level: + * + * Production Mode: + * 0: No error messages, errors, or warnings shown. Flash messages redirect. + * + * Development Mode: + * 1: Errors and warnings shown, model caches refreshed, flash messages halted. + * 2: As in 1, but also with full debug messages and SQL output. + * 3: As in 2, but also with full controller dump. + * + * In production mode, flash messages redirect after a time interval. + * In development mode, you need to click the flash message to continue. + */ + Configure::write('debug', 2); +/** + * Application wide charset encoding + */ + Configure::write('App.encoding', 'UTF-8'); +/** + * To configure CakePHP *not* to use mod_rewrite and to + * use CakePHP pretty URLs, remove these .htaccess + * files: + * + * /.htaccess + * /app/.htaccess + * /app/webroot/.htaccess + * + * And uncomment the App.baseUrl below: + */ + //Configure::write('App.baseUrl', env('SCRIPT_NAME')); +/** + * Uncomment the define below to use CakePHP admin routes. + * + * The value of the define determines the name of the route + * and its associated controller actions: + * + * 'admin' -> admin_index() and /admin/controller/index + * 'superuser' -> superuser_index() and /superuser/controller/index + */ + //Configure::write('Routing.admin', 'admin'); + +/** + * Turn off all caching application-wide. + * + */ + //Configure::write('Cache.disable', true); +/** + * Enable cache checking. + * + * If set to true, for view caching you must still use the controller + * var $cacheAction inside your controllers to define caching settings. + * You can either set it controller-wide by setting var $cacheAction = true, + * or in each action using $this->cacheAction = true. + * + */ + //Configure::write('Cache.check', true); +/** + * Defines the default error type when using the log() function. Used for + * differentiating error logging and debugging. Currently PHP supports LOG_DEBUG. + */ + define('LOG_ERROR', 2); +/** + * The preferred session handling method. Valid values: + * + * 'php' Uses settings defined in your php.ini. + * 'cake' Saves session files in CakePHP's /tmp directory. + * 'database' Uses CakePHP's database sessions. + * + * To define a custom session handler, save it at /app/config/<name>.php. + * Set the value of 'Session.save' to <name> to utilize it in CakePHP. + * + * To use database sessions, execute the SQL file found at /app/config/sql/sessions.sql. + * + */ + Configure::write('Session.save', 'php'); +/** + * The name of the table used to store CakePHP database sessions. + * + * 'Session.save' must be set to 'database' in order to utilize this constant. + * + * The table name set here should *not* include any table prefix defined elsewhere. + */ + //Configure::write('Session.table', 'cake_sessions'); +/** + * The DATABASE_CONFIG::$var to use for database session handling. + * + * 'Session.save' must be set to 'database' in order to utilize this constant. + */ + //Configure::write('Session.database', 'default'); +/** + * The name of CakePHP's session cookie. + */ + Configure::write('Session.cookie', 'CAKEPHP'); +/** + * Session time out time (in seconds). + * Actual value depends on 'Security.level' setting. + */ + Configure::write('Session.timeout', '120'); +/** + * If set to false, sessions are not automatically started. + */ + Configure::write('Session.start', true); +/** + * When set to false, HTTP_USER_AGENT will not be checked + * in the session + */ + Configure::write('Session.checkAgent', true); +/** + * The level of CakePHP security. The session timeout time defined + * in 'Session.timeout' is multiplied according to the settings here. + * Valid values: + * + * 'high' Session timeout in 'Session.timeout' x 10 + * 'medium' Session timeout in 'Session.timeout' x 100 + * 'low' Session timeout in 'Session.timeout' x 300 + * + * CakePHP session IDs are also regenerated between requests if + * 'Security.level' is set to 'high'. + */ + Configure::write('Security.level', 'high'); +/** + * A random string used in security hashing methods. + */ + Configure::write('Security.salt', 'DYhG93b0qyJfIxfs2guoUubWwvniR2G0FgaC9mi'); +/** + * Compress CSS output by removing comments, whitespace, repeating tags, etc. + * This requires a/var/cache directory to be writable by the web server for caching. + * and /vendors/csspp/csspp.php + * + * To use, prefix the CSS link URL with '/ccss/' instead of '/css/' or use HtmlHelper::css(). + */ + //Configure::write('Asset.filter.css', 'css.php'); +/** + * Plug in your own custom JavaScript compressor by dropping a script in your webroot to handle the + * output, and setting the config below to the name of the script. + * + * To use, prefix your JavaScript link URLs with '/cjs/' instead of '/js/' or use JavaScriptHelper::link(). + */ + //Configure::write('Asset.filter.js', 'custom_javascript_output_filter.php'); +/** + * The classname and database used in CakePHP's + * access control lists. + */ + Configure::write('Acl.classname', 'DbAcl'); + Configure::write('Acl.database', 'default'); +/** + * + * Cache Engine Configuration + * Default settings provided below + * + * File storage engine. + * + * Cache::config('default', array( + * 'engine' => 'File', //[required] + * 'duration'=> 3600, //[optional] + * 'probability'=> 100, //[optional] + * 'path' => CACHE, //[optional] use system tmp directory - remember to use absolute path + * 'prefix' => 'cake_', //[optional] prefix every cache file with this string + * 'lock' => false, //[optional] use file locking + * 'serialize' => true, [optional] + * )); + * + * + * APC (http://pecl.php.net/package/APC) + * + * Cache::config('default', array( + * 'engine' => 'Apc', //[required] + * 'duration'=> 3600, //[optional] + * 'probability'=> 100, //[optional] + * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string + * )); + * + * Xcache (http://xcache.lighttpd.net/) + * + * Cache::config('default', array( + * 'engine' => 'Xcache', //[required] + * 'duration'=> 3600, //[optional] + * 'probability'=> 100, //[optional] + * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string + * 'user' => 'user', //user from xcache.admin.user settings + * 'password' => 'password', //plaintext password (xcache.admin.pass) + * )); + * + * + * Memcache (http://www.danga.com/memcached/) + * + * Cache::config('default', array( + * 'engine' => 'Memcache', //[required] + * 'duration'=> 3600, //[optional] + * 'probability'=> 100, //[optional] + * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string + * 'servers' => array( + * '127.0.0.1:11211' // localhost, default port 11211 + * ), //[optional] + * 'compress' => false, // [optional] compress data in Memcache (slower, but uses less memory) + * )); + * + */ + Cache::config('default', array('engine' => 'File')); +?> \ No newline at end of file diff --git a/app/config/database.php b/app/config/database.php new file mode 100755 index 0000000..41cdea9 --- /dev/null +++ b/app/config/database.php @@ -0,0 +1,101 @@ +<?php +/* SVN FILE: $Id: database.php.default 8004 2009-01-16 20:15:21Z gwoo $ */ +/** + * This is core configuration file. + * + * Use it to configure core behaviour ofCake. + * + * PHP versions 4 and 5 + * + * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org) + * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project + * @package cake + * @subpackage cake.app.config + * @since CakePHP(tm) v 0.2.9 + * @version $Revision: 8004 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2009-01-16 12:15:21 -0800 (Fri, 16 Jan 2009) $ + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ +/** + * In this file you set up your database connection details. + * + * @package cake + * @subpackage cake.config + */ +/** + * Database configuration class. + * You can specify multiple configurations for production, development and testing. + * + * driver => The name of a supported driver; valid options are as follows: + * mysql - MySQL 4 & 5, + * mysqli - MySQL 4 & 5 Improved Interface (PHP5 only), + * sqlite - SQLite (PHP5 only), + * postgres - PostgreSQL 7 and higher, + * mssql - Microsoft SQL Server 2000 and higher, + * db2 - IBM DB2, Cloudscape, and Apache Derby (http://php.net/ibm-db2) + * oracle - Oracle 8 and higher + * firebird - Firebird/Interbase + * sybase - Sybase ASE + * adodb-[drivername] - ADOdb interface wrapper (see below), + * odbc - ODBC DBO driver + * + * You can add custom database drivers (or override existing drivers) by adding the + * appropriate file to app/models/datasources/dbo. Drivers should be named 'dbo_x.php', + * where 'x' is the name of the database. + * + * persistent => true / false + * Determines whether or not the database should use a persistent connection + * + * connect => + * ADOdb set the connect to one of these + * (http://phplens.com/adodb/supported.databases.html) and + * append it '|p' for persistent connection. (mssql|p for example, or just mssql for not persistent) + * For all other databases, this setting is deprecated. + * + * host => + * the host you connect to the database. To add a socket or port number, use 'port' => # + * + * prefix => + * Uses the given prefix for all the tables in this database. This setting can be overridden + * on a per-table basis with the Model::$tablePrefix property. + * + * schema => + * For Postgres and DB2, specifies which schema you would like to use the tables in. Postgres defaults to + * 'public', DB2 defaults to empty. + * + * encoding => + * For MySQL, MySQLi, Postgres and DB2, specifies the character encoding to use when connecting to the + * database. Uses database default. + * + */ +class DATABASE_CONFIG { + + var $default = array( + 'driver' => 'mysql', + 'persistent' => false, + 'host' => 'localhost', + 'login' => 'root', + 'password' => '', + 'database' => 'redmine_development', + 'prefix' => '', + ); + + var $test = array( + 'driver' => 'mysql', + 'persistent' => false, + 'host' => 'localhost', + 'login' => 'user', + 'password' => 'password', + 'database' => 'test_database_name', + 'prefix' => '', + ); +} +?> \ No newline at end of file diff --git a/app/config/database.php.default b/app/config/database.php.default new file mode 100755 index 0000000..c20fd5d --- /dev/null +++ b/app/config/database.php.default @@ -0,0 +1,101 @@ +<?php +/* SVN FILE: $Id: database.php.default 8004 2009-01-16 20:15:21Z gwoo $ */ +/** + * This is core configuration file. + * + * Use it to configure core behaviour ofCake. + * + * PHP versions 4 and 5 + * + * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org) + * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project + * @package cake + * @subpackage cake.app.config + * @since CakePHP(tm) v 0.2.9 + * @version $Revision: 8004 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2009-01-16 12:15:21 -0800 (Fri, 16 Jan 2009) $ + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ +/** + * In this file you set up your database connection details. + * + * @package cake + * @subpackage cake.config + */ +/** + * Database configuration class. + * You can specify multiple configurations for production, development and testing. + * + * driver => The name of a supported driver; valid options are as follows: + * mysql - MySQL 4 & 5, + * mysqli - MySQL 4 & 5 Improved Interface (PHP5 only), + * sqlite - SQLite (PHP5 only), + * postgres - PostgreSQL 7 and higher, + * mssql - Microsoft SQL Server 2000 and higher, + * db2 - IBM DB2, Cloudscape, and Apache Derby (http://php.net/ibm-db2) + * oracle - Oracle 8 and higher + * firebird - Firebird/Interbase + * sybase - Sybase ASE + * adodb-[drivername] - ADOdb interface wrapper (see below), + * odbc - ODBC DBO driver + * + * You can add custom database drivers (or override existing drivers) by adding the + * appropriate file to app/models/datasources/dbo. Drivers should be named 'dbo_x.php', + * where 'x' is the name of the database. + * + * persistent => true / false + * Determines whether or not the database should use a persistent connection + * + * connect => + * ADOdb set the connect to one of these + * (http://phplens.com/adodb/supported.databases.html) and + * append it '|p' for persistent connection. (mssql|p for example, or just mssql for not persistent) + * For all other databases, this setting is deprecated. + * + * host => + * the host you connect to the database. To add a socket or port number, use 'port' => # + * + * prefix => + * Uses the given prefix for all the tables in this database. This setting can be overridden + * on a per-table basis with the Model::$tablePrefix property. + * + * schema => + * For Postgres and DB2, specifies which schema you would like to use the tables in. Postgres defaults to + * 'public', DB2 defaults to empty. + * + * encoding => + * For MySQL, MySQLi, Postgres and DB2, specifies the character encoding to use when connecting to the + * database. Uses database default. + * + */ +class DATABASE_CONFIG { + + var $default = array( + 'driver' => 'mysql', + 'persistent' => false, + 'host' => 'localhost', + 'login' => 'user', + 'password' => 'password', + 'database' => 'database_name', + 'prefix' => '', + ); + + var $test = array( + 'driver' => 'mysql', + 'persistent' => false, + 'host' => 'localhost', + 'login' => 'user', + 'password' => 'password', + 'database' => 'test_database_name', + 'prefix' => '', + ); +} +?> \ No newline at end of file diff --git a/app/config/inflections.php b/app/config/inflections.php new file mode 100755 index 0000000..429c085 --- /dev/null +++ b/app/config/inflections.php @@ -0,0 +1,70 @@ +<?php +/* SVN FILE: $Id: inflections.php 7945 2008-12-19 02:16:01Z gwoo $ */ +/** + * Custom Inflected Words. + * + * This file is used to hold words that are not matched in the normail Inflector::pluralize() and + * Inflector::singularize() + * + * PHP versions 4 and % + * + * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org) + * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project + * @package cake + * @subpackage cake.app.config + * @since CakePHP(tm) v 1.0.0.2312 + * @version $Revision: 7945 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2008-12-18 18:16:01 -0800 (Thu, 18 Dec 2008) $ + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ +/** + * This is a key => value array of regex used to match words. + * If key matches then the value is returned. + * + * $pluralRules = array('/(s)tatus$/i' => '\1\2tatuses', '/^(ox)$/i' => '\1\2en', '/([m|l])ouse$/i' => '\1ice'); + */ + $pluralRules = array(); +/** + * This is a key only array of plural words that should not be inflected. + * Notice the last comma + * + * $uninflectedPlural = array('.*[nrlm]ese', '.*deer', '.*fish', '.*measles', '.*ois', '.*pox'); + */ + $uninflectedPlural = array(); +/** + * This is a key => value array of plural irregular words. + * If key matches then the value is returned. + * + * $irregularPlural = array('atlas' => 'atlases', 'beef' => 'beefs', 'brother' => 'brothers') + */ + $irregularPlural = array(); +/** + * This is a key => value array of regex used to match words. + * If key matches then the value is returned. + * + * $singularRules = array('/(s)tatuses$/i' => '\1\2tatus', '/(matr)ices$/i' =>'\1ix','/(vert|ind)ices$/i') + */ + $singularRules = array(); +/** + * This is a key only array of singular words that should not be inflected. + * You should not have to change this value below if you do change it use same format + * as the $uninflectedPlural above. + */ + $uninflectedSingular = $uninflectedPlural; +/** + * This is a key => value array of singular irregular words. + * Most of the time this will be a reverse of the above $irregularPlural array + * You should not have to change this value below if you do change it use same format + * + * $irregularSingular = array('atlases' => 'atlas', 'beefs' => 'beef', 'brothers' => 'brother') + */ + $irregularSingular = array_flip($irregularPlural); +?> \ No newline at end of file diff --git a/app/config/routes.php b/app/config/routes.php new file mode 100755 index 0000000..93c5c10 --- /dev/null +++ b/app/config/routes.php @@ -0,0 +1,39 @@ +<?php +/* SVN FILE: $Id: routes.php 7945 2008-12-19 02:16:01Z gwoo $ */ +/** + * Short description for file. + * + * In this file, you set up routes to your controllers and their actions. + * Routes are very important mechanism that allows you to freely connect + * different urls to chosen controllers and their actions (functions). + * + * PHP versions 4 and 5 + * + * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org) + * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project + * @package cake + * @subpackage cake.app.config + * @since CakePHP(tm) v 0.2.9 + * @version $Revision: 7945 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2008-12-18 18:16:01 -0800 (Thu, 18 Dec 2008) $ + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ +/** + * Here, we are connecting '/' (base path) to controller called 'Pages', + * its action called 'display', and we pass a param to select the view file + * to use (in this case, /app/views/pages/home.ctp)... + */ + Router::connect('/', array('controller' => 'welcome', 'action' => 'index')); +/** + * ...and connect the rest of 'Pages' controller's urls. + */ + Router::connect('/pages/*', array('controller' => 'pages', 'action' => 'display')); +?> \ No newline at end of file diff --git a/app/config/sql/db_acl.php b/app/config/sql/db_acl.php new file mode 100755 index 0000000..5f24eab --- /dev/null +++ b/app/config/sql/db_acl.php @@ -0,0 +1,79 @@ +<?php +/* SVN FILE: $Id: db_acl.php 7945 2008-12-19 02:16:01Z gwoo $ */ +/*DbAcl schema generated on: 2007-11-24 15:11:13 : 1195945453*/ +/** + * This is Acl Schema file + * + * Use it to configure database for ACL + * + * PHP versions 4 and 5 + * + * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org) + * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project + * @package cake + * @subpackage cake.app.config.sql + * @since CakePHP(tm) v 0.2.9 + * @version $Revision: 7945 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2008-12-18 18:16:01 -0800 (Thu, 18 Dec 2008) $ + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ +/* + * + * Using the Schema command line utility + * cake schema run create DbAcl + * + */ +class DbAclSchema extends CakeSchema { + + var $name = 'DbAcl'; + + function before($event = array()) { + return true; + } + + function after($event = array()) { + } + + var $acos = array( + 'id' => array('type'=>'integer', 'null' => false, 'default' => NULL, 'length' => 10, 'key' => 'primary'), + 'parent_id' => array('type'=>'integer', 'null' => true, 'default' => NULL, 'length' => 10), + 'model' => array('type'=>'string', 'null' => true), + 'foreign_key' => array('type'=>'integer', 'null' => true, 'default' => NULL, 'length' => 10), + 'alias' => array('type'=>'string', 'null' => true), + 'lft' => array('type'=>'integer', 'null' => true, 'default' => NULL, 'length' => 10), + 'rght' => array('type'=>'integer', 'null' => true, 'default' => NULL, 'length' => 10), + 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1)) + ); + + var $aros = array( + 'id' => array('type'=>'integer', 'null' => false, 'default' => NULL, 'length' => 10, 'key' => 'primary'), + 'parent_id' => array('type'=>'integer', 'null' => true, 'default' => NULL, 'length' => 10), + 'model' => array('type'=>'string', 'null' => true), + 'foreign_key' => array('type'=>'integer', 'null' => true, 'default' => NULL, 'length' => 10), + 'alias' => array('type'=>'string', 'null' => true), + 'lft' => array('type'=>'integer', 'null' => true, 'default' => NULL, 'length' => 10), + 'rght' => array('type'=>'integer', 'null' => true, 'default' => NULL, 'length' => 10), + 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1)) + ); + + var $aros_acos = array( + 'id' => array('type'=>'integer', 'null' => false, 'default' => NULL, 'length' => 10, 'key' => 'primary'), + 'aro_id' => array('type'=>'integer', 'null' => false, 'length' => 10, 'key' => 'index'), + 'aco_id' => array('type'=>'integer', 'null' => false, 'length' => 10), + '_create' => array('type'=>'string', 'null' => false, 'default' => '0', 'length' => 2), + '_read' => array('type'=>'string', 'null' => false, 'default' => '0', 'length' => 2), + '_update' => array('type'=>'string', 'null' => false, 'default' => '0', 'length' => 2), + '_delete' => array('type'=>'string', 'null' => false, 'default' => '0', 'length' => 2), + 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1), 'ARO_ACO_KEY' => array('column' => array('aro_id', 'aco_id'), 'unique' => 1)) + ); + +} +?> \ No newline at end of file diff --git a/app/config/sql/db_acl.sql b/app/config/sql/db_acl.sql new file mode 100755 index 0000000..080f06c --- /dev/null +++ b/app/config/sql/db_acl.sql @@ -0,0 +1,40 @@ +# $Id: db_acl.sql 7945 2008-12-19 02:16:01Z gwoo $ +# +# Copyright 2005-2008, Cake Software Foundation, Inc. +# +# Licensed under The MIT License +# Redistributions of files must retain the above copyright notice. +# http://www.opensource.org/licenses/mit-license.php The MIT License + +CREATE TABLE acos ( + id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + parent_id INTEGER(10) DEFAULT NULL, + model VARCHAR(255) DEFAULT '', + foreign_key INTEGER(10) UNSIGNED DEFAULT NULL, + alias VARCHAR(255) DEFAULT '', + lft INTEGER(10) DEFAULT NULL, + rght INTEGER(10) DEFAULT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE aros_acos ( + id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + aro_id INTEGER(10) UNSIGNED NOT NULL, + aco_id INTEGER(10) UNSIGNED NOT NULL, + _create CHAR(2) NOT NULL DEFAULT 0, + _read CHAR(2) NOT NULL DEFAULT 0, + _update CHAR(2) NOT NULL DEFAULT 0, + _delete CHAR(2) NOT NULL DEFAULT 0, + PRIMARY KEY(id) +); + +CREATE TABLE aros ( + id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + parent_id INTEGER(10) DEFAULT NULL, + model VARCHAR(255) DEFAULT '', + foreign_key INTEGER(10) UNSIGNED DEFAULT NULL, + alias VARCHAR(255) DEFAULT '', + lft INTEGER(10) DEFAULT NULL, + rght INTEGER(10) DEFAULT NULL, + PRIMARY KEY (id) +); diff --git a/app/config/sql/i18n.php b/app/config/sql/i18n.php new file mode 100755 index 0000000..72233ff --- /dev/null +++ b/app/config/sql/i18n.php @@ -0,0 +1,56 @@ +<?php +/* SVN FILE: $Id: i18n.php 7945 2008-12-19 02:16:01Z gwoo $ */ +/*i18n schema generated on: 2007-11-25 07:11:25 : 1196004805*/ +/** + * This is i18n Schema file + * + * Use it to configure database for i18n + * + * PHP versions 4 and 5 + * + * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org) + * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project + * @package cake + * @subpackage cake.app.config.sql + * @since CakePHP(tm) v 0.2.9 + * @version $Revision: 7945 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2008-12-18 18:16:01 -0800 (Thu, 18 Dec 2008) $ + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ +/* + * + * Using the Schema command line utility + * cake schema run create i18n + * + */ +class i18nSchema extends CakeSchema { + + var $name = 'i18n'; + + function before($event = array()) { + return true; + } + + function after($event = array()) { + } + + var $i18n = array( + 'id' => array('type'=>'integer', 'null' => false, 'default' => NULL, 'length' => 10, 'key' => 'primary'), + 'locale' => array('type'=>'string', 'null' => false, 'length' => 6, 'key' => 'index'), + 'model' => array('type'=>'string', 'null' => false, 'key' => 'index'), + 'foreign_key' => array('type'=>'integer', 'null' => false, 'length' => 10, 'key' => 'index'), + 'field' => array('type'=>'string', 'null' => false, 'key' => 'index'), + 'content' => array('type'=>'text', 'null' => true, 'default' => NULL), + 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1), 'locale' => array('column' => 'locale', 'unique' => 0), 'model' => array('column' => 'model', 'unique' => 0), 'row_id' => array('column' => 'foreign_key', 'unique' => 0), 'field' => array('column' => 'field', 'unique' => 0)) + ); + +} +?> \ No newline at end of file diff --git a/app/config/sql/i18n.sql b/app/config/sql/i18n.sql new file mode 100755 index 0000000..4660a5a --- /dev/null +++ b/app/config/sql/i18n.sql @@ -0,0 +1,26 @@ +# $Id: i18n.sql 7945 2008-12-19 02:16:01Z gwoo $ +# +# Copyright 2005-2008, Cake Software Foundation, Inc. +# +# Licensed under The MIT License +# Redistributions of files must retain the above copyright notice. +# http://www.opensource.org/licenses/mit-license.php The MIT License + +CREATE TABLE i18n ( + id int(10) NOT NULL auto_increment, + locale varchar(6) NOT NULL, + model varchar(255) NOT NULL, + foreign_key int(10) NOT NULL, + field varchar(255) NOT NULL, + content mediumtext, + PRIMARY KEY (id), +# UNIQUE INDEX I18N_LOCALE_FIELD(locale, model, foreign_key, field), +# INDEX I18N_LOCALE_ROW(locale, model, foreign_key), +# INDEX I18N_LOCALE_MODEL(locale, model), +# INDEX I18N_FIELD(model, foreign_key, field), +# INDEX I18N_ROW(model, foreign_key), + INDEX locale (locale), + INDEX model (model), + INDEX row_id (foreign_key), + INDEX field (field) +); diff --git a/app/config/sql/sessions.php b/app/config/sql/sessions.php new file mode 100755 index 0000000..7f00a26 --- /dev/null +++ b/app/config/sql/sessions.php @@ -0,0 +1,53 @@ +<?php +/* SVN FILE: $Id: sessions.php 7945 2008-12-19 02:16:01Z gwoo $ */ +/*Sessions schema generated on: 2007-11-25 07:11:54 : 1196004714*/ +/** + * This is Sessions Schema file + * + * Use it to configure database for Sessions + * + * PHP versions 4 and 5 + * + * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org) + * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project + * @package cake + * @subpackage cake.app.config.sql + * @since CakePHP(tm) v 0.2.9 + * @version $Revision: 7945 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2008-12-18 18:16:01 -0800 (Thu, 18 Dec 2008) $ + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ +/* + * + * Using the Schema command line utility + * cake schema run create Sessions + * + */ +class SessionsSchema extends CakeSchema { + + var $name = 'Sessions'; + + function before($event = array()) { + return true; + } + + function after($event = array()) { + } + + var $cake_sessions = array( + 'id' => array('type'=>'string', 'null' => false, 'key' => 'primary'), + 'data' => array('type'=>'text', 'null' => true, 'default' => NULL), + 'expires' => array('type'=>'integer', 'null' => true, 'default' => NULL), + 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1)) + ); + +} +?> \ No newline at end of file diff --git a/app/config/sql/sessions.sql b/app/config/sql/sessions.sql new file mode 100755 index 0000000..23a1925 --- /dev/null +++ b/app/config/sql/sessions.sql @@ -0,0 +1,16 @@ +# $Id: sessions.sql 7118 2008-06-04 20:49:29Z gwoo $ +# +# Copyright 2005-2008, Cake Software Foundation, Inc. +# 1785 E. Sahara Avenue, Suite 490-204 +# Las Vegas, Nevada 89104 +# +# Licensed under The MIT License +# Redistributions of files must retain the above copyright notice. +# http://www.opensource.org/licenses/mit-license.php The MIT License + +CREATE TABLE cake_sessions ( + id varchar(255) NOT NULL default '', + data text, + expires int(11) default NULL, + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/app/controllers/account_controller.php b/app/controllers/account_controller.php new file mode 100644 index 0000000..6bda7c8 --- /dev/null +++ b/app/controllers/account_controller.php @@ -0,0 +1,195 @@ +<?php +## Redmine - project management software +## Copyright (C) 2006-2008 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 +# helper :custom_fields +# include CustomFieldsHelper +# +# # prevents login action to be filtered by check_if_login_required application scope filter +# skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register, :activate] +# +# # Show user's account +# def show +# @user = User.active.find(params[:id]) +# @custom_values = @user.custom_values +# +# # show only public projects and private projects that the logged in user is also a member of +# @memberships = @user.memberships.select do |membership| +# membership.project.is_public? || (User.current.member_of?(membership.project)) +# end +# +# events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10) +# @events_by_day = events.group_by(&:event_date) +# +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# # Login request and validation +# def login +# if request.get? +# # Logout user +# self.logged_user = nil +# else +# # Authenticate user +# user = User.try_to_login(params[:username], params[:password]) +# if user.nil? +# # Invalid credentials +# flash.now[:error] = l(:notice_account_invalid_creditentials) +# elsif user.new_record? +# # Onthefly creation failed, display the registration form to fill/fix attributes +# @user = user +# session[:auth_source_registration] = {:login => user.login, :auth_source_id => user.auth_source_id } +# render :action => 'register' +# else +# # Valid user +# self.logged_user = user +# # generate a key and set cookie if autologin +# if params[:autologin] && Setting.autologin? +# token = Token.create(:user => user, :action => 'autologin') +# cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now } +# end +# redirect_back_or_default :controller => 'my', :action => 'page' +# end +# end +# end +# +# # Log out current user and redirect to welcome page +# def logout +# cookies.delete :autologin +# Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) if User.current.logged? +# self.logged_user = nil +# redirect_to home_url +# end +# +# # Enable user to choose a new password +# def lost_password +# redirect_to(home_url) && return unless Setting.lost_password? +# if params[:token] +# @token = Token.find_by_action_and_value("recovery", params[:token]) +# redirect_to(home_url) && return unless @token and !@token.expired? +# @user = @token.user +# if request.post? +# @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation] +# if @user.save +# @token.destroy +# flash[:notice] = l(:notice_account_password_updated) +# redirect_to :action => 'login' +# return +# end +# end +# render :template => "account/password_recovery" +# return +# else +# if request.post? +# user = User.find_by_mail(params[:mail]) +# # user not found in db +# flash.now[:error] = l(:notice_account_unknown_email) and return unless user +# # user uses an external authentification +# flash.now[:error] = l(:notice_can_t_change_password) and return if user.auth_source_id +# # create a new token for password recovery +# token = Token.new(:user => user, :action => "recovery") +# if token.save +# Mailer.deliver_lost_password(token) +# flash[:notice] = l(:notice_account_lost_email_sent) +# redirect_to :action => 'login' +# return +# end +# end +# end +# end +# +# # User self-registration +# def register +# redirect_to(home_url) && return unless Setting.self_registration? || session[:auth_source_registration] +# if request.get? +# session[:auth_source_registration] = nil +# @user = User.new(:language => Setting.default_language) +# else +# @user = User.new(params[:user]) +# @user.admin = false +# @user.status = User::STATUS_REGISTERED +# if session[:auth_source_registration] +# @user.status = User::STATUS_ACTIVE +# @user.login = session[:auth_source_registration][:login] +# @user.auth_source_id = session[:auth_source_registration][:auth_source_id] +# if @user.save +# session[:auth_source_registration] = nil +# self.logged_user = @user +# flash[:notice] = l(:notice_account_activated) +# redirect_to :controller => 'my', :action => 'account' +# end +# else +# @user.login = params[:user][:login] +# @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] +# case Setting.self_registration +# when '1' +# # Email activation +# token = Token.new(:user => @user, :action => "register") +# if @user.save and token.save +# Mailer.deliver_register(token) +# flash[:notice] = l(:notice_account_register_done) +# redirect_to :action => 'login' +# end +# when '3' +# # Automatic activation +# @user.status = User::STATUS_ACTIVE +# if @user.save +# self.logged_user = @user +# flash[:notice] = l(:notice_account_activated) +# redirect_to :controller => 'my', :action => 'account' +# end +# else +# # Manual activation by the administrator +# if @user.save +# # Sends an email to the administrators +# Mailer.deliver_account_activation_request(@user) +# flash[:notice] = l(:notice_account_pending) +# redirect_to :action => 'login' +# end +# end +# end +# end +# end +# +# # Token based account activation +# def activate +# redirect_to(home_url) && return unless Setting.self_registration? && params[:token] +# token = Token.find_by_action_and_value('register', params[:token]) +# redirect_to(home_url) && return unless token and !token.expired? +# user = token.user +# redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED +# user.status = User::STATUS_ACTIVE +# if user.save +# token.destroy +# flash[:notice] = l(:notice_account_activated) +# end +# redirect_to :action => 'login' +# end +# +#private +# def logged_user=(user) +# if user && user.is_a?(User) +# User.current = user +# session[:user_id] = user.id +# else +# User.current = User.anonymous +# session[:user_id] = nil +# end +# end +#end diff --git a/app/controllers/admin_controller.php b/app/controllers/admin_controller.php new file mode 100644 index 0000000..a5d3672 --- /dev/null +++ b/app/controllers/admin_controller.php @@ -0,0 +1,94 @@ +<?php +## 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 +# before_filter :require_admin +# +# helper :sort +# include SortHelper +# +# def index +# @no_configuration_data = Redmine::DefaultData::Loader::no_data? +# end +# +# def projects +# sort_init 'name', 'asc' +# sort_update %w(name is_public created_on) +# +# @status = params[:status] ? params[:status].to_i : 1 +# c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status]) +# +# unless params[:name].blank? +# name = "%#{params[:name].strip.downcase}%" +# c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name] +# end +# +# @project_count = Project.count(:conditions => c.conditions) +# @project_pages = Paginator.new self, @project_count, +# per_page_option, +# params['page'] +# @projects = Project.find :all, :order => sort_clause, +# :conditions => c.conditions, +# :limit => @project_pages.items_per_page, +# :offset => @project_pages.current.offset +# +# render :action => "projects", :layout => false if request.xhr? +# end +# +# def plugins +# @plugins = Redmine::Plugin.all +# end +# +# # Loads the default configuration +# # (roles, trackers, statuses, workflow, enumerations) +# def default_configuration +# if request.post? +# begin +# Redmine::DefaultData::Loader::load(params[:lang]) +# flash[:notice] = l(:notice_default_data_loaded) +# rescue Exception => e +# flash[:error] = l(:error_can_t_load_default_data, e.message) +# end +# end +# redirect_to :action => 'index' +# end +# +# def test_email +# raise_delivery_errors = ActionMailer::Base.raise_delivery_errors +# # Force ActionMailer to raise delivery errors so we can catch it +# ActionMailer::Base.raise_delivery_errors = true +# begin +# @test = Mailer.deliver_test(User.current) +# flash[:notice] = l(:notice_email_sent, User.current.mail) +# rescue Exception => e +# flash[:error] = l(:notice_email_error, e.message) +# end +# ActionMailer::Base.raise_delivery_errors = raise_delivery_errors +# redirect_to :controller => 'settings', :action => 'edit', :tab => 'notifications' +# end +# +# def info +# @db_adapter_name = ActiveRecord::Base.connection.adapter_name +# @flags = { +# :default_admin_changed => User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?, +# :file_repository_writable => File.writable?(Attachment.storage_path), +# :plugin_assets_writable => File.writable?(Engines.public_directory), +# :rmagick_available => Object.const_defined?(:Magick) +# } +# end +#end diff --git a/app/controllers/app_controller.php b/app/controllers/app_controller.php new file mode 100755 index 0000000..4495aba --- /dev/null +++ b/app/controllers/app_controller.php @@ -0,0 +1,230 @@ +<?php +#require 'uri' +#require 'cgi' +# +class AppController extends Controller { + var $layout = 'base'; + var $helpers = array('Html','Form','Javascript','Candy'); + + function beforeFilter() + { + $this->user_setup(); + //$this->check_if_login_required(); + //$this->set_localzation(); + + } +# filter_parameter_logging :password +# +# include Redmine::MenuManager::MenuController +# helper Redmine::MenuManager::MenuHelper +# +# REDMINE_SUPPORTED_SCM.each do |scm| +# require_dependency "repository/#{scm.underscore}" +# end +# +# def current_role +# @current_role ||= User.current.role_for_project(@project) +# end +# + function user_setup() + { + $this->set('currentuser',$this->find_current_user()); +# def user_setup +# # Check the settings cache for each request +# Setting.check_cache +# # Find the current user +# User.current = find_current_user +# end + } + function find_current_user() { +# # Returns the current user or nil if no user is logged in +# def find_current_user +# if session[:user_id] +# # existing session +# (User.active.find(session[:user_id]) rescue nil) +# elsif cookies[:autologin] && Setting.autologin? +# # auto-login feature +# User.find_by_autologin_key(cookies[:autologin]) +# elsif params[:key] && accept_key_auth_actions.include?(params[:action]) +# # RSS key authentication +# User.find_by_rss_key(params[:key]) +# end +# end + return array('id' => 3,'name' => 'yando','logged' => true); + } +# # check if login is globally required to access the application +# def check_if_login_required +# # no check needed if user is already logged in +# return true if User.current.logged? +# require_login if Setting.login_required? +# end +# +# def set_localization +# User.current.language = nil unless User.current.logged? +# lang = begin +# if !User.current.language.blank? && GLoc.valid_language?(User.current.language) +# User.current.language +# elsif request.env['HTTP_ACCEPT_LANGUAGE'] +# accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase +# if !accept_lang.blank? && (GLoc.valid_language?(accept_lang) || GLoc.valid_language?(accept_lang = accept_lang.split('-').first)) +# User.current.language = accept_lang +# end +# end +# rescue +# nil +# end || Setting.default_language +# set_language_if_valid(lang) +# end +# +# def require_login +# if !User.current.logged? +# redirect_to :controller => "account", :action => "login", :back_url => url_for(params) +# return false +# end +# true +# end +# +# def require_admin +# return unless require_login +# if !User.current.admin? +# render_403 +# return false +# end +# true +# end +# +# def deny_access +# User.current.logged? ? render_403 : require_login +# end +# +# # Authorize the user for the requested action +# def authorize(ctrl = params[:controller], action = params[:action]) +# allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project) +# allowed ? true : deny_access +# end +# +# # make sure that the user is a member of the project (or admin) if project is private +# # used as a before_filter for actions that do not require any particular permission on the project +# def check_project_privacy +# if @project && @project.active? +# if @project.is_public? || User.current.member_of?(@project) || User.current.admin? +# true +# else +# User.current.logged? ? render_403 : require_login +# end +# else +# @project = nil +# render_404 +# false +# end +# end +# +# def redirect_back_or_default(default) +# back_url = CGI.unescape(params[:back_url].to_s) +# if !back_url.blank? +# begin +# uri = URI.parse(back_url) +# # do not redirect user to another host or to the login or register page +# if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)}) +# redirect_to(back_url) and return +# end +# rescue URI::InvalidURIError +# # redirect to default +# end +# end +# redirect_to default +# end +# +# def render_403 +# @project = nil +# render :template => "common/403", :layout => !request.xhr?, :status => 403 +# return false +# end +# +# def render_404 +# render :template => "common/404", :layout => !request.xhr?, :status => 404 +# return false +# end +# +# def render_error(msg) +# flash.now[:error] = msg +# render :nothing => true, :layout => !request.xhr?, :status => 500 +# end +# +# def render_feed(items, options={}) +# @items = items || [] +# @items.sort! {|x,y| y.event_datetime <=> x.event_datetime } +# @items = @items.slice(0, Setting.feeds_limit.to_i) +# @title = options[:title] || Setting.app_title +# render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml' +# end +# +# def self.accept_key_auth(*actions) +# actions = actions.flatten.map(&:to_s) +# write_inheritable_attribute('accept_key_auth_actions', actions) +# end +# +# def accept_key_auth_actions +# self.class.read_inheritable_attribute('accept_key_auth_actions') || [] +# end +# +# # TODO: move to model +# def attach_files(obj, attachments) +# attached = [] +# unsaved = [] +# if attachments && attachments.is_a?(Hash) +# attachments.each_value do |attachment| +# file = attachment['file'] +# next unless file && file.size > 0 +# a = Attachment.create(:container => obj, +# :file => file, +# :description => attachment['description'].to_s.strip, +# :author => User.current) +# a.new_record? ? (unsaved << a) : (attached << a) +# end +# if unsaved.any? +# flash[:warning] = l(:warning_attachments_not_saved, unsaved.size) +# end +# end +# attached +# end +# +# # Returns the number of objects that should be displayed +# # on the paginated list +# def per_page_option +# per_page = nil +# if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i) +# per_page = params[:per_page].to_s.to_i +# session[:per_page] = per_page +# elsif session[:per_page] +# per_page = session[:per_page] +# else +# per_page = Setting.per_page_options_array.first || 25 +# end +# per_page +# end +# +# # qvalues http header parser +# # code taken from webrick +# def parse_qvalues(value) +# tmp = [] +# if value +# parts = value.split(/,\s*/) +# parts.each {|part| +# if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part) +# val = m[1] +# q = (m[2] or 1).to_f +# tmp.push([val, q]) +# end +# } +# tmp = tmp.sort_by{|val, q| -q} +# tmp.collect!{|val, q| val} +# end +# return tmp +# end +# +# # Returns a string that can be used as filename value in Content-Disposition header +# def filename_for_content_disposition(name) +# request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name +# end +} diff --git a/app/controllers/application.php b/app/controllers/application.php new file mode 100644 index 0000000..f6e9e70 --- /dev/null +++ b/app/controllers/application.php @@ -0,0 +1,235 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#require 'uri' +#require 'cgi' +# +#class ApplicationController < ActionController::Base +# layout 'base' +# +# before_filter :user_setup, :check_if_login_required, :set_localization +# filter_parameter_logging :password +# +# include Redmine::MenuManager::MenuController +# helper Redmine::MenuManager::MenuHelper +# +# REDMINE_SUPPORTED_SCM.each do |scm| +# require_dependency "repository/#{scm.underscore}" +# end +# +# def current_role +# @current_role ||= User.current.role_for_project(@project) +# end +# +# def user_setup +# # Check the settings cache for each request +# Setting.check_cache +# # Find the current user +# User.current = find_current_user +# end +# +# # Returns the current user or nil if no user is logged in +# def find_current_user +# if session[:user_id] +# # existing session +# (User.active.find(session[:user_id]) rescue nil) +# elsif cookies[:autologin] && Setting.autologin? +# # auto-login feature +# User.find_by_autologin_key(cookies[:autologin]) +# elsif params[:key] && accept_key_auth_actions.include?(params[:action]) +# # RSS key authentication +# User.find_by_rss_key(params[:key]) +# end +# end +# +# # check if login is globally required to access the application +# def check_if_login_required +# # no check needed if user is already logged in +# return true if User.current.logged? +# require_login if Setting.login_required? +# end +# +# def set_localization +# User.current.language = nil unless User.current.logged? +# lang = begin +# if !User.current.language.blank? && GLoc.valid_language?(User.current.language) +# User.current.language +# elsif request.env['HTTP_ACCEPT_LANGUAGE'] +# accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase +# if !accept_lang.blank? && (GLoc.valid_language?(accept_lang) || GLoc.valid_language?(accept_lang = accept_lang.split('-').first)) +# User.current.language = accept_lang +# end +# end +# rescue +# nil +# end || Setting.default_language +# set_language_if_valid(lang) +# end +# +# def require_login +# if !User.current.logged? +# redirect_to :controller => "account", :action => "login", :back_url => url_for(params) +# return false +# end +# true +# end +# +# def require_admin +# return unless require_login +# if !User.current.admin? +# render_403 +# return false +# end +# true +# end +# +# def deny_access +# User.current.logged? ? render_403 : require_login +# end +# +# # Authorize the user for the requested action +# def authorize(ctrl = params[:controller], action = params[:action]) +# allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project) +# allowed ? true : deny_access +# end +# +# # make sure that the user is a member of the project (or admin) if project is private +# # used as a before_filter for actions that do not require any particular permission on the project +# def check_project_privacy +# if @project && @project.active? +# if @project.is_public? || User.current.member_of?(@project) || User.current.admin? +# true +# else +# User.current.logged? ? render_403 : require_login +# end +# else +# @project = nil +# render_404 +# false +# end +# end +# +# def redirect_back_or_default(default) +# back_url = CGI.unescape(params[:back_url].to_s) +# if !back_url.blank? +# begin +# uri = URI.parse(back_url) +# # do not redirect user to another host or to the login or register page +# if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)}) +# redirect_to(back_url) and return +# end +# rescue URI::InvalidURIError +# # redirect to default +# end +# end +# redirect_to default +# end +# +# def render_403 +# @project = nil +# render :template => "common/403", :layout => !request.xhr?, :status => 403 +# return false +# end +# +# def render_404 +# render :template => "common/404", :layout => !request.xhr?, :status => 404 +# return false +# end +# +# def render_error(msg) +# flash.now[:error] = msg +# render :nothing => true, :layout => !request.xhr?, :status => 500 +# end +# +# def render_feed(items, options={}) +# @items = items || [] +# @items.sort! {|x,y| y.event_datetime <=> x.event_datetime } +# @items = @items.slice(0, Setting.feeds_limit.to_i) +# @title = options[:title] || Setting.app_title +# render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml' +# end +# +# def self.accept_key_auth(*actions) +# actions = actions.flatten.map(&:to_s) +# write_inheritable_attribute('accept_key_auth_actions', actions) +# end +# +# def accept_key_auth_actions +# self.class.read_inheritable_attribute('accept_key_auth_actions') || [] +# end +# +# # TODO: move to model +# def attach_files(obj, attachments) +# attached = [] +# unsaved = [] +# if attachments && attachments.is_a?(Hash) +# attachments.each_value do |attachment| +# file = attachment['file'] +# next unless file && file.size > 0 +# a = Attachment.create(:container => obj, +# :file => file, +# :description => attachment['description'].to_s.strip, +# :author => User.current) +# a.new_record? ? (unsaved << a) : (attached << a) +# end +# if unsaved.any? +# flash[:warning] = l(:warning_attachments_not_saved, unsaved.size) +# end +# end +# attached +# end +# +# # Returns the number of objects that should be displayed +# # on the paginated list +# def per_page_option +# per_page = nil +# if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i) +# per_page = params[:per_page].to_s.to_i +# session[:per_page] = per_page +# elsif session[:per_page] +# per_page = session[:per_page] +# else +# per_page = Setting.per_page_options_array.first || 25 +# end +# per_page +# end +# +# # qvalues http header parser +# # code taken from webrick +# def parse_qvalues(value) +# tmp = [] +# if value +# parts = value.split(/,\s*/) +# parts.each {|part| +# if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part) +# val = m[1] +# q = (m[2] or 1).to_f +# tmp.push([val, q]) +# end +# } +# tmp = tmp.sort_by{|val, q| -q} +# tmp.collect!{|val, q| val} +# end +# return tmp +# end +# +# # Returns a string that can be used as filename value in Content-Disposition header +# def filename_for_content_disposition(name) +# request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name +# end +#end diff --git a/app/controllers/attachments_controller.php b/app/controllers/attachments_controller.php new file mode 100644 index 0000000..e9cafe4 --- /dev/null +++ b/app/controllers/attachments_controller.php @@ -0,0 +1,75 @@ +<?php +## Redmine - project management software +## Copyright (C) 2006-2008 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class AttachmentsController < ApplicationController +# before_filter :find_project +# before_filter :read_authorize, :except => :destroy +# before_filter :delete_authorize, :only => :destroy +# +# verify :method => :post, :only => :destroy +# +# def show +# if @attachment.is_diff? +# @diff = File.new(@attachment.diskfile, "rb").read +# render :action => 'diff' +# elsif @attachment.is_text? +# @content = File.new(@attachment.diskfile, "rb").read +# render :action => 'file' +# elsif +# download +# end +# end +# +# def download +# if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project) +# @attachment.increment_download +# end +# +# # images are sent inline +# send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename), +# :type => @attachment.content_type, +# :disposition => (@attachment.image? ? 'inline' : 'attachment') +# +# end +# +# def destroy +# # Make sure association callbacks are called +# @attachment.container.attachments.delete(@attachment) +# redirect_to :back +# rescue ::ActionController::RedirectBackError +# redirect_to :controller => 'projects', :action => 'show', :id => @project +# end +# +#private +# def find_project +# @attachment = Attachment.find(params[:id]) +# # Show 404 if the filename in the url is wrong +# raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename +# @project = @attachment.project +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# def read_authorize +# @attachment.visible? ? true : deny_access +# end +# +# def delete_authorize +# @attachment.deletable? ? true : deny_access +# end +#end diff --git a/app/controllers/auth_sources_controller.php b/app/controllers/auth_sources_controller.php new file mode 100644 index 0000000..fd2b6c5 --- /dev/null +++ b/app/controllers/auth_sources_controller.php @@ -0,0 +1,83 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class AuthSourcesController < ApplicationController +# before_filter :require_admin +# +# def index +# list +# render :action => 'list' unless request.xhr? +# end +# +# # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html) +# verify :method => :post, :only => [ :destroy, :create, :update ], +# :redirect_to => { :action => :list } +# +# def list +# @auth_source_pages, @auth_sources = paginate :auth_sources, :per_page => 10 +# render :action => "list", :layout => false if request.xhr? +# end +# +# def new +# @auth_source = AuthSourceLdap.new +# end +# +# def create +# @auth_source = AuthSourceLdap.new(params[:auth_source]) +# if @auth_source.save +# flash[:notice] = l(:notice_successful_create) +# redirect_to :action => 'list' +# else +# render :action => 'new' +# end +# end +# +# def edit +# @auth_source = AuthSource.find(params[:id]) +# end +# +# def update +# @auth_source = AuthSource.find(params[:id]) +# if @auth_source.update_attributes(params[:auth_source]) +# flash[:notice] = l(:notice_successful_update) +# redirect_to :action => 'list' +# else +# render :action => 'edit' +# end +# end +# +# def test_connection +# @auth_method = AuthSource.find(params[:id]) +# begin +# @auth_method.test_connection +# flash[:notice] = l(:notice_successful_connection) +# rescue => text +# flash[:error] = "Unable to connect (#{text})" +# end +# redirect_to :action => 'list' +# end +# +# def destroy +# @auth_source = AuthSource.find(params[:id]) +# unless @auth_source.users.find(:first) +# @auth_source.destroy +# flash[:notice] = l(:notice_successful_delete) +# end +# redirect_to :action => 'list' +# end +#end diff --git a/app/controllers/boards_controller.php b/app/controllers/boards_controller.php new file mode 100644 index 0000000..a79bb95 --- /dev/null +++ b/app/controllers/boards_controller.php @@ -0,0 +1,88 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class BoardsController < ApplicationController +# before_filter :find_project, :authorize +# +# helper :messages +# include MessagesHelper +# helper :sort +# include SortHelper +# helper :watchers +# include WatchersHelper +# +# def index +# @boards = @project.boards +# # show the board if there is only one +# if @boards.size == 1 +# @board = @boards.first +# show +# end +# end +# +# def show +# sort_init 'updated_on', 'desc' +# sort_update 'created_on' => "#{Message.table_name}.created_on", +# 'replies' => "#{Message.table_name}.replies_count", +# 'updated_on' => "#{Message.table_name}.updated_on" +# +# @topic_count = @board.topics.count +# @topic_pages = Paginator.new self, @topic_count, per_page_option, params['page'] +# @topics = @board.topics.find :all, :order => ["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '), +# :include => [:author, {:last_reply => :author}], +# :limit => @topic_pages.items_per_page, +# :offset => @topic_pages.current.offset +# render :action => 'show', :layout => !request.xhr? +# end +# +# verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index } +# +# def new +# @board = Board.new(params[:board]) +# @board.project = @project +# if request.post? && @board.save +# flash[:notice] = l(:notice_successful_create) +# redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards' +# end +# end +# +# def edit +# if request.post? && @board.update_attributes(params[:board]) +# case params[:position] +# when 'highest'; @board.move_to_top +# when 'higher'; @board.move_higher +# when 'lower'; @board.move_lower +# when 'lowest'; @board.move_to_bottom +# end if params[:position] +# redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards' +# end +# end +# +# def destroy +# @board.destroy +# redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards' +# end +# +#private +# def find_project +# @project = Project.find(params[:project_id]) +# @board = @project.boards.find(params[:id]) if params[:id] +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +#end diff --git a/app/controllers/components/empty b/app/controllers/components/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/controllers/custom_fields_controller.php b/app/controllers/custom_fields_controller.php new file mode 100644 index 0000000..bb91d4e --- /dev/null +++ b/app/controllers/custom_fields_controller.php @@ -0,0 +1,89 @@ +<?php +## 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 +# before_filter :require_admin +# +# def index +# list +# render :action => 'list' unless request.xhr? +# end +# +# def list +# @custom_fields_by_type = CustomField.find(:all).group_by {|f| f.class.name } +# @tab = params[:tab] || 'IssueCustomField' +# render :action => "list", :layout => false if request.xhr? +# end +# +# def new +# case params[:type] +# when "IssueCustomField" +# @custom_field = IssueCustomField.new(params[:custom_field]) +# @custom_field.trackers = Tracker.find(params[:tracker_ids]) if params[:tracker_ids] +# when "UserCustomField" +# @custom_field = UserCustomField.new(params[:custom_field]) +# when "ProjectCustomField" +# @custom_field = ProjectCustomField.new(params[:custom_field]) +# when "TimeEntryCustomField" +# @custom_field = TimeEntryCustomField.new(params[:custom_field]) +# else +# redirect_to :action => 'list' +# return +# end +# if request.post? and @custom_field.save +# flash[:notice] = l(:notice_successful_create) +# redirect_to :action => 'list', :tab => @custom_field.class.name +# end +# @trackers = Tracker.find(:all, :order => 'position') +# end +# +# def edit +# @custom_field = CustomField.find(params[:id]) +# if request.post? and @custom_field.update_attributes(params[:custom_field]) +# if @custom_field.is_a? IssueCustomField +# @custom_field.trackers = params[:tracker_ids] ? Tracker.find(params[:tracker_ids]) : [] +# end +# flash[:notice] = l(:notice_successful_update) +# redirect_to :action => 'list', :tab => @custom_field.class.name +# end +# @trackers = Tracker.find(:all, :order => 'position') +# end +# +# def move +# @custom_field = CustomField.find(params[:id]) +# case params[:position] +# when 'highest' +# @custom_field.move_to_top +# when 'higher' +# @custom_field.move_higher +# when 'lower' +# @custom_field.move_lower +# when 'lowest' +# @custom_field.move_to_bottom +# end if params[:position] +# redirect_to :action => 'list', :tab => @custom_field.class.name +# end +# +# def destroy +# @custom_field = CustomField.find(params[:id]).destroy +# redirect_to :action => 'list', :tab => @custom_field.class.name +# rescue +# flash[:error] = "Unable to delete custom field" +# redirect_to :action => 'list' +# end +#end diff --git a/app/controllers/documents_controller.php b/app/controllers/documents_controller.php new file mode 100644 index 0000000..6461c30 --- /dev/null +++ b/app/controllers/documents_controller.php @@ -0,0 +1,89 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class DocumentsController < ApplicationController +# before_filter :find_project, :only => [:index, :new] +# before_filter :find_document, :except => [:index, :new] +# before_filter :authorize +# +# helper :attachments +# +# def index +# @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category' +# documents = @project.documents.find :all, :include => [:attachments, :category] +# case @sort_by +# when 'date' +# @grouped = documents.group_by {|d| d.created_on.to_date } +# when 'title' +# @grouped = documents.group_by {|d| d.title.first.upcase} +# when 'author' +# @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author} +# else +# @grouped = documents.group_by(&:category) +# end +# @document = @project.documents.build +# render :layout => false if request.xhr? +# end +# +# def show +# @attachments = @document.attachments.find(:all, :order => "created_on DESC") +# end +# +# def new +# @document = @project.documents.build(params[:document]) +# if request.post? and @document.save +# attach_files(@document, params[:attachments]) +# flash[:notice] = l(:notice_successful_create) +# Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added') +# redirect_to :action => 'index', :project_id => @project +# end +# end +# +# def edit +# @categories = Enumeration::get_values('DCAT') +# if request.post? and @document.update_attributes(params[:document]) +# flash[:notice] = l(:notice_successful_update) +# redirect_to :action => 'show', :id => @document +# end +# end +# +# def destroy +# @document.destroy +# redirect_to :controller => 'documents', :action => 'index', :project_id => @project +# end +# +# def add_attachment +# attachments = attach_files(@document, params[:attachments]) +# Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added') +# redirect_to :action => 'show', :id => @document +# end +# +#private +# def find_project +# @project = Project.find(params[:project_id]) +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# def find_document +# @document = Document.find(params[:id]) +# @project = @document.project +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +#end diff --git a/app/controllers/enumerations_controller.php b/app/controllers/enumerations_controller.php new file mode 100644 index 0000000..4220aa7 --- /dev/null +++ b/app/controllers/enumerations_controller.php @@ -0,0 +1,94 @@ +<?php +## 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 +# before_filter :require_admin +# +# def index +# list +# render :action => 'list' +# end +# +# # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html) +# verify :method => :post, :only => [ :destroy, :create, :update ], +# :redirect_to => { :action => :list } +# +# def list +# end +# +# def new +# @enumeration = Enumeration.new(:opt => params[:opt]) +# end +# +# def create +# @enumeration = Enumeration.new(params[:enumeration]) +# if @enumeration.save +# flash[:notice] = l(:notice_successful_create) +# redirect_to :action => 'list', :opt => @enumeration.opt +# else +# render :action => 'new' +# end +# end +# +# def edit +# @enumeration = Enumeration.find(params[:id]) +# end +# +# def update +# @enumeration = Enumeration.find(params[:id]) +# if @enumeration.update_attributes(params[:enumeration]) +# flash[:notice] = l(:notice_successful_update) +# redirect_to :action => 'list', :opt => @enumeration.opt +# else +# render :action => 'edit' +# end +# end +# +# def move +# @enumeration = Enumeration.find(params[:id]) +# case params[:position] +# when 'highest' +# @enumeration.move_to_top +# when 'higher' +# @enumeration.move_higher +# when 'lower' +# @enumeration.move_lower +# when 'lowest' +# @enumeration.move_to_bottom +# end if params[:position] +# redirect_to :action => 'index' +# end +# +# def destroy +# @enumeration = Enumeration.find(params[:id]) +# if !@enumeration.in_use? +# # No associated objects +# @enumeration.destroy +# redirect_to :action => 'index' +# elsif params[:reassign_to_id] +# if reassign_to = Enumeration.find_by_opt_and_id(@enumeration.opt, params[:reassign_to_id]) +# @enumeration.destroy(reassign_to) +# redirect_to :action => 'index' +# end +# end +# @enumerations = Enumeration.get_values(@enumeration.opt) - [@enumeration] +# #rescue +# # flash[:error] = 'Unable to delete enumeration' +# # redirect_to :action => 'index' +# end +#end diff --git a/app/controllers/issue_categories_controller.php b/app/controllers/issue_categories_controller.php new file mode 100644 index 0000000..4ded1a5 --- /dev/null +++ b/app/controllers/issue_categories_controller.php @@ -0,0 +1,53 @@ +<?php +## 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 +# menu_item :settings +# before_filter :find_project, :authorize +# +# verify :method => :post, :only => :destroy +# +# def edit +# if request.post? and @category.update_attributes(params[:category]) +# flash[:notice] = l(:notice_successful_update) +# redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project +# end +# end +# +# def destroy +# @issue_count = @category.issues.size +# if @issue_count == 0 +# # No issue assigned to this category +# @category.destroy +# redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' +# elsif params[:todo] +# reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id]) if params[:todo] == 'reassign' +# @category.destroy(reassign_to) +# redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' +# end +# @categories = @project.issue_categories - [@category] +# end +# +#private +# def find_project +# @category = IssueCategory.find(params[:id]) +# @project = @category.project +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +#end diff --git a/app/controllers/issue_relations_controller.php b/app/controllers/issue_relations_controller.php new file mode 100644 index 0000000..53dd31e --- /dev/null +++ b/app/controllers/issue_relations_controller.php @@ -0,0 +1,59 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class IssueRelationsController < ApplicationController +# before_filter :find_project, :authorize +# +# def new +# @relation = IssueRelation.new(params[:relation]) +# @relation.issue_from = @issue +# @relation.save if request.post? +# respond_to do |format| +# format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue } +# format.js do +# render :update do |page| +# page.replace_html "relations", :partial => 'issues/relations' +# if @relation.errors.empty? +# page << "$('relation_delay').value = ''" +# page << "$('relation_issue_to_id').value = ''" +# end +# end +# end +# end +# end +# +# def destroy +# relation = IssueRelation.find(params[:id]) +# if request.post? && @issue.relations.include?(relation) +# relation.destroy +# @issue.reload +# end +# respond_to do |format| +# format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue } +# format.js { render(:update) {|page| page.replace_html "relations", :partial => 'issues/relations'} } +# end +# end +# +#private +# def find_project +# @issue = Issue.find(params[:issue_id]) +# @project = @issue.project +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +#end diff --git a/app/controllers/issue_statuses_controller.php b/app/controllers/issue_statuses_controller.php new file mode 100644 index 0000000..38c6939 --- /dev/null +++ b/app/controllers/issue_statuses_controller.php @@ -0,0 +1,85 @@ +<?php +## 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 +# before_filter :require_admin +# +# verify :method => :post, :only => [ :destroy, :create, :update, :move ], +# :redirect_to => { :action => :list } +# +# def index +# list +# render :action => 'list' unless request.xhr? +# end +# +# def list +# @issue_status_pages, @issue_statuses = paginate :issue_statuses, :per_page => 25, :order => "position" +# render :action => "list", :layout => false if request.xhr? +# end +# +# def new +# @issue_status = IssueStatus.new +# end +# +# def create +# @issue_status = IssueStatus.new(params[:issue_status]) +# if @issue_status.save +# flash[:notice] = l(:notice_successful_create) +# redirect_to :action => 'list' +# else +# render :action => 'new' +# end +# end +# +# def edit +# @issue_status = IssueStatus.find(params[:id]) +# end +# +# def update +# @issue_status = IssueStatus.find(params[:id]) +# if @issue_status.update_attributes(params[:issue_status]) +# flash[:notice] = l(:notice_successful_update) +# redirect_to :action => 'list' +# else +# render :action => 'edit' +# end +# end +# +# def move +# @issue_status = IssueStatus.find(params[:id]) +# case params[:position] +# when 'highest' +# @issue_status.move_to_top +# when 'higher' +# @issue_status.move_higher +# when 'lower' +# @issue_status.move_lower +# when 'lowest' +# @issue_status.move_to_bottom +# end if params[:position] +# redirect_to :action => 'list' +# end +# +# def destroy +# IssueStatus.find(params[:id]).destroy +# redirect_to :action => 'list' +# rescue +# flash[:error] = "Unable to delete issue status" +# redirect_to :action => 'list' +# end +#end diff --git a/app/controllers/issues_controller.php b/app/controllers/issues_controller.php new file mode 100644 index 0000000..e8c8b44 --- /dev/null +++ b/app/controllers/issues_controller.php @@ -0,0 +1,488 @@ +<?php +## Redmine - project management software +## Copyright (C) 2006-2008 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 +# menu_item :new_issue, :only => :new +# +# before_filter :find_issue, :only => [:show, :edit, :reply] +# before_filter :find_issues, :only => [:bulk_edit, :move, :destroy] +# before_filter :find_project, :only => [:new, :update_form, :preview] +# before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu] +# before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar] +# accept_key_auth :index, :changes +# +# helper :journals +# helper :projects +# include ProjectsHelper +# helper :custom_fields +# include CustomFieldsHelper +# helper :issue_relations +# include IssueRelationsHelper +# helper :watchers +# include WatchersHelper +# helper :attachments +# include AttachmentsHelper +# helper :queries +# helper :sort +# include SortHelper +# include IssuesHelper +# helper :timelog +# include Redmine::Export::PDF +# +# def index +# retrieve_query +# sort_init 'id', 'desc' +# sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h})) +# +# if @query.valid? +# limit = per_page_option +# respond_to do |format| +# format.html { } +# format.atom { } +# format.csv { limit = Setting.issues_export_limit.to_i } +# format.pdf { limit = Setting.issues_export_limit.to_i } +# end +# @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement) +# @issue_pages = Paginator.new self, @issue_count, limit, params['page'] +# @issues = Issue.find :all, :order => sort_clause, +# :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ], +# :conditions => @query.statement, +# :limit => limit, +# :offset => @issue_pages.current.offset +# respond_to do |format| +# format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? } +# format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") } +# format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') } +# format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') } +# end +# else +# # Send html if the query is not valid +# render(:template => 'issues/index.rhtml', :layout => !request.xhr?) +# end +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# def changes +# retrieve_query +# sort_init 'id', 'desc' +# sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h})) +# +# if @query.valid? +# @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ], +# :conditions => @query.statement, +# :limit => 25, +# :order => "#{Journal.table_name}.created_on DESC" +# end +# @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name) +# render :layout => false, :content_type => 'application/atom+xml' +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# def show +# @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC") +# @journals.each_with_index {|j,i| j.indice = i+1} +# @journals.reverse! if User.current.wants_comments_in_reverse_order? +# @allowed_statuses = @issue.new_statuses_allowed_to(User.current) +# @edit_allowed = User.current.allowed_to?(:edit_issues, @project) +# @priorities = Enumeration::get_values('IPRI') +# @time_entry = TimeEntry.new +# respond_to do |format| +# format.html { render :template => 'issues/show.rhtml' } +# format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' } +# format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") } +# end +# end +# +# # Add a new issue +# # The new issue will be created from an existing one if copy_from parameter is given +# def new +# @issue = Issue.new +# @issue.copy_from(params[:copy_from]) if params[:copy_from] +# @issue.project = @project +# # Tracker must be set before custom field values +# @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first) +# if @issue.tracker.nil? +# flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.' +# render :nothing => true, :layout => true +# return +# end +# if params[:issue].is_a?(Hash) +# @issue.attributes = params[:issue] +# @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project) +# end +# @issue.author = User.current +# +# default_status = IssueStatus.default +# unless default_status +# flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").' +# render :nothing => true, :layout => true +# return +# end +# @issue.status = default_status +# @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq +# +# if request.get? || request.xhr? +# @issue.start_date ||= Date.today +# else +# requested_status = IssueStatus.find_by_id(params[:issue][:status_id]) +# # Check that the user is allowed to apply the requested status +# @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status +# if @issue.save +# attach_files(@issue, params[:attachments]) +# flash[:notice] = l(:notice_successful_create) +# Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added') +# redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } : +# { :action => 'show', :id => @issue }) +# return +# end +# end +# @priorities = Enumeration::get_values('IPRI') +# render :layout => !request.xhr? +# end +# +# # Attributes that can be updated on workflow transition (without :edit permission) +# # TODO: make it configurable (at least per role) +# UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION) +# +# def edit +# @allowed_statuses = @issue.new_statuses_allowed_to(User.current) +# @priorities = Enumeration::get_values('IPRI') +# @edit_allowed = User.current.allowed_to?(:edit_issues, @project) +# @time_entry = TimeEntry.new +# +# @notes = params[:notes] +# journal = @issue.init_journal(User.current, @notes) +# # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed +# if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue] +# attrs = params[:issue].dup +# attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed +# attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s} +# @issue.attributes = attrs +# end +# +# if request.post? +# @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today) +# @time_entry.attributes = params[:time_entry] +# attachments = attach_files(@issue, params[:attachments]) +# attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} +# +# call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal}) +# +# if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save +# # Log spend time +# if User.current.allowed_to?(:log_time, @project) +# @time_entry.save +# end +# if !journal.new_record? +# # Only send notification if something was actually changed +# flash[:notice] = l(:notice_successful_update) +# Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') +# end +# redirect_to(params[:back_to] || {:action => 'show', :id => @issue}) +# end +# end +# rescue ActiveRecord::StaleObjectError +# # Optimistic locking exception +# flash.now[:error] = l(:notice_locking_conflict) +# end +# +# def reply +# journal = Journal.find(params[:journal_id]) if params[:journal_id] +# if journal +# user = journal.user +# text = journal.notes +# else +# user = @issue.author +# text = @issue.description +# end +# content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> " +# content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n" +# render(:update) { |page| +# page.<< "$('notes').value = \"#{content}\";" +# page.show 'update' +# page << "Form.Element.focus('notes');" +# page << "Element.scrollTo('update');" +# page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;" +# } +# end +# +# # Bulk edit a set of issues +# def bulk_edit +# if request.post? +# status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id]) +# priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id]) +# assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id]) +# category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id]) +# fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id]) +# +# unsaved_issue_ids = [] +# @issues.each do |issue| +# journal = issue.init_journal(User.current, params[:notes]) +# issue.priority = priority if priority +# issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none' +# issue.category = category if category || params[:category_id] == 'none' +# issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none' +# issue.start_date = params[:start_date] unless params[:start_date].blank? +# issue.due_date = params[:due_date] unless params[:due_date].blank? +# issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank? +# call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue }) +# # Don't save any change to the issue if the user is not authorized to apply the requested status +# if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save +# # Send notification for each issue (if changed) +# Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated') +# else +# # Keep unsaved issue ids to display them in flash error +# unsaved_issue_ids << issue.id +# end +# end +# if unsaved_issue_ids.empty? +# flash[:notice] = l(:notice_successful_update) unless @issues.empty? +# else +# flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #')) +# end +# redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project}) +# return +# end +# # Find potential statuses the user could be allowed to switch issues to +# @available_statuses = Workflow.find(:all, :include => :new_status, +# :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort +# end +# +# def move +# @allowed_projects = [] +# # find projects to which the user is allowed to move the issue +# if User.current.admin? +# # admin is allowed to move issues to any active (visible) project +# @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name') +# else +# User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)} +# end +# @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id] +# @target_project ||= @project +# @trackers = @target_project.trackers +# if request.post? +# new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id]) +# unsaved_issue_ids = [] +# @issues.each do |issue| +# issue.init_journal(User.current) +# unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker) +# end +# if unsaved_issue_ids.empty? +# flash[:notice] = l(:notice_successful_update) unless @issues.empty? +# else +# flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #')) +# end +# redirect_to :controller => 'issues', :action => 'index', :project_id => @project +# return +# end +# render :layout => false if request.xhr? +# end +# +# def destroy +# @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f +# if @hours > 0 +# case params[:todo] +# when 'destroy' +# # nothing to do +# when 'nullify' +# TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues]) +# when 'reassign' +# reassign_to = @project.issues.find_by_id(params[:reassign_to_id]) +# if reassign_to.nil? +# flash.now[:error] = l(:error_issue_not_found_in_project) +# return +# else +# TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues]) +# end +# else +# # display the destroy form +# return +# end +# end +# @issues.each(&:destroy) +# redirect_to :action => 'index', :project_id => @project +# end +# +# def gantt +# @gantt = Redmine::Helpers::Gantt.new(params) +# retrieve_query +# if @query.valid? +# events = [] +# # Issues that have start and due dates +# events += Issue.find(:all, +# :order => "start_date, due_date", +# :include => [:tracker, :status, :assigned_to, :priority, :project], +# :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to] +# ) +# # Issues that don't have a due date but that are assigned to a version with a date +# events += Issue.find(:all, +# :order => "start_date, effective_date", +# :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version], +# :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to] +# ) +# # Versions +# events += Version.find(:all, :include => :project, +# :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to]) +# +# @gantt.events = events +# end +# +# respond_to do |format| +# format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? } +# format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png") } if @gantt.respond_to?('to_image') +# format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") } +# end +# end +# +# def calendar +# if params[:year] and params[:year].to_i > 1900 +# @year = params[:year].to_i +# if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13 +# @month = params[:month].to_i +# end +# end +# @year ||= Date.today.year +# @month ||= Date.today.month +# +# @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month) +# retrieve_query +# if @query.valid? +# events = [] +# events += Issue.find(:all, +# :include => [:tracker, :status, :assigned_to, :priority, :project], +# :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt] +# ) +# events += Version.find(:all, :include => :project, +# :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt]) +# +# @calendar.events = events +# end +# +# render :layout => false if request.xhr? +# end +# +# def context_menu +# @issues = Issue.find_all_by_id(params[:ids], :include => :project) +# if (@issues.size == 1) +# @issue = @issues.first +# @allowed_statuses = @issue.new_statuses_allowed_to(User.current) +# end +# projects = @issues.collect(&:project).compact.uniq +# @project = projects.first if projects.size == 1 +# +# @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)), +# :log_time => (@project && User.current.allowed_to?(:log_time, @project)), +# :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))), +# :move => (@project && User.current.allowed_to?(:move_issues, @project)), +# :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), +# :delete => (@project && User.current.allowed_to?(:delete_issues, @project)) +# } +# if @project +# @assignables = @project.assignable_users +# @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to) +# end +# +# @priorities = Enumeration.get_values('IPRI').reverse +# @statuses = IssueStatus.find(:all, :order => 'position') +# @back = request.env['HTTP_REFERER'] +# +# render :layout => false +# end +# +# def update_form +# @issue = Issue.new(params[:issue]) +# render :action => :new, :layout => false +# end +# +# def preview +# @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank? +# @attachements = @issue.attachments if @issue +# @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil) +# render :partial => 'common/preview' +# end +# +#private +# def find_issue +# @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category]) +# @project = @issue.project +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# # Filter for bulk operations +# def find_issues +# @issues = Issue.find_all_by_id(params[:id] || params[:ids]) +# raise ActiveRecord::RecordNotFound if @issues.empty? +# projects = @issues.collect(&:project).compact.uniq +# if projects.size == 1 +# @project = projects.first +# else +# # TODO: let users bulk edit/move/destroy issues from different projects +# render_error 'Can not bulk edit/move/destroy issues from different projects' and return false +# end +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# def find_project +# @project = Project.find(params[:project_id]) +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# def find_optional_project +# @project = Project.find(params[:project_id]) unless params[:project_id].blank? +# allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true) +# allowed ? true : deny_access +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# # Retrieve query from session or build a new query +# def retrieve_query +# if !params[:query_id].blank? +# cond = "project_id IS NULL" +# cond << " OR project_id = #{@project.id}" if @project +# @query = Query.find(params[:query_id], :conditions => cond) +# @query.project = @project +# session[:query] = {:id => @query.id, :project_id => @query.project_id} +# else +# if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil) +# # Give it a name, required to be valid +# @query = Query.new(:name => "_") +# @query.project = @project +# if params[:fields] and params[:fields].is_a? Array +# params[:fields].each do |field| +# @query.add_filter(field, params[:operators][field], params[:values][field]) +# end +# else +# @query.available_filters.keys.each do |field| +# @query.add_short_filter(field, params[field]) if params[field] +# end +# end +# session[:query] = {:project_id => @query.project_id, :filters => @query.filters} +# else +# @query = Query.find_by_id(session[:query][:id]) if session[:query][:id] +# @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters]) +# @query.project = @project +# end +# end +# end +#end diff --git a/app/controllers/journals_controller.php b/app/controllers/journals_controller.php new file mode 100644 index 0000000..6650389 --- /dev/null +++ b/app/controllers/journals_controller.php @@ -0,0 +1,42 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2008 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 JournalsController < ApplicationController +# before_filter :find_journal +# +# def edit +# if request.post? +# @journal.update_attributes(:notes => params[:notes]) if params[:notes] +# @journal.destroy if @journal.details.empty? && @journal.notes.blank? +# call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params}) +# respond_to do |format| +# format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id } +# format.js { render :action => 'update' } +# end +# end +# end +# +#private +# def find_journal +# @journal = Journal.find(params[:id]) +# render_403 and return false unless @journal.editable_by?(User.current) +# @project = @journal.journalized.project +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +#end diff --git a/app/controllers/mail_handler_controller.php b/app/controllers/mail_handler_controller.php new file mode 100644 index 0000000..dc55b07 --- /dev/null +++ b/app/controllers/mail_handler_controller.php @@ -0,0 +1,45 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2008 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 MailHandlerController < ActionController::Base +# before_filter :check_credential +# +# verify :method => :post, +# :only => :index, +# :render => { :nothing => true, :status => 405 } +# +# # Submits an incoming email to MailHandler +# def index +# options = params.dup +# email = options.delete(:email) +# if MailHandler.receive(email, options) +# render :nothing => true, :status => :created +# else +# render :nothing => true, :status => :unprocessable_entity +# end +# end +# +# private +# +# def check_credential +# User.current = nil +# unless Setting.mail_handler_api_enabled? && params[:key] == Setting.mail_handler_api_key +# render :nothing => true, :status => 403 +# end +# end +#end diff --git a/app/controllers/members_controller.php b/app/controllers/members_controller.php new file mode 100644 index 0000000..039a2a6 --- /dev/null +++ b/app/controllers/members_controller.php @@ -0,0 +1,62 @@ +<?php +## 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 +# before_filter :find_member, :except => :new +# before_filter :find_project, :only => :new +# before_filter :authorize +# +# def new +# @project.members << Member.new(params[:member]) if request.post? +# respond_to do |format| +# format.html { redirect_to :action => 'settings', :tab => 'members', :id => @project } +# format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} } +# end +# end +# +# def edit +# if request.post? and @member.update_attributes(params[:member]) +# respond_to do |format| +# format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project } +# format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} } +# end +# end +# end +# +# def destroy +# @member.destroy +# respond_to do |format| +# format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project } +# format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} } +# end +# end +# +#private +# def find_project +# @project = Project.find(params[:id]) +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# def find_member +# @member = Member.find(params[:id]) +# @project = @member.project +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +#end diff --git a/app/controllers/messages_controller.php b/app/controllers/messages_controller.php new file mode 100644 index 0000000..0b842d6 --- /dev/null +++ b/app/controllers/messages_controller.php @@ -0,0 +1,126 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class MessagesController < ApplicationController +# menu_item :boards +# before_filter :find_board, :only => [:new, :preview] +# before_filter :find_message, :except => [:new, :preview] +# before_filter :authorize, :except => [:preview, :edit, :destroy] +# +# verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show } +# verify :xhr => true, :only => :quote +# +# helper :watchers +# helper :attachments +# include AttachmentsHelper +# +# # Show a topic and its replies +# def show +# @replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}]) +# @replies.reverse! if User.current.wants_comments_in_reverse_order? +# @reply = Message.new(:subject => "RE: #{@message.subject}") +# render :action => "show", :layout => false if request.xhr? +# end +# +# # Create a new topic +# def new +# @message = Message.new(params[:message]) +# @message.author = User.current +# @message.board = @board +# if params[:message] && User.current.allowed_to?(:edit_messages, @project) +# @message.locked = params[:message]['locked'] +# @message.sticky = params[:message]['sticky'] +# end +# if request.post? && @message.save +# attach_files(@message, params[:attachments]) +# redirect_to :action => 'show', :id => @message +# end +# end +# +# # Reply to a topic +# def reply +# @reply = Message.new(params[:reply]) +# @reply.author = User.current +# @reply.board = @board +# @topic.children << @reply +# if !@reply.new_record? +# attach_files(@reply, params[:attachments]) +# end +# redirect_to :action => 'show', :id => @topic +# end +# +# # Edit a message +# def edit +# render_403 and return false unless @message.editable_by?(User.current) +# if params[:message] +# @message.locked = params[:message]['locked'] +# @message.sticky = params[:message]['sticky'] +# end +# if request.post? && @message.update_attributes(params[:message]) +# attach_files(@message, params[:attachments]) +# flash[:notice] = l(:notice_successful_update) +# redirect_to :action => 'show', :id => @topic +# end +# end +# +# # Delete a messages +# def destroy +# render_403 and return false unless @message.destroyable_by?(User.current) +# @message.destroy +# redirect_to @message.parent.nil? ? +# { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } : +# { :action => 'show', :id => @message.parent } +# end +# +# def quote +# user = @message.author +# text = @message.content +# content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> " +# content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n" +# render(:update) { |page| +# page.<< "$('message_content').value = \"#{content}\";" +# page.show 'reply' +# page << "Form.Element.focus('message_content');" +# page << "Element.scrollTo('reply');" +# page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;" +# } +# end +# +# def preview +# message = @board.messages.find_by_id(params[:id]) +# @attachements = message.attachments if message +# @text = (params[:message] || params[:reply])[:content] +# render :partial => 'common/preview' +# end +# +#private +# def find_message +# find_board +# @message = @board.messages.find(params[:id], :include => :parent) +# @topic = @message.root +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# def find_board +# @board = Board.find(params[:board_id], :include => :project) +# @project = @board.project +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +#end diff --git a/app/controllers/my_controller.php b/app/controllers/my_controller.php new file mode 100644 index 0000000..a2595ed --- /dev/null +++ b/app/controllers/my_controller.php @@ -0,0 +1,161 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class MyController < ApplicationController +# before_filter :require_login +# +# helper :issues +# +# BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues, +# 'issuesreportedbyme' => :label_reported_issues, +# 'issueswatched' => :label_watched_issues, +# 'news' => :label_news_latest, +# 'calendar' => :label_calendar, +# 'documents' => :label_document_plural, +# 'timelog' => :label_spent_time +# }.freeze +# +# DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'], +# 'right' => ['issuesreportedbyme'] +# }.freeze +# +# verify :xhr => true, +# :session => :page_layout, +# :only => [:add_block, :remove_block, :order_blocks] +# +# def index +# page +# render :action => 'page' +# end +# +# # Show user's page +# def page +# @user = User.current +# @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT +# end +# +# # Edit user's account +# def account +# @user = User.current +# @pref = @user.pref +# if request.post? +# @user.attributes = params[:user] +# @user.mail_notification = (params[:notification_option] == 'all') +# @user.pref.attributes = params[:pref] +# @user.pref[:no_self_notified] = (params[:no_self_notified] == '1') +# if @user.save +# @user.pref.save +# @user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : []) +# set_language_if_valid @user.language +# flash[:notice] = l(:notice_account_updated) +# redirect_to :action => 'account' +# return +# end +# end +# @notification_options = [[l(:label_user_mail_option_all), 'all'], +# [l(:label_user_mail_option_none), 'none']] +# # Only users that belong to more than 1 project can select projects for which they are notified +# # Note that @user.membership.size would fail since AR ignores :include association option when doing a count +# @notification_options.insert 1, [l(:label_user_mail_option_selected), 'selected'] if @user.memberships.length > 1 +# @notification_option = @user.mail_notification? ? 'all' : (@user.notified_projects_ids.empty? ? 'none' : 'selected') +# end +# +# # Manage user's password +# def password +# @user = User.current +# flash[:error] = l(:notice_can_t_change_password) and redirect_to :action => 'account' and return if @user.auth_source_id +# if request.post? +# if @user.check_password?(params[:password]) +# @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation] +# if @user.save +# flash[:notice] = l(:notice_account_password_updated) +# redirect_to :action => 'account' +# end +# else +# flash[:error] = l(:notice_account_wrong_password) +# end +# end +# end +# +# # Create a new feeds key +# def reset_rss_key +# if request.post? && User.current.rss_token +# User.current.rss_token.destroy +# flash[:notice] = l(:notice_feeds_access_key_reseted) +# end +# redirect_to :action => 'account' +# end +# +# # User's page layout configuration +# def page_layout +# @user = User.current +# @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup +# session[:page_layout] = @blocks +# %w(top left right).each {|f| session[:page_layout][f] ||= [] } +# @block_options = [] +# BLOCKS.each {|k, v| @block_options << [l(v), k]} +# end +# +# # Add a block to user's page +# # The block is added on top of the page +# # params[:block] : id of the block to add +# def add_block +# block = params[:block] +# render(:nothing => true) and return unless block && (BLOCKS.keys.include? block) +# @user = User.current +# # remove if already present in a group +# %w(top left right).each {|f| (session[:page_layout][f] ||= []).delete block } +# # add it on top +# session[:page_layout]['top'].unshift block +# render :partial => "block", :locals => {:user => @user, :block_name => block} +# end +# +# # Remove a block to user's page +# # params[:block] : id of the block to remove +# def remove_block +# block = params[:block] +# # remove block in all groups +# %w(top left right).each {|f| (session[:page_layout][f] ||= []).delete block } +# render :nothing => true +# end +# +# # Change blocks order on user's page +# # params[:group] : group to order (top, left or right) +# # params[:list-(top|left|right)] : array of block ids of the group +# def order_blocks +# group = params[:group] +# group_items = params["list-#{group}"] +# if group_items and group_items.is_a? Array +# # remove group blocks if they are presents in other groups +# %w(top left right).each {|f| +# session[:page_layout][f] = (session[:page_layout][f] || []) - group_items +# } +# session[:page_layout][group] = group_items +# end +# render :nothing => true +# end +# +# # Save user's page layout +# def page_layout_save +# @user = User.current +# @user.pref[:my_page_layout] = session[:page_layout] if session[:page_layout] +# @user.pref.save +# session[:page_layout] = nil +# redirect_to :action => 'page' +# end +#end diff --git a/app/controllers/news_controller.php b/app/controllers/news_controller.php new file mode 100644 index 0000000..f9e18ef --- /dev/null +++ b/app/controllers/news_controller.php @@ -0,0 +1,109 @@ +<?php +## 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 +# before_filter :find_news, :except => [:new, :index, :preview] +# before_filter :find_project, :only => [:new, :preview] +# before_filter :authorize, :except => [:index, :preview] +# before_filter :find_optional_project, :only => :index +# accept_key_auth :index +# +# def index +# @news_pages, @newss = paginate :news, +# :per_page => 10, +# :conditions => (@project ? {:project_id => @project.id} : Project.visible_by(User.current)), +# :include => [:author, :project], +# :order => "#{News.table_name}.created_on DESC" +# respond_to do |format| +# format.html { render :layout => false if request.xhr? } +# format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") } +# end +# end +# +# def show +# @comments = @news.comments +# @comments.reverse! if User.current.wants_comments_in_reverse_order? +# end +# +# def new +# @news = News.new(:project => @project, :author => User.current) +# if request.post? +# @news.attributes = params[:news] +# if @news.save +# flash[:notice] = l(:notice_successful_create) +# Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added') +# redirect_to :controller => 'news', :action => 'index', :project_id => @project +# end +# end +# end +# +# def edit +# if request.post? and @news.update_attributes(params[:news]) +# flash[:notice] = l(:notice_successful_update) +# redirect_to :action => 'show', :id => @news +# end +# end +# +# def add_comment +# @comment = Comment.new(params[:comment]) +# @comment.author = User.current +# if @news.comments << @comment +# flash[:notice] = l(:label_comment_added) +# redirect_to :action => 'show', :id => @news +# else +# render :action => 'show' +# end +# end +# +# def destroy_comment +# @news.comments.find(params[:comment_id]).destroy +# redirect_to :action => 'show', :id => @news +# end +# +# def destroy +# @news.destroy +# redirect_to :action => 'index', :project_id => @project +# end +# +# def preview +# @text = (params[:news] ? params[:news][:description] : nil) +# render :partial => 'common/preview' +# end +# +#private +# def find_news +# @news = News.find(params[:id]) +# @project = @news.project +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# def find_project +# @project = Project.find(params[:project_id]) +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# def find_optional_project +# return true unless params[:project_id] +# @project = Project.find(params[:project_id]) +# authorize +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +#end diff --git a/app/controllers/projects_controller.php b/app/controllers/projects_controller.php new file mode 100644 index 0000000..d1b4965 --- /dev/null +++ b/app/controllers/projects_controller.php @@ -0,0 +1,297 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class ProjectsController < ApplicationController +# menu_item :overview +# menu_item :activity, :only => :activity +# menu_item :roadmap, :only => :roadmap +# menu_item :files, :only => [:list_files, :add_file] +# menu_item :settings, :only => :settings +# menu_item :issues, :only => [:changelog] +# +# before_filter :find_project, :except => [ :index, :list, :add, :activity ] +# before_filter :find_optional_project, :only => :activity +# before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ] +# before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ] +# accept_key_auth :activity +# +# helper :sort +# include SortHelper +# helper :custom_fields +# include CustomFieldsHelper +# helper :issues +# helper IssuesHelper +# helper :queries +# include QueriesHelper +# helper :repositories +# include RepositoriesHelper +# include ProjectsHelper +# +# # Lists visible projects +# def index +# projects = Project.find :all, +# :conditions => Project.visible_by(User.current), +# :include => :parent +# respond_to do |format| +# format.html { +# @project_tree = projects.group_by {|p| p.parent || p} +# @project_tree.keys.each {|p| @project_tree[p] -= [p]} +# } +# format.atom { +# render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i), +# :title => "#{Setting.app_title}: #{l(:label_project_latest)}") +# } +# end +# end +# +# # Add a new project +# def add +# @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") +# @trackers = Tracker.all +# @root_projects = Project.find(:all, +# :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}", +# :order => 'name') +# @project = Project.new(params[:project]) +# if request.get? +# @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers? +# @project.trackers = Tracker.all +# @project.is_public = Setting.default_projects_public? +# @project.enabled_module_names = Redmine::AccessControl.available_project_modules +# else +# @project.enabled_module_names = params[:enabled_modules] +# if @project.save +# flash[:notice] = l(:notice_successful_create) +# redirect_to :controller => 'admin', :action => 'projects' +# end +# end +# end +# +# # Show @project +# def show +# if params[:jump] +# # try to redirect to the requested menu item +# redirect_to_project_menu_item(@project, params[:jump]) && return +# end +# +# @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role} +# @subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current)) +# @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") +# @trackers = @project.rolled_up_trackers +# +# cond = @project.project_condition(Setting.display_subprojects_issues?) +# Issue.visible_by(User.current) do +# @open_issues_by_tracker = Issue.count(:group => :tracker, +# :include => [:project, :status, :tracker], +# :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false]) +# @total_issues_by_tracker = Issue.count(:group => :tracker, +# :include => [:project, :status, :tracker], +# :conditions => cond) +# end +# TimeEntry.visible_by(User.current) do +# @total_hours = TimeEntry.sum(:hours, +# :include => :project, +# :conditions => cond).to_f +# end +# @key = User.current.rss_key +# end +# +# def settings +# @root_projects = Project.find(:all, +# :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id], +# :order => 'name') +# @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") +# @issue_category ||= IssueCategory.new +# @member ||= @project.members.new +# @trackers = Tracker.all +# @repository ||= @project.repository +# @wiki ||= @project.wiki +# end +# +# # Edit @project +# def edit +# if request.post? +# @project.attributes = params[:project] +# if @project.save +# flash[:notice] = l(:notice_successful_update) +# redirect_to :action => 'settings', :id => @project +# else +# settings +# render :action => 'settings' +# end +# end +# end +# +# def modules +# @project.enabled_module_names = params[:enabled_modules] +# redirect_to :action => 'settings', :id => @project, :tab => 'modules' +# end +# +# def archive +# @project.archive if request.post? && @project.active? +# redirect_to :controller => 'admin', :action => 'projects' +# end +# +# def unarchive +# @project.unarchive if request.post? && !@project.active? +# redirect_to :controller => 'admin', :action => 'projects' +# end +# +# # Delete @project +# def destroy +# @project_to_destroy = @project +# if request.post? and params[:confirm] +# @project_to_destroy.destroy +# redirect_to :controller => 'admin', :action => 'projects' +# end +# # hide project in layout +# @project = nil +# end +# +# # Add a new issue category to @project +# def add_issue_category +# @category = @project.issue_categories.build(params[:category]) +# if request.post? and @category.save +# respond_to do |format| +# format.html do +# flash[:notice] = l(:notice_successful_create) +# redirect_to :action => 'settings', :tab => 'categories', :id => @project +# end +# format.js do +# # IE doesn't support the replace_html rjs method for select box options +# render(:update) {|page| page.replace "issue_category_id", +# content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]') +# } +# end +# end +# end +# end +# +# # Add a new version to @project +# def add_version +# @version = @project.versions.build(params[:version]) +# if request.post? and @version.save +# flash[:notice] = l(:notice_successful_create) +# redirect_to :action => 'settings', :tab => 'versions', :id => @project +# end +# end +# +# def add_file +# if request.post? +# container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id])) +# attachments = attach_files(container, params[:attachments]) +# if !attachments.empty? && Setting.notified_events.include?('file_added') +# Mailer.deliver_attachments_added(attachments) +# end +# redirect_to :controller => 'projects', :action => 'list_files', :id => @project +# return +# end +# @versions = @project.versions.sort +# end +# +# def list_files +# sort_init 'filename', 'asc' +# sort_update 'filename' => "#{Attachment.table_name}.filename", +# 'created_on' => "#{Attachment.table_name}.created_on", +# 'size' => "#{Attachment.table_name}.filesize", +# 'downloads' => "#{Attachment.table_name}.downloads" +# +# @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)] +# @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse +# render :layout => !request.xhr? +# end +# +# # Show changelog for @project +# def changelog +# @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position') +# retrieve_selected_tracker_ids(@trackers) +# @versions = @project.versions.sort +# end +# +# def roadmap +# @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true]) +# retrieve_selected_tracker_ids(@trackers) +# @versions = @project.versions.sort +# @versions = @versions.select {|v| !v.completed? } unless params[:completed] +# end +# +# def activity +# @days = Setting.activity_days_default.to_i +# +# if params[:from] +# begin; @date_to = params[:from].to_date + 1; rescue; end +# end +# +# @date_to ||= Date.today + 1 +# @date_from = @date_to - @days +# @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') +# @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id])) +# +# @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project, +# :with_subprojects => @with_subprojects, +# :author => @author) +# @activity.scope_select {|t| !params["show_#{t}"].nil?} +# @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty? +# +# events = @activity.events(@date_from, @date_to) +# +# respond_to do |format| +# format.html { +# @events_by_day = events.group_by(&:event_date) +# render :layout => false if request.xhr? +# } +# format.atom { +# title = l(:label_activity) +# if @author +# title = @author.name +# elsif @activity.scope.size == 1 +# title = l("label_#{@activity.scope.first.singularize}_plural") +# end +# render_feed(events, :title => "#{@project || Setting.app_title}: #{title}") +# } +# end +# +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +#private +# # Find project of id params[:id] +# # if not found, redirect to project list +# # Used as a before_filter +# def find_project +# @project = Project.find(params[:id]) +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# def find_optional_project +# return true unless params[:id] +# @project = Project.find(params[:id]) +# authorize +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# def retrieve_selected_tracker_ids(selectable_trackers) +# if ids = params[:tracker_ids] +# @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s } +# else +# @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s } +# end +# end +#end diff --git a/app/controllers/queries_controller.php b/app/controllers/queries_controller.php new file mode 100644 index 0000000..99090fb --- /dev/null +++ b/app/controllers/queries_controller.php @@ -0,0 +1,81 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class QueriesController < ApplicationController +# menu_item :issues +# before_filter :find_query, :except => :new +# before_filter :find_optional_project, :only => :new +# +# def new +# @query = Query.new(params[:query]) +# @query.project = params[:query_is_for_all] ? nil : @project +# @query.user = User.current +# @query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin? +# @query.column_names = nil if params[:default_columns] +# +# params[:fields].each do |field| +# @query.add_filter(field, params[:operators][field], params[:values][field]) +# end if params[:fields] +# +# if request.post? && params[:confirm] && @query.save +# flash[:notice] = l(:notice_successful_create) +# redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query +# return +# end +# render :layout => false if request.xhr? +# end +# +# def edit +# if request.post? +# @query.filters = {} +# params[:fields].each do |field| +# @query.add_filter(field, params[:operators][field], params[:values][field]) +# end if params[:fields] +# @query.attributes = params[:query] +# @query.project = nil if params[:query_is_for_all] +# @query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin? +# @query.column_names = nil if params[:default_columns] +# +# if @query.save +# flash[:notice] = l(:notice_successful_update) +# redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query +# end +# end +# end +# +# def destroy +# @query.destroy if request.post? +# redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 +# end +# +#private +# def find_query +# @query = Query.find(params[:id]) +# @project = @query.project +# render_403 unless @query.editable_by?(User.current) +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# def find_optional_project +# @project = Project.find(params[:project_id]) if params[:project_id] +# User.current.allowed_to?(:save_queries, @project, :global => true) +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +#end diff --git a/app/controllers/reports_controller.php b/app/controllers/reports_controller.php new file mode 100644 index 0000000..9be246e --- /dev/null +++ b/app/controllers/reports_controller.php @@ -0,0 +1,237 @@ +<?php +## 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 +# menu_item :issues +# before_filter :find_project, :authorize +# +# def issue_report +# @statuses = IssueStatus.find(:all, :order => 'position') +# +# case params[:detail] +# when "tracker" +# @field = "tracker_id" +# @rows = @project.trackers +# @data = issues_by_tracker +# @report_title = l(:field_tracker) +# render :template => "reports/issue_report_details" +# when "version" +# @field = "fixed_version_id" +# @rows = @project.versions.sort +# @data = issues_by_version +# @report_title = l(:field_version) +# render :template => "reports/issue_report_details" +# when "priority" +# @field = "priority_id" +# @rows = Enumeration::get_values('IPRI') +# @data = issues_by_priority +# @report_title = l(:field_priority) +# render :template => "reports/issue_report_details" +# when "category" +# @field = "category_id" +# @rows = @project.issue_categories +# @data = issues_by_category +# @report_title = l(:field_category) +# render :template => "reports/issue_report_details" +# when "assigned_to" +# @field = "assigned_to_id" +# @rows = @project.members.collect { |m| m.user } +# @data = issues_by_assigned_to +# @report_title = l(:field_assigned_to) +# render :template => "reports/issue_report_details" +# when "author" +# @field = "author_id" +# @rows = @project.members.collect { |m| m.user } +# @data = issues_by_author +# @report_title = l(:field_author) +# render :template => "reports/issue_report_details" +# when "subproject" +# @field = "project_id" +# @rows = @project.active_children +# @data = issues_by_subproject +# @report_title = l(:field_subproject) +# render :template => "reports/issue_report_details" +# else +# @trackers = @project.trackers +# @versions = @project.versions.sort +# @priorities = Enumeration::get_values('IPRI') +# @categories = @project.issue_categories +# @assignees = @project.members.collect { |m| m.user } +# @authors = @project.members.collect { |m| m.user } +# @subprojects = @project.active_children +# issues_by_tracker +# issues_by_version +# issues_by_priority +# issues_by_category +# issues_by_assigned_to +# issues_by_author +# issues_by_subproject +# +# render :template => "reports/issue_report" +# end +# end +# +# def delays +# @trackers = Tracker.find(:all) +# if request.get? +# @selected_tracker_ids = @trackers.collect {|t| t.id.to_s } +# else +# @selected_tracker_ids = params[:tracker_ids].collect { |id| id.to_i.to_s } if params[:tracker_ids] and params[:tracker_ids].is_a? Array +# end +# @selected_tracker_ids ||= [] +# @raw = +# ActiveRecord::Base.connection.select_all("SELECT datediff( a.created_on, b.created_on ) as delay, count(a.id) as total +# FROM issue_histories a, issue_histories b, issues i +# WHERE a.status_id =5 +# AND a.issue_id = b.issue_id +# AND a.issue_id = i.id +# AND i.tracker_id in (#{@selected_tracker_ids.join(',')}) +# AND b.id = ( +# SELECT min( c.id ) +# FROM issue_histories c +# WHERE b.issue_id = c.issue_id ) +# GROUP BY delay") unless @selected_tracker_ids.empty? +# @raw ||=[] +# +# @x_from = 0 +# @x_to = 0 +# @y_from = 0 +# @y_to = 0 +# @sum_total = 0 +# @sum_delay = 0 +# @raw.each do |r| +# @x_to = [r['delay'].to_i, @x_to].max +# @y_to = [r['total'].to_i, @y_to].max +# @sum_total = @sum_total + r['total'].to_i +# @sum_delay = @sum_delay + r['total'].to_i * r['delay'].to_i +# end +# end +# +#private +# # Find project of id params[:id] +# def find_project +# @project = Project.find(params[:id]) +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# def issues_by_tracker +# @issues_by_tracker ||= +# ActiveRecord::Base.connection.select_all("select s.id as status_id, +# s.is_closed as closed, +# t.id as tracker_id, +# count(i.id) as total +# from +# #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Tracker.table_name} t +# where +# i.status_id=s.id +# and i.tracker_id=t.id +# and i.project_id=#{@project.id} +# group by s.id, s.is_closed, t.id") +# end +# +# def issues_by_version +# @issues_by_version ||= +# ActiveRecord::Base.connection.select_all("select s.id as status_id, +# s.is_closed as closed, +# v.id as fixed_version_id, +# count(i.id) as total +# from +# #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Version.table_name} v +# where +# i.status_id=s.id +# and i.fixed_version_id=v.id +# and i.project_id=#{@project.id} +# group by s.id, s.is_closed, v.id") +# end +# +# def issues_by_priority +# @issues_by_priority ||= +# ActiveRecord::Base.connection.select_all("select s.id as status_id, +# s.is_closed as closed, +# p.id as priority_id, +# count(i.id) as total +# from +# #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Enumeration.table_name} p +# where +# i.status_id=s.id +# and i.priority_id=p.id +# and i.project_id=#{@project.id} +# group by s.id, s.is_closed, p.id") +# end +# +# def issues_by_category +# @issues_by_category ||= +# ActiveRecord::Base.connection.select_all("select s.id as status_id, +# s.is_closed as closed, +# c.id as category_id, +# count(i.id) as total +# from +# #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssueCategory.table_name} c +# where +# i.status_id=s.id +# and i.category_id=c.id +# and i.project_id=#{@project.id} +# group by s.id, s.is_closed, c.id") +# end +# +# def issues_by_assigned_to +# @issues_by_assigned_to ||= +# ActiveRecord::Base.connection.select_all("select s.id as status_id, +# s.is_closed as closed, +# a.id as assigned_to_id, +# count(i.id) as total +# from +# #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a +# where +# i.status_id=s.id +# and i.assigned_to_id=a.id +# and i.project_id=#{@project.id} +# group by s.id, s.is_closed, a.id") +# end +# +# def issues_by_author +# @issues_by_author ||= +# ActiveRecord::Base.connection.select_all("select s.id as status_id, +# s.is_closed as closed, +# a.id as author_id, +# count(i.id) as total +# from +# #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a +# where +# i.status_id=s.id +# and i.author_id=a.id +# and i.project_id=#{@project.id} +# group by s.id, s.is_closed, a.id") +# end +# +# def issues_by_subproject +# @issues_by_subproject ||= +# ActiveRecord::Base.connection.select_all("select s.id as status_id, +# s.is_closed as closed, +# i.project_id as project_id, +# count(i.id) as total +# from +# #{Issue.table_name} i, #{IssueStatus.table_name} s +# where +# i.status_id=s.id +# and i.project_id IN (#{@project.active_children.collect{|p| p.id}.join(',')}) +# group by s.id, s.is_closed, i.project_id") if @project.active_children.any? +# @issues_by_subproject ||= [] +# end +#end diff --git a/app/controllers/repositories_controller.php b/app/controllers/repositories_controller.php new file mode 100644 index 0000000..3cdba45 --- /dev/null +++ b/app/controllers/repositories_controller.php @@ -0,0 +1,329 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#require 'SVG/Graph/Bar' +#require 'SVG/Graph/BarHorizontal' +#require 'digest/sha1' +# +#class ChangesetNotFound < Exception; end +#class InvalidRevisionParam < Exception; end +# +#class RepositoriesController < ApplicationController +# menu_item :repository +# before_filter :find_repository, :except => :edit +# before_filter :find_project, :only => :edit +# before_filter :authorize +# accept_key_auth :revisions +# +# rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed +# +# def edit +# @repository = @project.repository +# if !@repository +# @repository = Repository.factory(params[:repository_scm]) +# @repository.project = @project if @repository +# end +# if request.post? && @repository +# @repository.attributes = params[:repository] +# @repository.save +# end +# render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'} +# end +# +# def committers +# @committers = @repository.committers +# @users = @project.users +# additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id) +# @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty? +# @users.compact! +# @users.sort! +# if request.post? && params[:committers].is_a?(Hash) +# # Build a hash with repository usernames as keys and corresponding user ids as values +# @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h} +# flash[:notice] = l(:notice_successful_update) +# redirect_to :action => 'committers', :id => @project +# end +# end +# +# def destroy +# @repository.destroy +# redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository' +# end +# +# def show +# # check if new revisions have been committed in the repository +# @repository.fetch_changesets if Setting.autofetch_changesets? +# # root entries +# @entries = @repository.entries('', @rev) +# # latest changesets +# @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC") +# show_error_not_found unless @entries || @changesets.any? +# end +# +# def browse +# @entries = @repository.entries(@path, @rev) +# if request.xhr? +# @entries ? render(:partial => 'dir_list_content') : render(:nothing => true) +# else +# show_error_not_found and return unless @entries +# @properties = @repository.properties(@path, @rev) +# render :action => 'browse' +# end +# end +# +# def changes +# @entry = @repository.entry(@path, @rev) +# show_error_not_found and return unless @entry +# @changesets = @repository.changesets_for_path(@path) +# @properties = @repository.properties(@path, @rev) +# end +# +# def revisions +# @changeset_count = @repository.changesets.count +# @changeset_pages = Paginator.new self, @changeset_count, +# per_page_option, +# params['page'] +# @changesets = @repository.changesets.find(:all, +# :limit => @changeset_pages.items_per_page, +# :offset => @changeset_pages.current.offset, +# :include => :user) +# +# respond_to do |format| +# format.html { render :layout => false if request.xhr? } +# format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") } +# end +# end +# +# def entry +# @entry = @repository.entry(@path, @rev) +# show_error_not_found and return unless @entry +# +# # If the entry is a dir, show the browser +# browse and return if @entry.is_dir? +# +# @content = @repository.cat(@path, @rev) +# show_error_not_found and return unless @content +# if 'raw' == params[:format] || @content.is_binary_data? +# # Force the download if it's a binary file +# send_data @content, :filename => @path.split('/').last +# else +# # Prevent empty lines when displaying a file with Windows style eol +# @content.gsub!("\r\n", "\n") +# end +# end +# +# def annotate +# @entry = @repository.entry(@path, @rev) +# show_error_not_found and return unless @entry +# +# @annotate = @repository.scm.annotate(@path, @rev) +# render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty? +# end +# +# def revision +# @changeset = @repository.changesets.find_by_revision(@rev) +# raise ChangesetNotFound unless @changeset +# +# respond_to do |format| +# format.html +# format.js {render :layout => false} +# end +# rescue ChangesetNotFound +# show_error_not_found +# end +# +# def diff +# if params[:format] == 'diff' +# @diff = @repository.diff(@path, @rev, @rev_to) +# show_error_not_found and return unless @diff +# filename = "changeset_r#{@rev}" +# filename << "_r#{@rev_to}" if @rev_to +# send_data @diff.join, :filename => "#{filename}.diff", +# :type => 'text/x-patch', +# :disposition => 'attachment' +# else +# @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline' +# @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type) +# +# # Save diff type as user preference +# if User.current.logged? && @diff_type != User.current.pref[:diff_type] +# User.current.pref[:diff_type] = @diff_type +# User.current.preference.save +# end +# +# @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}") +# unless read_fragment(@cache_key) +# @diff = @repository.diff(@path, @rev, @rev_to) +# show_error_not_found unless @diff +# end +# end +# end +# +# def stats +# end +# +# def graph +# data = nil +# case params[:graph] +# when "commits_per_month" +# data = graph_commits_per_month(@repository) +# when "commits_per_author" +# data = graph_commits_per_author(@repository) +# end +# if data +# headers["Content-Type"] = "image/svg+xml" +# send_data(data, :type => "image/svg+xml", :disposition => "inline") +# else +# render_404 +# end +# end +# +#private +# def find_project +# @project = Project.find(params[:id]) +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# REV_PARAM_RE = %r{^[a-f0-9]*$} +# +# def find_repository +# @project = Project.find(params[:id]) +# @repository = @project.repository +# render_404 and return false unless @repository +# @path = params[:path].join('/') unless params[:path].nil? +# @path ||= '' +# @rev = params[:rev] +# @rev_to = params[:rev_to] +# raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE) +# rescue ActiveRecord::RecordNotFound +# render_404 +# rescue InvalidRevisionParam +# show_error_not_found +# end +# +# def show_error_not_found +# render_error l(:error_scm_not_found) +# end +# +# # Handler for Redmine::Scm::Adapters::CommandFailed exception +# def show_error_command_failed(exception) +# render_error l(:error_scm_command_failed, exception.message) +# end +# +# def graph_commits_per_month(repository) +# @date_to = Date.today +# @date_from = @date_to << 11 +# @date_from = Date.civil(@date_from.year, @date_from.month, 1) +# commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to]) +# commits_by_month = [0] * 12 +# commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last } +# +# changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to]) +# changes_by_month = [0] * 12 +# changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last } +# +# fields = [] +# month_names = l(:actionview_datehelper_select_month_names_abbr).split(',') +# 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]} +# +# graph = SVG::Graph::Bar.new( +# :height => 300, +# :width => 800, +# :fields => fields.reverse, +# :stack => :side, +# :scale_integers => true, +# :step_x_labels => 2, +# :show_data_values => false, +# :graph_title => l(:label_commits_per_month), +# :show_graph_title => true +# ) +# +# graph.add_data( +# :data => commits_by_month[0..11].reverse, +# :title => l(:label_revision_plural) +# ) +# +# graph.add_data( +# :data => changes_by_month[0..11].reverse, +# :title => l(:label_change_plural) +# ) +# +# graph.burn +# end +# +# def graph_commits_per_author(repository) +# commits_by_author = repository.changesets.count(:all, :group => :committer) +# commits_by_author.sort! {|x, y| x.last <=> y.last} +# +# changes_by_author = repository.changes.count(:all, :group => :committer) +# h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o} +# +# fields = commits_by_author.collect {|r| r.first} +# commits_data = commits_by_author.collect {|r| r.last} +# changes_data = commits_by_author.collect {|r| h[r.first] || 0} +# +# fields = fields + [""]*(10 - fields.length) if fields.length<10 +# commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10 +# changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10 +# +# # Remove email adress in usernames +# fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') } +# +# graph = SVG::Graph::BarHorizontal.new( +# :height => 400, +# :width => 800, +# :fields => fields, +# :stack => :side, +# :scale_integers => true, +# :show_data_values => false, +# :rotate_y_labels => false, +# :graph_title => l(:label_commits_per_author), +# :show_graph_title => true +# ) +# +# graph.add_data( +# :data => commits_data, +# :title => l(:label_revision_plural) +# ) +# +# graph.add_data( +# :data => changes_data, +# :title => l(:label_change_plural) +# ) +# +# graph.burn +# end +# +#end +# +#class Date +# def months_ago(date = Date.today) +# (date.year - self.year)*12 + (date.month - self.month) +# end +# +# def weeks_ago(date = Date.today) +# (date.year - self.year)*52 + (date.cweek - self.cweek) +# end +#end +# +#class String +# def with_leading_slash +# starts_with?('/') ? self : "/#{self}" +# end +#end diff --git a/app/controllers/roles_controller.php b/app/controllers/roles_controller.php new file mode 100644 index 0000000..981eca0 --- /dev/null +++ b/app/controllers/roles_controller.php @@ -0,0 +1,95 @@ +<?php +## 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 +# before_filter :require_admin +# +# verify :method => :post, :only => [ :destroy, :move ], +# :redirect_to => { :action => :list } +# +# def index +# list +# render :action => 'list' unless request.xhr? +# end +# +# def list +# @role_pages, @roles = paginate :roles, :per_page => 25, :order => 'builtin, position' +# render :action => "list", :layout => false if request.xhr? +# end +# +# def new +# # Prefills the form with 'Non member' role permissions +# @role = Role.new(params[:role] || {:permissions => Role.non_member.permissions}) +# if request.post? && @role.save +# # workflow copy +# if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from])) +# @role.workflows.copy(copy_from) +# end +# flash[:notice] = l(:notice_successful_create) +# redirect_to :action => 'list' +# end +# @permissions = @role.setable_permissions +# @roles = Role.find :all, :order => 'builtin, position' +# end +# +# def edit +# @role = Role.find(params[:id]) +# if request.post? and @role.update_attributes(params[:role]) +# flash[:notice] = l(:notice_successful_update) +# redirect_to :action => 'list' +# end +# @permissions = @role.setable_permissions +# end +# +# def destroy +# @role = Role.find(params[:id]) +# @role.destroy +# redirect_to :action => 'list' +# rescue +# flash[:error] = 'This role is in use and can not be deleted.' +# redirect_to :action => 'index' +# end +# +# def move +# @role = Role.find(params[:id]) +# case params[:position] +# when 'highest' +# @role.move_to_top +# when 'higher' +# @role.move_higher +# when 'lower' +# @role.move_lower +# when 'lowest' +# @role.move_to_bottom +# end if params[:position] +# redirect_to :action => 'list' +# end +# +# def report +# @roles = Role.find(:all, :order => 'builtin, position') +# @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? } +# if request.post? +# @roles.each do |role| +# role.permissions = params[:permissions][role.id.to_s] +# role.save +# end +# flash[:notice] = l(:notice_successful_update) +# redirect_to :action => 'list' +# end +# end +#end diff --git a/app/controllers/search_controller.php b/app/controllers/search_controller.php new file mode 100644 index 0000000..848716d --- /dev/null +++ b/app/controllers/search_controller.php @@ -0,0 +1,117 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class SearchController < ApplicationController +# before_filter :find_optional_project +# +# helper :messages +# include MessagesHelper +# +# def index +# @question = params[:q] || "" +# @question.strip! +# @all_words = params[:all_words] || (params[:submit] ? false : true) +# @titles_only = !params[:titles_only].nil? +# +# projects_to_search = +# case params[:scope] +# when 'all' +# nil +# when 'my_projects' +# User.current.memberships.collect(&:project) +# when 'subprojects' +# @project ? ([ @project ] + @project.active_children) : nil +# else +# @project +# end +# +# offset = nil +# begin; offset = params[:offset].to_time if params[:offset]; rescue; end +# +# # quick jump to an issue +# if @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(User.current)) +# redirect_to :controller => "issues", :action => "show", :id => $1 +# return +# end +# +# @object_types = %w(issues news documents changesets wiki_pages messages projects) +# if projects_to_search.is_a? Project +# # don't search projects +# @object_types.delete('projects') +# # only show what the user is allowed to view +# @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)} +# end +# +# @scope = @object_types.select {|t| params[t]} +# @scope = @object_types if @scope.empty? +# +# # extract tokens from the question +# # eg. hello "bye bye" => ["hello", "bye bye"] +# @tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')} +# # tokens must be at least 3 character long +# @tokens = @tokens.uniq.select {|w| w.length > 2 } +# +# if !@tokens.empty? +# # no more than 5 tokens to search for +# @tokens.slice! 5..-1 if @tokens.size > 5 +# # strings used in sql like statement +# like_tokens = @tokens.collect {|w| "%#{w.downcase}%"} +# +# @results = [] +# @results_by_type = Hash.new {|h,k| h[k] = 0} +# +# limit = 10 +# @scope.each do |s| +# r, c = s.singularize.camelcase.constantize.search(like_tokens, projects_to_search, +# :all_words => @all_words, +# :titles_only => @titles_only, +# :limit => (limit+1), +# :offset => offset, +# :before => params[:previous].nil?) +# @results += r +# @results_by_type[s] += c +# end +# @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime} +# if params[:previous].nil? +# @pagination_previous_date = @results[0].event_datetime if offset && @results[0] +# if @results.size > limit +# @pagination_next_date = @results[limit-1].event_datetime +# @results = @results[0, limit] +# end +# else +# @pagination_next_date = @results[-1].event_datetime if offset && @results[-1] +# if @results.size > limit +# @pagination_previous_date = @results[-(limit)].event_datetime +# @results = @results[-(limit), limit] +# end +# end +# else +# @question = "" +# end +# render :layout => false if request.xhr? +# end +# +#private +# def find_optional_project +# return true unless params[:id] +# @project = Project.find(params[:id]) +# check_project_privacy +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +#end diff --git a/app/controllers/settings_controller.php b/app/controllers/settings_controller.php new file mode 100644 index 0000000..5c6a7fb --- /dev/null +++ b/app/controllers/settings_controller.php @@ -0,0 +1,60 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class SettingsController < ApplicationController +# before_filter :require_admin +# +# def index +# edit +# render :action => 'edit' +# end +# +# def edit +# @notifiables = %w(issue_added issue_updated news_added document_added file_added message_posted) +# if request.post? && params[:settings] && params[:settings].is_a?(Hash) +# settings = (params[:settings] || {}).dup.symbolize_keys +# settings.each do |name, value| +# # remove blank values in array settings +# value.delete_if {|v| v.blank? } if value.is_a?(Array) +# Setting[name] = value +# end +# flash[:notice] = l(:notice_successful_update) +# redirect_to :action => 'edit', :tab => params[:tab] +# return +# end +# @options = {} +# @options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] } +# @deliveries = ActionMailer::Base.perform_deliveries +# +# @guessed_host_and_path = request.host_with_port.dup +# @guessed_host_and_path << ('/'+ request.relative_url_root.gsub(%r{^\/}, '')) unless request.relative_url_root.blank? +# end +# +# def plugin +# @plugin = Redmine::Plugin.find(params[:id]) +# if request.post? +# Setting["plugin_#{@plugin.id}"] = params[:settings] +# flash[:notice] = l(:notice_successful_update) +# redirect_to :action => 'plugin', :id => @plugin.id +# end +# @partial = @plugin.settings[:partial] +# @settings = Setting["plugin_#{@plugin.id}"] +# rescue Redmine::PluginNotFound +# render_404 +# end +#end diff --git a/app/controllers/sys_controller.php b/app/controllers/sys_controller.php new file mode 100644 index 0000000..30a9a1e --- /dev/null +++ b/app/controllers/sys_controller.php @@ -0,0 +1,47 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class SysController < ActionController::Base +# wsdl_service_name 'Sys' +# web_service_api SysApi +# web_service_scaffold :invoke +# +# before_invocation :check_enabled +# +# # Returns the projects list, with their repositories +# def projects_with_repository_enabled +# Project.has_module(:repository).find(:all, :include => :repository, :order => 'identifier') +# end +# +# # Registers a repository for the given project identifier +# def repository_created(identifier, vendor, url) +# project = Project.find_by_identifier(identifier) +# # Do not create the repository if the project has already one +# return 0 unless project && project.repository.nil? +# logger.debug "Repository for #{project.name} was created" +# repository = Repository.factory(vendor, :project => project, :url => url) +# repository.save +# repository.id || 0 +# end +# +#protected +# +# def check_enabled(name, args) +# Setting.sys_api_enabled? +# end +#end diff --git a/app/controllers/timelog_controller.php b/app/controllers/timelog_controller.php new file mode 100644 index 0000000..7488ac3 --- /dev/null +++ b/app/controllers/timelog_controller.php @@ -0,0 +1,291 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class TimelogController < ApplicationController +# menu_item :issues +# before_filter :find_project, :authorize, :only => [:edit, :destroy] +# before_filter :find_optional_project, :only => [:report, :details] +# +# verify :method => :post, :only => :destroy, :redirect_to => { :action => :details } +# +# helper :sort +# include SortHelper +# helper :issues +# include TimelogHelper +# helper :custom_fields +# include CustomFieldsHelper +# +# def report +# @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id", +# :klass => Project, +# :label => :label_project}, +# 'version' => {:sql => "#{Issue.table_name}.fixed_version_id", +# :klass => Version, +# :label => :label_version}, +# 'category' => {:sql => "#{Issue.table_name}.category_id", +# :klass => IssueCategory, +# :label => :field_category}, +# 'member' => {:sql => "#{TimeEntry.table_name}.user_id", +# :klass => User, +# :label => :label_member}, +# 'tracker' => {:sql => "#{Issue.table_name}.tracker_id", +# :klass => Tracker, +# :label => :label_tracker}, +# 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id", +# :klass => Enumeration, +# :label => :label_activity}, +# 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id", +# :klass => Issue, +# :label => :label_issue} +# } +# +# # Add list and boolean custom fields as available criterias +# custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields) +# custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf| +# @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)", +# :format => cf.field_format, +# :label => cf.name} +# end if @project +# +# # Add list and boolean time entry custom fields +# TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf| +# @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)", +# :format => cf.field_format, +# :label => cf.name} +# end +# +# @criterias = params[:criterias] || [] +# @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria} +# @criterias.uniq! +# @criterias = @criterias[0,3] +# +# @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month' +# +# retrieve_date_range +# +# unless @criterias.empty? +# sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ') +# sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ') +# +# sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours" +# sql << " FROM #{TimeEntry.table_name}" +# sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id" +# sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id" +# sql << " WHERE" +# sql << " (%s) AND" % @project.project_condition(Setting.display_subprojects_issues?) if @project +# sql << " (%s) AND" % Project.allowed_to_condition(User.current, :view_time_entries) +# sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)] +# sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on" +# +# @hours = ActiveRecord::Base.connection.select_all(sql) +# +# @hours.each do |row| +# case @columns +# when 'year' +# row['year'] = row['tyear'] +# when 'month' +# row['month'] = "#{row['tyear']}-#{row['tmonth']}" +# when 'week' +# row['week'] = "#{row['tyear']}-#{row['tweek']}" +# when 'day' +# row['day'] = "#{row['spent_on']}" +# end +# end +# +# @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f} +# +# @periods = [] +# # Date#at_beginning_of_ not supported in Rails 1.2.x +# date_from = @from.to_time +# # 100 columns max +# while date_from <= @to.to_time && @periods.length < 100 +# case @columns +# when 'year' +# @periods << "#{date_from.year}" +# date_from = (date_from + 1.year).at_beginning_of_year +# when 'month' +# @periods << "#{date_from.year}-#{date_from.month}" +# date_from = (date_from + 1.month).at_beginning_of_month +# when 'week' +# @periods << "#{date_from.year}-#{date_from.to_date.cweek}" +# date_from = (date_from + 7.day).at_beginning_of_week +# when 'day' +# @periods << "#{date_from.to_date}" +# date_from = date_from + 1.day +# end +# end +# end +# +# respond_to do |format| +# format.html { render :layout => !request.xhr? } +# format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') } +# end +# end +# +# def details +# sort_init 'spent_on', 'desc' +# sort_update 'spent_on' => 'spent_on', +# 'user' => 'user_id', +# 'activity' => 'activity_id', +# 'project' => "#{Project.table_name}.name", +# 'issue' => 'issue_id', +# 'hours' => 'hours' +# +# cond = ARCondition.new +# if @project.nil? +# cond << Project.allowed_to_condition(User.current, :view_time_entries) +# elsif @issue.nil? +# cond << @project.project_condition(Setting.display_subprojects_issues?) +# else +# cond << ["#{TimeEntry.table_name}.issue_id = ?", @issue.id] +# end +# +# retrieve_date_range +# cond << ['spent_on BETWEEN ? AND ?', @from, @to] +# +# TimeEntry.visible_by(User.current) do +# respond_to do |format| +# format.html { +# # Paginate results +# @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions) +# @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page'] +# @entries = TimeEntry.find(:all, +# :include => [:project, :activity, :user, {:issue => :tracker}], +# :conditions => cond.conditions, +# :order => sort_clause, +# :limit => @entry_pages.items_per_page, +# :offset => @entry_pages.current.offset) +# @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f +# +# render :layout => !request.xhr? +# } +# format.atom { +# entries = TimeEntry.find(:all, +# :include => [:project, :activity, :user, {:issue => :tracker}], +# :conditions => cond.conditions, +# :order => "#{TimeEntry.table_name}.created_on DESC", +# :limit => Setting.feeds_limit.to_i) +# render_feed(entries, :title => l(:label_spent_time)) +# } +# format.csv { +# # Export all entries +# @entries = TimeEntry.find(:all, +# :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], +# :conditions => cond.conditions, +# :order => sort_clause) +# send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') +# } +# end +# end +# end +# +# def edit +# render_403 and return if @time_entry && !@time_entry.editable_by?(User.current) +# @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today) +# @time_entry.attributes = params[:time_entry] +# if request.post? and @time_entry.save +# flash[:notice] = l(:notice_successful_update) +# redirect_back_or_default :action => 'details', :project_id => @time_entry.project +# return +# end +# end +# +# def destroy +# render_404 and return unless @time_entry +# render_403 and return unless @time_entry.editable_by?(User.current) +# @time_entry.destroy +# flash[:notice] = l(:notice_successful_delete) +# redirect_to :back +# rescue ::ActionController::RedirectBackError +# redirect_to :action => 'details', :project_id => @time_entry.project +# end +# +#private +# def find_project +# if params[:id] +# @time_entry = TimeEntry.find(params[:id]) +# @project = @time_entry.project +# elsif params[:issue_id] +# @issue = Issue.find(params[:issue_id]) +# @project = @issue.project +# elsif params[:project_id] +# @project = Project.find(params[:project_id]) +# else +# render_404 +# return false +# end +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# def find_optional_project +# if !params[:issue_id].blank? +# @issue = Issue.find(params[:issue_id]) +# @project = @issue.project +# elsif !params[:project_id].blank? +# @project = Project.find(params[:project_id]) +# end +# deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true) +# end +# +# # Retrieves the date range based on predefined ranges or specific from/to param dates +# def retrieve_date_range +# @free_period = false +# @from, @to = nil, nil +# +# if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?) +# case params[:period].to_s +# when 'today' +# @from = @to = Date.today +# when 'yesterday' +# @from = @to = Date.today - 1 +# when 'current_week' +# @from = Date.today - (Date.today.cwday - 1)%7 +# @to = @from + 6 +# when 'last_week' +# @from = Date.today - 7 - (Date.today.cwday - 1)%7 +# @to = @from + 6 +# when '7_days' +# @from = Date.today - 7 +# @to = Date.today +# when 'current_month' +# @from = Date.civil(Date.today.year, Date.today.month, 1) +# @to = (@from >> 1) - 1 +# when 'last_month' +# @from = Date.civil(Date.today.year, Date.today.month, 1) << 1 +# @to = (@from >> 1) - 1 +# when '30_days' +# @from = Date.today - 30 +# @to = Date.today +# when 'current_year' +# @from = Date.civil(Date.today.year, 1, 1) +# @to = Date.civil(Date.today.year, 12, 31) +# end +# elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?)) +# begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end +# begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end +# @free_period = true +# else +# # default +# end +# +# @from, @to = @to, @from if @from && @to && @from > @to +# @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) - 1 +# @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) +# end +#end diff --git a/app/controllers/trackers_controller.php b/app/controllers/trackers_controller.php new file mode 100644 index 0000000..16c9568 --- /dev/null +++ b/app/controllers/trackers_controller.php @@ -0,0 +1,80 @@ +<?php +## 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 +# before_filter :require_admin +# +# def index +# list +# render :action => 'list' unless request.xhr? +# end +# +# # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html) +# verify :method => :post, :only => [ :destroy, :move ], :redirect_to => { :action => :list } +# +# def list +# @tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position' +# render :action => "list", :layout => false if request.xhr? +# end +# +# def new +# @tracker = Tracker.new(params[:tracker]) +# if request.post? and @tracker.save +# # workflow copy +# if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from])) +# @tracker.workflows.copy(copy_from) +# end +# flash[:notice] = l(:notice_successful_create) +# redirect_to :action => 'list' +# end +# @trackers = Tracker.find :all, :order => 'position' +# end +# +# def edit +# @tracker = Tracker.find(params[:id]) +# if request.post? and @tracker.update_attributes(params[:tracker]) +# flash[:notice] = l(:notice_successful_update) +# redirect_to :action => 'list' +# end +# end +# +# def move +# @tracker = Tracker.find(params[:id]) +# case params[:position] +# when 'highest' +# @tracker.move_to_top +# when 'higher' +# @tracker.move_higher +# when 'lower' +# @tracker.move_lower +# when 'lowest' +# @tracker.move_to_bottom +# end if params[:position] +# redirect_to :action => 'list' +# end +# +# def destroy +# @tracker = Tracker.find(params[:id]) +# unless @tracker.issues.empty? +# flash[:error] = "This tracker contains issues and can\'t be deleted." +# else +# @tracker.destroy +# end +# redirect_to :action => 'list' +# end +#end diff --git a/app/controllers/users_controller.php b/app/controllers/users_controller.php new file mode 100644 index 0000000..5b1615c --- /dev/null +++ b/app/controllers/users_controller.php @@ -0,0 +1,105 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class UsersController < ApplicationController +# before_filter :require_admin +# +# helper :sort +# include SortHelper +# helper :custom_fields +# include CustomFieldsHelper +# +# def index +# list +# render :action => 'list' unless request.xhr? +# end +# +# def list +# sort_init 'login', 'asc' +# sort_update %w(login firstname lastname mail admin created_on last_login_on) +# +# @status = params[:status] ? params[:status].to_i : 1 +# c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status]) +# +# unless params[:name].blank? +# name = "%#{params[:name].strip.downcase}%" +# c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", name, name, name] +# end +# +# @user_count = User.count(:conditions => c.conditions) +# @user_pages = Paginator.new self, @user_count, +# per_page_option, +# params['page'] +# @users = User.find :all,:order => sort_clause, +# :conditions => c.conditions, +# :limit => @user_pages.items_per_page, +# :offset => @user_pages.current.offset +# +# render :action => "list", :layout => false if request.xhr? +# end +# +# def add +# if request.get? +# @user = User.new(:language => Setting.default_language) +# else +# @user = User.new(params[:user]) +# @user.admin = params[:user][:admin] || false +# @user.login = params[:user][:login] +# @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id +# if @user.save +# Mailer.deliver_account_information(@user, params[:password]) if params[:send_information] +# flash[:notice] = l(:notice_successful_create) +# redirect_to :action => 'list' +# end +# end +# @auth_sources = AuthSource.find(:all) +# end +# +# def edit +# @user = User.find(params[:id]) +# if request.post? +# @user.admin = params[:user][:admin] if params[:user][:admin] +# @user.login = params[:user][:login] if params[:user][:login] +# @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id +# if @user.update_attributes(params[:user]) +# flash[:notice] = l(:notice_successful_update) +# # Give a string to redirect_to otherwise it would use status param as the response code +# redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page])) +# end +# end +# @auth_sources = AuthSource.find(:all) +# @roles = Role.find_all_givable +# @projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects +# @membership ||= Member.new +# @memberships = @user.memberships +# end +# +# def edit_membership +# @user = User.find(params[:id]) +# @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user) +# @membership.attributes = params[:membership] +# @membership.save if request.post? +# redirect_to :action => 'edit', :id => @user, :tab => 'memberships' +# end +# +# def destroy_membership +# @user = User.find(params[:id]) +# Member.find(params[:membership_id]).destroy if request.post? +# redirect_to :action => 'edit', :id => @user, :tab => 'memberships' +# end +#end diff --git a/app/controllers/versions_controller.php b/app/controllers/versions_controller.php new file mode 100644 index 0000000..ab08dd2 --- /dev/null +++ b/app/controllers/versions_controller.php @@ -0,0 +1,55 @@ +<?php +## 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 +# menu_item :roadmap +# before_filter :find_project, :authorize +# +# def show +# end +# +# def edit +# if request.post? and @version.update_attributes(params[:version]) +# flash[:notice] = l(:notice_successful_update) +# redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project +# end +# end +# +# def destroy +# @version.destroy +# redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project +# rescue +# flash[:error] = l(:notice_unable_delete_version) +# redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project +# end +# +# def status_by +# respond_to do |format| +# format.html { render :action => 'show' } +# format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} } +# end +# end +# +#private +# def find_project +# @version = Version.find(params[:id]) +# @project = @version.project +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +#end diff --git a/app/controllers/watchers_controller.php b/app/controllers/watchers_controller.php new file mode 100644 index 0000000..9befbe5 --- /dev/null +++ b/app/controllers/watchers_controller.php @@ -0,0 +1,71 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class WatchersController < ApplicationController +# before_filter :find_project +# before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch] +# before_filter :authorize, :only => :new +# +# verify :method => :post, +# :only => [ :watch, :unwatch ], +# :render => { :nothing => true, :status => :method_not_allowed } +# +# def watch +# set_watcher(User.current, true) +# end +# +# def unwatch +# set_watcher(User.current, false) +# end +# +# def new +# @watcher = Watcher.new(params[:watcher]) +# @watcher.watchable = @watched +# @watcher.save if request.post? +# respond_to do |format| +# format.html { redirect_to :back } +# format.js do +# render :update do |page| +# page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched} +# end +# end +# end +# rescue ::ActionController::RedirectBackError +# render :text => 'Watcher added.', :layout => true +# end +# +#private +# def find_project +# klass = Object.const_get(params[:object_type].camelcase) +# return false unless klass.respond_to?('watched_by') +# @watched = klass.find(params[:object_id]) +# @project = @watched.project +# rescue +# render_404 +# end +# +# def set_watcher(user, watching) +# @watched.set_watcher(user, watching) +# respond_to do |format| +# format.html { redirect_to :back } +# format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} } +# end +# rescue ::ActionController::RedirectBackError +# render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true +# end +#end diff --git a/app/controllers/welcome_controller.php b/app/controllers/welcome_controller.php new file mode 100755 index 0000000..a26d558 --- /dev/null +++ b/app/controllers/welcome_controller.php @@ -0,0 +1,9 @@ +<?php +class WelcomeController extends AppController +{ + var $uses = array('News'/*,'Project'*/); + function index(){ + //$news = $this->News->latest($this->User->current()); + //$projects = $this->Project->latest($this->User->current()); + } +} diff --git a/app/controllers/wiki_controller.php b/app/controllers/wiki_controller.php new file mode 100644 index 0000000..624a61f --- /dev/null +++ b/app/controllers/wiki_controller.php @@ -0,0 +1,212 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#require 'diff' +# +#class WikiController < ApplicationController +# before_filter :find_wiki, :authorize +# before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy] +# +# verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index } +# +# helper :attachments +# include AttachmentsHelper +# +# # display a page (in editing mode if it doesn't exist) +# def index +# page_title = params[:page] +# @page = @wiki.find_or_new_page(page_title) +# if @page.new_record? +# if User.current.allowed_to?(:edit_wiki_pages, @project) +# edit +# render :action => 'edit' +# else +# render_404 +# end +# return +# end +# if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project) +# # Redirects user to the current version if he's not allowed to view previous versions +# redirect_to :version => nil +# return +# end +# @content = @page.content_for_version(params[:version]) +# if params[:export] == 'html' +# export = render_to_string :action => 'export', :layout => false +# send_data(export, :type => 'text/html', :filename => "#{@page.title}.html") +# return +# elsif params[:export] == 'txt' +# send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt") +# return +# end +# @editable = editable? +# render :action => 'show' +# end +# +# # edit an existing page or a new one +# def edit +# @page = @wiki.find_or_new_page(params[:page]) +# return render_403 unless editable? +# @page.content = WikiContent.new(:page => @page) if @page.new_record? +# +# @content = @page.content_for_version(params[:version]) +# @content.text = initial_page_content(@page) if @content.text.blank? +# # don't keep previous comment +# @content.comments = nil +# if request.get? +# # To prevent StaleObjectError exception when reverting to a previous version +# @content.version = @page.content.version +# else +# if !@page.new_record? && @content.text == params[:content][:text] +# # don't save if text wasn't changed +# redirect_to :action => 'index', :id => @project, :page => @page.title +# return +# end +# #@content.text = params[:content][:text] +# #@content.comments = params[:content][:comments] +# @content.attributes = params[:content] +# @content.author = User.current +# # if page is new @page.save will also save content, but not if page isn't a new record +# if (@page.new_record? ? @page.save : @content.save) +# redirect_to :action => 'index', :id => @project, :page => @page.title +# end +# end +# rescue ActiveRecord::StaleObjectError +# # Optimistic locking exception +# flash[:error] = l(:notice_locking_conflict) +# end +# +# # rename a page +# def rename +# return render_403 unless editable? +# @page.redirect_existing_links = true +# # used to display the *original* title if some AR validation errors occur +# @original_title = @page.pretty_title +# if request.post? && @page.update_attributes(params[:wiki_page]) +# flash[:notice] = l(:notice_successful_update) +# redirect_to :action => 'index', :id => @project, :page => @page.title +# end +# end +# +# def protect +# @page.update_attribute :protected, params[:protected] +# redirect_to :action => 'index', :id => @project, :page => @page.title +# end +# +# # show page history +# def history +# @version_count = @page.content.versions.count +# @version_pages = Paginator.new self, @version_count, per_page_option, params['p'] +# # don't load text +# @versions = @page.content.versions.find :all, +# :select => "id, author_id, comments, updated_on, version", +# :order => 'version DESC', +# :limit => @version_pages.items_per_page + 1, +# :offset => @version_pages.current.offset +# +# render :layout => false if request.xhr? +# end +# +# def diff +# @diff = @page.diff(params[:version], params[:version_from]) +# render_404 unless @diff +# end +# +# def annotate +# @annotate = @page.annotate(params[:version]) +# render_404 unless @annotate +# end +# +# # remove a wiki page and its history +# def destroy +# return render_403 unless editable? +# @page.destroy +# redirect_to :action => 'special', :id => @project, :page => 'Page_index' +# end +# +# # display special pages +# def special +# page_title = params[:page].downcase +# case page_title +# # show pages index, sorted by title +# when 'page_index', 'date_index' +# # eager load information about last updates, without loading text +# @pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on", +# :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id", +# :order => 'title' +# @pages_by_date = @pages.group_by {|p| p.updated_on.to_date} +# @pages_by_parent_id = @pages.group_by(&:parent_id) +# # export wiki to a single html file +# when 'export' +# @pages = @wiki.pages.find :all, :order => 'title' +# export = render_to_string :action => 'export_multiple', :layout => false +# send_data(export, :type => 'text/html', :filename => "wiki.html") +# return +# else +# # requested special page doesn't exist, redirect to default page +# redirect_to :action => 'index', :id => @project, :page => nil and return +# end +# render :action => "special_#{page_title}" +# end +# +# def preview +# page = @wiki.find_page(params[:page]) +# # page is nil when previewing a new page +# return render_403 unless page.nil? || editable?(page) +# if page +# @attachements = page.attachments +# @previewed = page.content +# end +# @text = params[:content][:text] +# render :partial => 'common/preview' +# end +# +# def add_attachment +# return render_403 unless editable? +# attach_files(@page, params[:attachments]) +# redirect_to :action => 'index', :page => @page.title +# end +# +#private +# +# def find_wiki +# @project = Project.find(params[:id]) +# @wiki = @project.wiki +# render_404 unless @wiki +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +# +# # Finds the requested page and returns a 404 error if it doesn't exist +# def find_existing_page +# @page = @wiki.find_page(params[:page]) +# render_404 if @page.nil? +# end +# +# # Returns true if the current user is allowed to edit the page, otherwise false +# def editable?(page = @page) +# page.editable_by?(User.current) +# end +# +# # Returns the default content of a new wiki page +# def initial_page_content(page) +# helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) +# extend helper unless self.instance_of?(helper) +# helper.instance_method(:initial_page_content).bind(self).call(page) +# end +#end diff --git a/app/controllers/wikis_controller.php b/app/controllers/wikis_controller.php new file mode 100644 index 0000000..4f63dda --- /dev/null +++ b/app/controllers/wikis_controller.php @@ -0,0 +1,45 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class WikisController < ApplicationController +# menu_item :settings +# before_filter :find_project, :authorize +# +# # Create or update a project's wiki +# def edit +# @wiki = @project.wiki || Wiki.new(:project => @project) +# @wiki.attributes = params[:wiki] +# @wiki.save if request.post? +# render(:update) {|page| page.replace_html "tab-content-wiki", :partial => 'projects/settings/wiki'} +# end +# +# # Delete a project's wiki +# def destroy +# if request.post? && params[:confirm] && @project.wiki +# @project.wiki.destroy +# redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'wiki' +# end +# end +# +#private +# def find_project +# @project = Project.find(params[:id]) +# rescue ActiveRecord::RecordNotFound +# render_404 +# end +#end diff --git a/app/controllers/workflows_controller.php b/app/controllers/workflows_controller.php new file mode 100644 index 0000000..e45ba31 --- /dev/null +++ b/app/controllers/workflows_controller.php @@ -0,0 +1,46 @@ +<?php +## Redmine - project management software +## Copyright (C) 2006-2008 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 WorkflowsController < ApplicationController +# before_filter :require_admin +# +# def index +# @workflow_counts = Workflow.count_by_tracker_and_role +# end +# +# def edit +# @role = Role.find_by_id(params[:role_id]) +# @tracker = Tracker.find_by_id(params[:tracker_id]) +# +# if request.post? +# Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id]) +# (params[:issue_status] || []).each { |old, news| +# news.each { |new| +# @role.workflows.build(:tracker_id => @tracker.id, :old_status_id => old, :new_status_id => new) +# } +# } +# if @role.save +# flash[:notice] = l(:notice_successful_update) +# redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker +# end +# end +# @roles = Role.find(:all, :order => 'builtin, position') +# @trackers = Tracker.find(:all, :order => 'position') +# @statuses = IssueStatus.find(:all, :order => 'position') +# end +#end diff --git a/app/index.php b/app/index.php new file mode 100755 index 0000000..4e60b84 --- /dev/null +++ b/app/index.php @@ -0,0 +1,24 @@ +<?php +/* SVN FILE: $Id: index.php 7945 2008-12-19 02:16:01Z gwoo $ */ +/** + * PHP versions 4 and 5 + * + * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org) + * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project + * @package cake + * @subpackage cake.app + * @since CakePHP(tm) v 0.10.0.1076 + * @version $Revision: 7945 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2008-12-18 18:16:01 -0800 (Thu, 18 Dec 2008) $ + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ +require 'webroot' . DIRECTORY_SEPARATOR . 'index.php'; +?> \ No newline at end of file diff --git a/app/locale/eng/LC_MESSAGES/empty b/app/locale/eng/LC_MESSAGES/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/models/attachment.php b/app/models/attachment.php new file mode 100644 index 0000000..2013429 --- /dev/null +++ b/app/models/attachment.php @@ -0,0 +1,152 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#require "digest/md5" +# +#class Attachment < ActiveRecord::Base +# belongs_to :container, :polymorphic => true +# belongs_to :author, :class_name => "User", :foreign_key => "author_id" +# +# validates_presence_of :container, :filename, :author +# validates_length_of :filename, :maximum => 255 +# validates_length_of :disk_filename, :maximum => 255 +# +# acts_as_event :title => :filename, +# :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}} +# +# acts_as_activity_provider :type => 'files', +# :permission => :view_files, +# :author_key => :author_id, +# :find_options => {:select => "#{Attachment.table_name}.*", +# :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " + +# "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id"} +# +# acts_as_activity_provider :type => 'documents', +# :permission => :view_documents, +# :author_key => :author_id, +# :find_options => {:select => "#{Attachment.table_name}.*", +# :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " + +# "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"} +# +# cattr_accessor :storage_path +# @@storage_path = "#{RAILS_ROOT}/files" +# +# def validate +# errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes +# end +# +# def file=(incoming_file) +# unless incoming_file.nil? +# @temp_file = incoming_file +# if @temp_file.size > 0 +# self.filename = sanitize_filename(@temp_file.original_filename) +# self.disk_filename = Attachment.disk_filename(filename) +# self.content_type = @temp_file.content_type.to_s.chomp +# self.filesize = @temp_file.size +# end +# end +# end +# +# def file +# nil +# end +# +# # Copy temp file to its final location +# def before_save +# if @temp_file && (@temp_file.size > 0) +# logger.debug("saving '#{self.diskfile}'") +# File.open(diskfile, "wb") do |f| +# f.write(@temp_file.read) +# end +# self.digest = self.class.digest(diskfile) +# end +# # Don't save the content type if it's longer than the authorized length +# if self.content_type && self.content_type.length > 255 +# self.content_type = nil +# end +# end +# +# # Deletes file on the disk +# def after_destroy +# File.delete(diskfile) if !filename.blank? && File.exist?(diskfile) +# end +# +# # Returns file's location on disk +# def diskfile +# "#{@@storage_path}/#{self.disk_filename}" +# end +# +# def increment_download +# increment!(:downloads) +# end +# +# def project +# container.project +# end +# +# def visible?(user=User.current) +# container.attachments_visible?(user) +# end +# +# def deletable?(user=User.current) +# container.attachments_deletable?(user) +# end +# +# def image? +# self.filename =~ /\.(jpe?g|gif|png)$/i +# end +# +# def is_text? +# Redmine::MimeType.is_type?('text', filename) +# end +# +# def is_diff? +# self.filename =~ /\.(patch|diff)$/i +# end +# +#private +# def sanitize_filename(value) +# # get only the filename, not the whole path +# just_filename = value.gsub(/^.*(\\|\/)/, '') +# # NOTE: File.basename doesn't work right with Windows paths on Unix +# # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/')) +# +# # Finally, replace all non alphanumeric, hyphens or periods with underscore +# @filename = just_filename.gsub(/[^\w\.\-]/,'_') +# end +# +# # Returns an ASCII or hashed filename +# def self.disk_filename(filename) +# df = DateTime.now.strftime("%y%m%d%H%M%S") + "_" +# if filename =~ %r{^[a-zA-Z0-9_\.\-]*$} +# df << filename +# else +# df << Digest::MD5.hexdigest(filename) +# # keep the extension if any +# df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$} +# end +# df +# end +# +# # Returns the MD5 digest of the file at given path +# def self.digest(filename) +# File.open(filename, 'rb') do |f| +# Digest::MD5.hexdigest(f.read) +# end +# end +#end diff --git a/app/models/auth_source.php b/app/models/auth_source.php new file mode 100644 index 0000000..b481755 --- /dev/null +++ b/app/models/auth_source.php @@ -0,0 +1,50 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class AuthSource < ActiveRecord::Base +# has_many :users +# +# validates_presence_of :name +# validates_uniqueness_of :name +# validates_length_of :name, :maximum => 60 +# +# def authenticate(login, password) +# end +# +# def test_connection +# end +# +# def auth_method_name +# "Abstract" +# end +# +# # Try to authenticate a user not yet registered against available sources +# def self.authenticate(login, password) +# AuthSource.find(:all, :conditions => ["onthefly_register=?", true]).each do |source| +# begin +# logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug? +# attrs = source.authenticate(login, password) +# rescue => e +# logger.error "Error during authentication: #{e.message}" +# attrs = nil +# end +# return attrs if attrs +# end +# return nil +# end +#end diff --git a/app/models/auth_source_ldap.php b/app/models/auth_source_ldap.php new file mode 100644 index 0000000..f768ac6 --- /dev/null +++ b/app/models/auth_source_ldap.php @@ -0,0 +1,99 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#require 'net/ldap' +#require 'iconv' +# +#class AuthSourceLdap < AuthSource +# validates_presence_of :host, :port, :attr_login +# validates_length_of :name, :host, :account_password, :maximum => 60, :allow_nil => true +# validates_length_of :account, :base_dn, :maximum => 255, :allow_nil => true +# validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true +# validates_numericality_of :port, :only_integer => true +# +# before_validation :strip_ldap_attributes +# +# def after_initialize +# self.port = 389 if self.port == 0 +# end +# +# def authenticate(login, password) +# return nil if login.blank? || password.blank? +# attrs = [] +# # get user's DN +# ldap_con = initialize_ldap_con(self.account, self.account_password) +# login_filter = Net::LDAP::Filter.eq( self.attr_login, login ) +# object_filter = Net::LDAP::Filter.eq( "objectClass", "*" ) +# dn = String.new +# ldap_con.search( :base => self.base_dn, +# :filter => object_filter & login_filter, +# # only ask for the DN if on-the-fly registration is disabled +# :attributes=> (onthefly_register? ? ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail] : ['dn'])) do |entry| +# dn = entry.dn +# attrs = [:firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname), +# :lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname), +# :mail => AuthSourceLdap.get_attr(entry, self.attr_mail), +# :auth_source_id => self.id ] if onthefly_register? +# end +# return nil if dn.empty? +# logger.debug "DN found for #{login}: #{dn}" if logger && logger.debug? +# # authenticate user +# ldap_con = initialize_ldap_con(dn, password) +# return nil unless ldap_con.bind +# # return user's attributes +# logger.debug "Authentication successful for '#{login}'" if logger && logger.debug? +# attrs +# rescue Net::LDAP::LdapError => text +# raise "LdapError: " + text +# end +# +# # test the connection to the LDAP +# def test_connection +# ldap_con = initialize_ldap_con(self.account, self.account_password) +# ldap_con.open { } +# rescue Net::LDAP::LdapError => text +# raise "LdapError: " + text +# end +# +# def auth_method_name +# "LDAP" +# end +# +# private +# +# def strip_ldap_attributes +# [:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr| +# write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil? +# end +# end +# +# def initialize_ldap_con(ldap_user, ldap_password) +# options = { :host => self.host, +# :port => self.port, +# :encryption => (self.tls ? :simple_tls : nil) +# } +# options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank? +# Net::LDAP.new options +# end +# +# def self.get_attr(entry, attr_name) +# if !attr_name.blank? +# entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name] +# end +# end +#end diff --git a/app/models/behaviors/empty b/app/models/behaviors/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/models/board.php b/app/models/board.php new file mode 100644 index 0000000..67781bb --- /dev/null +++ b/app/models/board.php @@ -0,0 +1,30 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class Board < ActiveRecord::Base +# belongs_to :project +# has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC" +# has_many :messages, :dependent => :delete_all, :order => "#{Message.table_name}.created_on DESC" +# belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id +# acts_as_list :scope => :project_id +# acts_as_watchable +# +# validates_presence_of :name, :description +# validates_length_of :name, :maximum => 30 +# validates_length_of :description, :maximum => 255 +#end diff --git a/app/models/change.php b/app/models/change.php new file mode 100644 index 0000000..35b7750 --- /dev/null +++ b/app/models/change.php @@ -0,0 +1,27 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class Change < ActiveRecord::Base +# belongs_to :changeset +# +# validates_presence_of :changeset_id, :action, :path +# +# def relative_path +# changeset.repository.relative_path(path) +# end +#end diff --git a/app/models/changeset.php b/app/models/changeset.php new file mode 100644 index 0000000..11bc313 --- /dev/null +++ b/app/models/changeset.php @@ -0,0 +1,156 @@ +<?php +## Redmine - project management software +## Copyright (C) 2006-2008 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#require 'iconv' +# +#class Changeset < ActiveRecord::Base +# belongs_to :repository +# belongs_to :user +# has_many :changes, :dependent => :delete_all +# has_and_belongs_to_many :issues +# +# acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))}, +# :description => :comments, +# :datetime => :committed_on, +# :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}} +# +# acts_as_searchable :columns => 'comments', +# :include => {:repository => :project}, +# :project_key => "#{Repository.table_name}.project_id", +# :date_column => 'committed_on' +# +# acts_as_activity_provider :timestamp => "#{table_name}.committed_on", +# :author_key => :user_id, +# :find_options => {:include => {:repository => :project}} +# +# validates_presence_of :repository_id, :revision, :committed_on, :commit_date +# validates_uniqueness_of :revision, :scope => :repository_id +# validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true +# +# def revision=(r) +# write_attribute :revision, (r.nil? ? nil : r.to_s) +# end +# +# def comments=(comment) +# write_attribute(:comments, Changeset.normalize_comments(comment)) +# end +# +# def committed_on=(date) +# self.commit_date = date +# super +# end +# +# def project +# repository.project +# end +# +# def author +# user || committer.to_s.split('<').first +# end +# +# def before_create +# self.user = repository.find_committer_user(committer) +# end +# +# def after_create +# scan_comment_for_issue_ids +# end +# require 'pp' +# +# def scan_comment_for_issue_ids +# return if comments.blank? +# # keywords used to reference issues +# ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip) +# # keywords used to fix issues +# fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip) +# # status and optional done ratio applied +# fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id) +# done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i +# +# kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|") +# return if kw_regexp.blank? +# +# referenced_issues = [] +# +# if ref_keywords.delete('*') +# # find any issue ID in the comments +# target_issue_ids = [] +# comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] } +# referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids) +# end +# +# comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match| +# action = match[0] +# target_issue_ids = match[1].scan(/\d+/) +# target_issues = repository.project.issues.find_all_by_id(target_issue_ids) +# if fix_status && fix_keywords.include?(action.downcase) +# # update status of issues +# logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug? +# target_issues.each do |issue| +# # the issue may have been updated by the closure of another one (eg. duplicate) +# issue.reload +# # don't change the status is the issue is closed +# next if issue.status.is_closed? +# csettext = "r#{self.revision}" +# if self.scmid && (! (csettext =~ /^r[0-9]+$/)) +# csettext = "commit:\"#{self.scmid}\"" +# end +# journal = issue.init_journal(user || User.anonymous, l(:text_status_changed_by_changeset, csettext)) +# issue.status = fix_status +# issue.done_ratio = done_ratio if done_ratio +# issue.save +# Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') +# end +# end +# referenced_issues += target_issues +# end +# +# self.issues = referenced_issues.uniq +# end +# +# # Returns the previous changeset +# def previous +# @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC') +# end +# +# # Returns the next changeset +# def next +# @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC') +# end +# +# # Strips and reencodes a commit log before insertion into the database +# def self.normalize_comments(str) +# to_utf8(str.to_s.strip) +# end +# +# private +# +# +# def self.to_utf8(str) +# return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii +# encoding = Setting.commit_logs_encoding.to_s.strip +# unless encoding.blank? || encoding == 'UTF-8' +# begin +# return Iconv.conv('UTF-8', encoding, str) +# rescue Iconv::Failure +# # do nothing here +# end +# end +# str +# end +#end diff --git a/app/models/comment.php b/app/models/comment.php new file mode 100644 index 0000000..33b3ae8 --- /dev/null +++ b/app/models/comment.php @@ -0,0 +1,24 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class Comment < ActiveRecord::Base +# belongs_to :commented, :polymorphic => true, :counter_cache => true +# belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' +# +# validates_presence_of :commented, :author, :comments +#end diff --git a/app/models/custom_field.php b/app/models/custom_field.php new file mode 100644 index 0000000..bbf6b84 --- /dev/null +++ b/app/models/custom_field.php @@ -0,0 +1,76 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class CustomField < ActiveRecord::Base +# has_many :custom_values, :dependent => :delete_all +# acts_as_list :scope => 'type = \'#{self.class}\'' +# serialize :possible_values +# +# FIELD_FORMATS = { "string" => { :name => :label_string, :order => 1 }, +# "text" => { :name => :label_text, :order => 2 }, +# "int" => { :name => :label_integer, :order => 3 }, +# "float" => { :name => :label_float, :order => 4 }, +# "list" => { :name => :label_list, :order => 5 }, +# "date" => { :name => :label_date, :order => 6 }, +# "bool" => { :name => :label_boolean, :order => 7 } +# }.freeze +# +# validates_presence_of :name, :field_format +# validates_uniqueness_of :name, :scope => :type +# validates_length_of :name, :maximum => 30 +# validates_format_of :name, :with => /^[\w\s\.\'\-]*$/i +# validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys +# +# def initialize(attributes = nil) +# super +# self.possible_values ||= [] +# end +# +# def before_validation +# # remove empty values +# self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact +# # make sure these fields are not searchable +# self.searchable = false if %w(int float date bool).include?(field_format) +# true +# end +# +# def validate +# if self.field_format == "list" +# errors.add(:possible_values, :activerecord_error_blank) if self.possible_values.nil? || self.possible_values.empty? +# errors.add(:possible_values, :activerecord_error_invalid) unless self.possible_values.is_a? Array +# end +# +# # validate default value +# v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil) +# v.custom_field.is_required = false +# errors.add(:default_value, :activerecord_error_invalid) unless v.valid? +# end +# +# def <=>(field) +# position <=> field.position +# end +# +# # to move in project_custom_field +# def self.for_all +# find(:all, :conditions => ["is_for_all=?", true], :order => 'position') +# end +# +# def type_name +# nil +# end +#end diff --git a/app/models/custom_value.php b/app/models/custom_value.php new file mode 100644 index 0000000..14fe871 --- /dev/null +++ b/app/models/custom_value.php @@ -0,0 +1,56 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class CustomValue < ActiveRecord::Base +# belongs_to :custom_field +# belongs_to :customized, :polymorphic => true +# +# def after_initialize +# if custom_field && new_record? && (customized_type.blank? || (customized && customized.new_record?)) +# self.value ||= custom_field.default_value +# end +# end +# +# # Returns true if the boolean custom value is true +# def true? +# self.value == '1' +# end +# +#protected +# def validate +# if value.blank? +# errors.add(:value, :activerecord_error_blank) if custom_field.is_required? and value.blank? +# else +# errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp) +# errors.add(:value, :activerecord_error_too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length +# errors.add(:value, :activerecord_error_too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length +# +# # Format specific validations +# case custom_field.field_format +# when 'int' +# errors.add(:value, :activerecord_error_not_a_number) unless value =~ /^[+-]?\d+$/ +# when 'float' +# begin; Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end +# when 'date' +# errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ +# when 'list' +# errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include?(value) +# end +# end +# end +#end diff --git a/app/models/datasources/empty b/app/models/datasources/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/models/document.php b/app/models/document.php new file mode 100644 index 0000000..4eb310d --- /dev/null +++ b/app/models/document.php @@ -0,0 +1,38 @@ +<?php +## 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" +# acts_as_attachable :delete_permission => :manage_documents +# +# acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project +# acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"}, +# :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil }, +# :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}} +# acts_as_activity_provider :find_options => {:include => :project} +# +# validates_presence_of :project, :title, :category +# validates_length_of :title, :maximum => 60 +# +# def after_initialize +# if new_record? +# self.category ||= Enumeration.default('DCAT') +# end +# end +#end diff --git a/app/models/enabled_module.php b/app/models/enabled_module.php new file mode 100644 index 0000000..c93b27a --- /dev/null +++ b/app/models/enabled_module.php @@ -0,0 +1,24 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class EnabledModule < ActiveRecord::Base +# belongs_to :project +# +# validates_presence_of :name +# validates_uniqueness_of :name, :scope => :project_id +#end diff --git a/app/models/enumeration.php b/app/models/enumeration.php new file mode 100644 index 0000000..b7e797f --- /dev/null +++ b/app/models/enumeration.php @@ -0,0 +1,82 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class Enumeration < ActiveRecord::Base +# acts_as_list :scope => 'opt = \'#{opt}\'' +# +# before_destroy :check_integrity +# +# validates_presence_of :opt, :name +# validates_uniqueness_of :name, :scope => [:opt] +# validates_length_of :name, :maximum => 30 +# +# # Single table inheritance would be an option +# OPTIONS = { +# "IPRI" => {:label => :enumeration_issue_priorities, :model => Issue, :foreign_key => :priority_id}, +# "DCAT" => {:label => :enumeration_doc_categories, :model => Document, :foreign_key => :category_id}, +# "ACTI" => {:label => :enumeration_activities, :model => TimeEntry, :foreign_key => :activity_id} +# }.freeze +# +# def self.get_values(option) +# find(:all, :conditions => {:opt => option}, :order => 'position') +# end +# +# def self.default(option) +# find(:first, :conditions => {:opt => option, :is_default => true}, :order => 'position') +# end +# +# def option_name +# OPTIONS[self.opt][:label] +# end +# +# def before_save +# if is_default? && is_default_changed? +# Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt}) +# end +# end +# +# def objects_count +# OPTIONS[self.opt][:model].count(:conditions => "#{OPTIONS[self.opt][:foreign_key]} = #{id}") +# end +# +# def in_use? +# self.objects_count != 0 +# end +# +# alias :destroy_without_reassign :destroy +# +# # Destroy the enumeration +# # If a enumeration is specified, objects are reassigned +# def destroy(reassign_to = nil) +# if reassign_to && reassign_to.is_a?(Enumeration) +# OPTIONS[self.opt][:model].update_all("#{OPTIONS[self.opt][:foreign_key]} = #{reassign_to.id}", "#{OPTIONS[self.opt][:foreign_key]} = #{id}") +# end +# destroy_without_reassign +# end +# +# def <=>(enumeration) +# position <=> enumeration.position +# end +# +# def to_s; name end +# +#private +# def check_integrity +# raise "Can't delete enumeration" if self.in_use? +# end +#end diff --git a/app/models/issue.php b/app/models/issue.php new file mode 100644 index 0000000..683efaf --- /dev/null +++ b/app/models/issue.php @@ -0,0 +1,281 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class Issue < ActiveRecord::Base +# belongs_to :project +# belongs_to :tracker +# belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' +# belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' +# belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id' +# belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id' +# belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id' +# belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' +# +# has_many :journals, :as => :journalized, :dependent => :destroy +# has_many :time_entries, :dependent => :delete_all +# has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC" +# +# has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all +# has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all +# +# acts_as_attachable :after_remove => :attachment_removed +# acts_as_customizable +# acts_as_watchable +# acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"], +# :include => [:project, :journals], +# # sort by id so that limited eager loading doesn't break with postgresql +# :order_column => "#{table_name}.id" +# acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"}, +# :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}} +# +# acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]}, +# :author_key => :author_id +# +# validates_presence_of :subject, :priority, :project, :tracker, :author, :status +# validates_length_of :subject, :maximum => 255 +# validates_inclusion_of :done_ratio, :in => 0..100 +# validates_numericality_of :estimated_hours, :allow_nil => true +# +# def after_initialize +# if new_record? +# # set default values for new records only +# self.status ||= IssueStatus.default +# self.priority ||= Enumeration.default('IPRI') +# end +# end +# +# # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields +# def available_custom_fields +# (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : [] +# end +# +# def copy_from(arg) +# issue = arg.is_a?(Issue) ? arg : Issue.find(arg) +# self.attributes = issue.attributes.dup +# self.custom_values = issue.custom_values.collect {|v| v.clone} +# self +# end +# +# # Move an issue to a new project and tracker +# def move_to(new_project, new_tracker = nil) +# transaction do +# if new_project && project_id != new_project.id +# # delete issue relations +# unless Setting.cross_project_issue_relations? +# self.relations_from.clear +# self.relations_to.clear +# end +# # issue is moved to another project +# # reassign to the category with same name if any +# new_category = category.nil? ? nil : new_project.issue_categories.find_by_name(category.name) +# self.category = new_category +# self.fixed_version = nil +# self.project = new_project +# end +# if new_tracker +# self.tracker = new_tracker +# end +# if save +# # Manually update project_id on related time entries +# TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id}) +# else +# rollback_db_transaction +# return false +# end +# end +# return true +# end +# +# def priority_id=(pid) +# self.priority = nil +# write_attribute(:priority_id, pid) +# end +# +# def estimated_hours=(h) +# write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) +# end +# +# def validate +# if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? +# errors.add :due_date, :activerecord_error_not_a_date +# end +# +# if self.due_date and self.start_date and self.due_date < self.start_date +# errors.add :due_date, :activerecord_error_greater_than_start_date +# end +# +# if start_date && soonest_start && start_date < soonest_start +# errors.add :start_date, :activerecord_error_invalid +# end +# end +# +# def validate_on_create +# errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker) +# end +# +# def before_create +# # default assignment based on category +# if assigned_to.nil? && category && category.assigned_to +# self.assigned_to = category.assigned_to +# end +# end +# +# def before_save +# if @current_journal +# # attributes changes +# (Issue.column_names - %w(id description)).each {|c| +# @current_journal.details << JournalDetail.new(:property => 'attr', +# :prop_key => c, +# :old_value => @issue_before_change.send(c), +# :value => send(c)) unless send(c)==@issue_before_change.send(c) +# } +# # custom fields changes +# custom_values.each {|c| +# next if (@custom_values_before_change[c.custom_field_id]==c.value || +# (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?)) +# @current_journal.details << JournalDetail.new(:property => 'cf', +# :prop_key => c.custom_field_id, +# :old_value => @custom_values_before_change[c.custom_field_id], +# :value => c.value) +# } +# @current_journal.save +# end +# # Save the issue even if the journal is not saved (because empty) +# true +# end +# +# def after_save +# # Reload is needed in order to get the right status +# reload +# +# # Update start/due dates of following issues +# relations_from.each(&:set_issue_to_dates) +# +# # Close duplicates if the issue was closed +# if @issue_before_change && !@issue_before_change.closed? && self.closed? +# duplicates.each do |duplicate| +# # Reload is need in case the duplicate was updated by a previous duplicate +# duplicate.reload +# # Don't re-close it if it's already closed +# next if duplicate.closed? +# # Same user and notes +# duplicate.init_journal(@current_journal.user, @current_journal.notes) +# duplicate.update_attribute :status, self.status +# end +# end +# end +# +# def init_journal(user, notes = "") +# @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) +# @issue_before_change = self.clone +# @issue_before_change.status = self.status +# @custom_values_before_change = {} +# self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value } +# # Make sure updated_on is updated when adding a note. +# updated_on_will_change! +# @current_journal +# end +# +# # Return true if the issue is closed, otherwise false +# def closed? +# self.status.is_closed? +# end +# +# # Returns true if the issue is overdue +# def overdue? +# !due_date.nil? && (due_date < Date.today) +# end +# +# # Users the issue can be assigned to +# def assignable_users +# project.assignable_users +# end +# +# # Returns an array of status that user is able to apply +# def new_statuses_allowed_to(user) +# statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker) +# statuses << status unless statuses.empty? +# statuses.uniq.sort +# end +# +# # Returns the mail adresses of users that should be notified for the issue +# def recipients +# recipients = project.recipients +# # Author and assignee are always notified unless they have been locked +# recipients << author.mail if author && author.active? +# recipients << assigned_to.mail if assigned_to && assigned_to.active? +# recipients.compact.uniq +# end +# +# def spent_hours +# @spent_hours ||= time_entries.sum(:hours) || 0 +# end +# +# def relations +# (relations_from + relations_to).sort +# end +# +# def all_dependent_issues +# dependencies = [] +# relations_from.each do |relation| +# dependencies << relation.issue_to +# dependencies += relation.issue_to.all_dependent_issues +# end +# dependencies +# end +# +# # Returns an array of issues that duplicate this one +# def duplicates +# relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from} +# end +# +# # Returns the due date or the target due date if any +# # Used on gantt chart +# def due_before +# due_date || (fixed_version ? fixed_version.effective_date : nil) +# end +# +# def duration +# (start_date && due_date) ? due_date - start_date : 0 +# end +# +# def soonest_start +# @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min +# end +# +# def self.visible_by(usr) +# with_scope(:find => { :conditions => Project.visible_by(usr) }) do +# yield +# end +# end +# +# def to_s +# "#{tracker} ##{id}: #{subject}" +# end +# +# private +# +# # Callback on attachment deletion +# def attachment_removed(obj) +# journal = init_journal(User.current) +# journal.details << JournalDetail.new(:property => 'attachment', +# :prop_key => obj.id, +# :old_value => obj.filename) +# journal.save +# end +#end diff --git a/app/models/issue_category.php b/app/models/issue_category.php new file mode 100644 index 0000000..9ce980d --- /dev/null +++ b/app/models/issue_category.php @@ -0,0 +1,44 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class IssueCategory < ActiveRecord::Base +# belongs_to :project +# belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id' +# has_many :issues, :foreign_key => 'category_id', :dependent => :nullify +# +# validates_presence_of :name +# validates_uniqueness_of :name, :scope => [:project_id] +# validates_length_of :name, :maximum => 30 +# +# alias :destroy_without_reassign :destroy +# +# # Destroy the category +# # If a category is specified, issues are reassigned to this category +# def destroy(reassign_to = nil) +# if reassign_to && reassign_to.is_a?(IssueCategory) && reassign_to.project == self.project +# Issue.update_all("category_id = #{reassign_to.id}", "category_id = #{id}") +# end +# destroy_without_reassign +# end +# +# def <=>(category) +# name <=> category.name +# end +# +# def to_s; name end +#end diff --git a/app/models/issue_custom_field.php b/app/models/issue_custom_field.php new file mode 100644 index 0000000..e611e02 --- /dev/null +++ b/app/models/issue_custom_field.php @@ -0,0 +1,28 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class IssueCustomField < CustomField +# has_and_belongs_to_many :projects, :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :foreign_key => "custom_field_id" +# has_and_belongs_to_many :trackers, :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :foreign_key => "custom_field_id" +# has_many :issues, :through => :issue_custom_values +# +# def type_name +# :label_issue_plural +# end +#end +# diff --git a/app/models/issue_relation.php b/app/models/issue_relation.php new file mode 100644 index 0000000..a604aa6 --- /dev/null +++ b/app/models/issue_relation.php @@ -0,0 +1,80 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class IssueRelation < ActiveRecord::Base +# belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id' +# belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id' +# +# TYPE_RELATES = "relates" +# TYPE_DUPLICATES = "duplicates" +# TYPE_BLOCKS = "blocks" +# TYPE_PRECEDES = "precedes" +# +# TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 }, +# TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 }, +# TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 }, +# TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 }, +# }.freeze +# +# validates_presence_of :issue_from, :issue_to, :relation_type +# validates_inclusion_of :relation_type, :in => TYPES.keys +# validates_numericality_of :delay, :allow_nil => true +# validates_uniqueness_of :issue_to_id, :scope => :issue_from_id +# +# def validate +# if issue_from && issue_to +# errors.add :issue_to_id, :activerecord_error_invalid if issue_from_id == issue_to_id +# errors.add :issue_to_id, :activerecord_error_not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations? +# errors.add_to_base :activerecord_error_circular_dependency if issue_to.all_dependent_issues.include? issue_from +# end +# end +# +# def other_issue(issue) +# (self.issue_from_id == issue.id) ? issue_to : issue_from +# end +# +# def label_for(issue) +# TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow +# end +# +# def before_save +# if TYPE_PRECEDES == relation_type +# self.delay ||= 0 +# else +# self.delay = nil +# end +# set_issue_to_dates +# end +# +# def set_issue_to_dates +# soonest_start = self.successor_soonest_start +# if soonest_start && (!issue_to.start_date || issue_to.start_date < soonest_start) +# issue_to.start_date, issue_to.due_date = successor_soonest_start, successor_soonest_start + issue_to.duration +# issue_to.save +# end +# end +# +# def successor_soonest_start +# return nil unless (TYPE_PRECEDES == self.relation_type) && (issue_from.start_date || issue_from.due_date) +# (issue_from.due_date || issue_from.start_date) + 1 + delay +# end +# +# def <=>(relation) +# TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order] +# end +#end diff --git a/app/models/issue_status.php b/app/models/issue_status.php new file mode 100644 index 0000000..fe63b2a --- /dev/null +++ b/app/models/issue_status.php @@ -0,0 +1,70 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class IssueStatus < ActiveRecord::Base +# before_destroy :check_integrity +# has_many :workflows, :foreign_key => "old_status_id", :dependent => :delete_all +# acts_as_list +# +# validates_presence_of :name +# validates_uniqueness_of :name +# validates_length_of :name, :maximum => 30 +# validates_format_of :name, :with => /^[\w\s\'\-]*$/i +# +# def after_save +# IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default? +# end +# +# # Returns the default status for new issues +# def self.default +# find(:first, :conditions =>["is_default=?", true]) +# end +# +# # Returns an array of all statuses the given role can switch to +# # Uses association cache when called more than one time +# def new_statuses_allowed_to(role, tracker) +# new_statuses = workflows.select {|w| w.role_id == role.id && w.tracker_id == tracker.id}.collect{|w| w.new_status} if role && tracker +# new_statuses ? new_statuses.compact.sort{|x, y| x.position <=> y.position } : [] +# end +# +# # Same thing as above but uses a database query +# # More efficient than the previous method if called just once +# def find_new_statuses_allowed_to(role, tracker) +# new_statuses = workflows.find(:all, +# :include => :new_status, +# :conditions => ["role_id=? and tracker_id=?", role.id, tracker.id]).collect{ |w| w.new_status }.compact if role && tracker +# new_statuses ? new_statuses.sort{|x, y| x.position <=> y.position } : [] +# end +# +# def new_status_allowed_to?(status, role, tracker) +# status && role && tracker ? +# !workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => role.id, :tracker_id => tracker.id}).nil? : +# false +# end +# +# def <=>(status) +# position <=> status.position +# end +# +# def to_s; name end +# +#private +# def check_integrity +# raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id]) +# end +#end diff --git a/app/models/journal.php b/app/models/journal.php new file mode 100644 index 0000000..39f2208 --- /dev/null +++ b/app/models/journal.php @@ -0,0 +1,69 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class Journal < ActiveRecord::Base +# belongs_to :journalized, :polymorphic => true +# # added as a quick fix to allow eager loading of the polymorphic association +# # since always associated to an issue, for now +# belongs_to :issue, :foreign_key => :journalized_id +# +# belongs_to :user +# has_many :details, :class_name => "JournalDetail", :dependent => :delete_all +# attr_accessor :indice +# +# acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" }, +# :description => :notes, +# :author => :user, +# :type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' }, +# :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}} +# +# acts_as_activity_provider :type => 'issues', +# :permission => :view_issues, +# :author_key => :user_id, +# :find_options => {:include => [{:issue => :project}, :details, :user], +# :conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" + +# " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"} +# +# def save +# # Do not save an empty journal +# (details.empty? && notes.blank?) ? false : super +# end +# +# # Returns the new status if the journal contains a status change, otherwise nil +# def new_status +# c = details.detect {|detail| detail.prop_key == 'status_id'} +# (c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil +# end +# +# def new_value_for(prop) +# c = details.detect {|detail| detail.prop_key == prop} +# c ? c.value : nil +# end +# +# def editable_by?(usr) +# usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project))) +# end +# +# def project +# journalized.respond_to?(:project) ? journalized.project : nil +# end +# +# def attachments +# journalized.respond_to?(:attachments) ? journalized.attachments : nil +# end +#end diff --git a/app/models/journal_detail.php b/app/models/journal_detail.php new file mode 100644 index 0000000..1505b2d --- /dev/null +++ b/app/models/journal_detail.php @@ -0,0 +1,26 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class JournalDetail < ActiveRecord::Base +# belongs_to :journal +# +# def before_save +# self.value = value[0..254] if value && value.is_a?(String) +# self.old_value = old_value[0..254] if old_value && old_value.is_a?(String) +# end +#end diff --git a/app/models/mail_handler.php b/app/models/mail_handler.php new file mode 100644 index 0000000..6f5028f --- /dev/null +++ b/app/models/mail_handler.php @@ -0,0 +1,193 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class MailHandler < ActionMailer::Base +# include ActionView::Helpers::SanitizeHelper +# +# class UnauthorizedAction < StandardError; end +# class MissingInformation < StandardError; end +# +# attr_reader :email, :user +# +# def self.receive(email, options={}) +# @@handler_options = options.dup +# +# @@handler_options[:issue] ||= {} +# +# @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String) +# @@handler_options[:allow_override] ||= [] +# # Project needs to be overridable if not specified +# @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project) +# # Status overridable by default +# @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status) +# super email +# end +# +# # Processes incoming emails +# def receive(email) +# @email = email +# @user = User.active.find(:first, :conditions => ["LOWER(mail) = ?", email.from.first.to_s.strip.downcase]) +# unless @user +# # Unknown user => the email is ignored +# # TODO: ability to create the user's account +# logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info +# return false +# end +# User.current = @user +# dispatch +# end +# +# private +# +# ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]} +# +# def dispatch +# if m = email.subject.match(ISSUE_REPLY_SUBJECT_RE) +# receive_issue_update(m[1].to_i) +# else +# receive_issue +# end +# rescue ActiveRecord::RecordInvalid => e +# # TODO: send a email to the user +# logger.error e.message if logger +# false +# rescue MissingInformation => e +# logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger +# false +# rescue UnauthorizedAction => e +# logger.error "MailHandler: unauthorized attempt from #{user}" if logger +# false +# end +# +# # Creates a new issue +# def receive_issue +# project = target_project +# tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first) +# category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category))) +# priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority))) +# status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status))) +# +# # check permission +# raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) +# issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority) +# # check workflow +# if status && issue.new_statuses_allowed_to(user).include?(status) +# issue.status = status +# end +# issue.subject = email.subject.chomp.toutf8 +# issue.description = plain_text_body +# # custom fields +# issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c| +# if value = get_keyword(c.name, :override => true) +# h[c.id] = value +# end +# h +# end +# issue.save! +# add_attachments(issue) +# logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info +# # add To and Cc as watchers +# add_watchers(issue) +# # send notification after adding watchers so that they can reply to Redmine +# Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added') +# issue +# end +# +# def target_project +# # TODO: other ways to specify project: +# # * parse the email To field +# # * specific project (eg. Setting.mail_handler_target_project) +# target = Project.find_by_identifier(get_keyword(:project)) +# raise MissingInformation.new('Unable to determine target project') if target.nil? +# target +# end +# +# # Adds a note to an existing issue +# def receive_issue_update(issue_id) +# status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status))) +# +# issue = Issue.find_by_id(issue_id) +# return unless issue +# # check permission +# raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project) +# raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project) +# +# # add the note +# journal = issue.init_journal(user, plain_text_body) +# add_attachments(issue) +# # check workflow +# if status && issue.new_statuses_allowed_to(user).include?(status) +# issue.status = status +# end +# issue.save! +# logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info +# Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') +# journal +# end +# +# def add_attachments(obj) +# if email.has_attachments? +# email.attachments.each do |attachment| +# Attachment.create(:container => obj, +# :file => attachment, +# :author => user, +# :content_type => attachment.content_type) +# end +# end +# end +# +# # Adds To and Cc as watchers of the given object if the sender has the +# # appropriate permission +# def add_watchers(obj) +# if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project) +# addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase} +# unless addresses.empty? +# watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses]) +# watchers.each {|w| obj.add_watcher(w)} +# end +# end +# end +# +# def get_keyword(attr, options={}) +# if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i +# $1.strip +# elsif !@@handler_options[:issue][attr].blank? +# @@handler_options[:issue][attr] +# end +# end +# +# # Returns the text/plain part of the email +# # If not found (eg. HTML-only email), returns the body with tags removed +# def plain_text_body +# return @plain_text_body unless @plain_text_body.nil? +# parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten +# if parts.empty? +# parts << @email +# end +# plain_text_part = parts.detect {|p| p.content_type == 'text/plain'} +# if plain_text_part.nil? +# # no text/plain part found, assuming html-only email +# # strip html tags and remove doctype directive +# @plain_text_body = strip_tags(@email.body.to_s) +# @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, '' +# else +# @plain_text_body = plain_text_part.body.to_s +# end +# @plain_text_body.strip! +# end +#end diff --git a/app/models/mailer.php b/app/models/mailer.php new file mode 100644 index 0000000..54fcea6 --- /dev/null +++ b/app/models/mailer.php @@ -0,0 +1,254 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class Mailer < ActionMailer::Base +# helper :application +# helper :issues +# helper :custom_fields +# +# include ActionController::UrlWriter +# +# def issue_add(issue) +# redmine_headers 'Project' => issue.project.identifier, +# 'Issue-Id' => issue.id, +# 'Issue-Author' => issue.author.login +# redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to +# recipients issue.recipients +# cc(issue.watcher_recipients - @recipients) +# subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}" +# body :issue => issue, +# :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue) +# end +# +# def issue_edit(journal) +# issue = journal.journalized +# redmine_headers 'Project' => issue.project.identifier, +# 'Issue-Id' => issue.id, +# 'Issue-Author' => issue.author.login +# redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to +# @author = journal.user +# recipients issue.recipients +# # Watchers in cc +# cc(issue.watcher_recipients - @recipients) +# s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] " +# s << "(#{issue.status.name}) " if journal.new_value_for('status_id') +# s << issue.subject +# subject s +# body :issue => issue, +# :journal => journal, +# :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue) +# end +# +# def reminder(user, issues, days) +# set_language_if_valid user.language +# recipients user.mail +# subject l(:mail_subject_reminder, issues.size) +# body :issues => issues, +# :days => days, +# :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc') +# end +# +# def document_added(document) +# redmine_headers 'Project' => document.project.identifier +# recipients document.project.recipients +# subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}" +# body :document => document, +# :document_url => url_for(:controller => 'documents', :action => 'show', :id => document) +# end +# +# def attachments_added(attachments) +# container = attachments.first.container +# added_to = '' +# added_to_url = '' +# case container.class.name +# when 'Project' +# added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container) +# added_to = "#{l(:label_project)}: #{container}" +# when 'Version' +# added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id) +# added_to = "#{l(:label_version)}: #{container.name}" +# when 'Document' +# added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id) +# added_to = "#{l(:label_document)}: #{container.title}" +# end +# redmine_headers 'Project' => container.project.identifier +# recipients container.project.recipients +# subject "[#{container.project.name}] #{l(:label_attachment_new)}" +# body :attachments => attachments, +# :added_to => added_to, +# :added_to_url => added_to_url +# end +# +# def news_added(news) +# redmine_headers 'Project' => news.project.identifier +# recipients news.project.recipients +# subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}" +# body :news => news, +# :news_url => url_for(:controller => 'news', :action => 'show', :id => news) +# end +# +# def message_posted(message, recipients) +# redmine_headers 'Project' => message.project.identifier, +# 'Topic-Id' => (message.parent_id || message.id) +# recipients(recipients) +# subject "[#{message.board.project.name} - #{message.board.name}] #{message.subject}" +# body :message => message, +# :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root) +# end +# +# def account_information(user, password) +# set_language_if_valid user.language +# recipients user.mail +# subject l(:mail_subject_register, Setting.app_title) +# body :user => user, +# :password => password, +# :login_url => url_for(:controller => 'account', :action => 'login') +# end +# +# def account_activation_request(user) +# # Send the email to all active administrators +# recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact +# subject l(:mail_subject_account_activation_request, Setting.app_title) +# body :user => user, +# :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc') +# end +# +# def lost_password(token) +# set_language_if_valid(token.user.language) +# recipients token.user.mail +# subject l(:mail_subject_lost_password, Setting.app_title) +# body :token => token, +# :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value) +# end +# +# def register(token) +# set_language_if_valid(token.user.language) +# recipients token.user.mail +# subject l(:mail_subject_register, Setting.app_title) +# body :token => token, +# :url => url_for(:controller => 'account', :action => 'activate', :token => token.value) +# end +# +# def test(user) +# set_language_if_valid(user.language) +# recipients user.mail +# subject 'Redmine test' +# body :url => url_for(:controller => 'welcome') +# end +# +# # Overrides default deliver! method to prevent from sending an email +# # with no recipient, cc or bcc +# def deliver!(mail = @mail) +# return false if (recipients.nil? || recipients.empty?) && +# (cc.nil? || cc.empty?) && +# (bcc.nil? || bcc.empty?) +# super +# end +# +# # Sends reminders to issue assignees +# # Available options: +# # * :days => how many days in the future to remind about (defaults to 7) +# # * :tracker => id of tracker for filtering issues (defaults to all trackers) +# # * :project => id or identifier of project to process (defaults to all projects) +# def self.reminders(options={}) +# days = options[:days] || 7 +# project = options[:project] ? Project.find(options[:project]) : nil +# tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil +# +# s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date] +# s << "#{Issue.table_name}.assigned_to_id IS NOT NULL" +# s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}" +# s << "#{Issue.table_name}.project_id = #{project.id}" if project +# s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker +# +# issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker], +# :conditions => s.conditions +# ).group_by(&:assigned_to) +# issues_by_assignee.each do |assignee, issues| +# deliver_reminder(assignee, issues, days) unless assignee.nil? +# end +# end +# +# private +# def initialize_defaults(method_name) +# super +# set_language_if_valid Setting.default_language +# from Setting.mail_from +# +# # URL options +# h = Setting.host_name +# h = h.to_s.gsub(%r{\/.*$}, '') unless ActionController::AbstractRequest.relative_url_root.blank? +# default_url_options[:host] = h +# default_url_options[:protocol] = Setting.protocol +# +# # Common headers +# headers 'X-Mailer' => 'Redmine', +# 'X-Redmine-Host' => Setting.host_name, +# 'X-Redmine-Site' => Setting.app_title +# end +# +# # Appends a Redmine header field (name is prepended with 'X-Redmine-') +# def redmine_headers(h) +# h.each { |k,v| headers["X-Redmine-#{k}"] = v } +# end +# +# # Overrides the create_mail method +# def create_mail +# # Removes the current user from the recipients and cc +# # if he doesn't want to receive notifications about what he does +# @author ||= User.current +# if @author.pref[:no_self_notified] +# recipients.delete(@author.mail) if recipients +# cc.delete(@author.mail) if cc +# end +# # Blind carbon copy recipients +# if Setting.bcc_recipients? +# bcc([recipients, cc].flatten.compact.uniq) +# recipients [] +# cc [] +# end +# super +# end +# +# # Renders a message with the corresponding layout +# def render_message(method_name, body) +# layout = method_name.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml' +# body[:content_for_layout] = render(:file => method_name, :body => body) +# ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}", :use_full_path => true) +# end +# +# # for the case of plain text only +# def body(*params) +# value = super(*params) +# if Setting.plain_text_mail? +# templates = Dir.glob("#{template_path}/#{@template}.text.plain.{rhtml,erb}") +# unless String === @body or templates.empty? +# template = File.basename(templates.first) +# @body[:content_for_layout] = render(:file => template, :body => @body) +# @body = ActionView::Base.new(template_root, @body, self).render(:file => "mailer/layout.text.plain.rhtml", :use_full_path => true) +# return @body +# end +# end +# return value +# end +# +# # Makes partial rendering work with Rails 1.2 (retro-compatibility) +# def self.controller_path +# '' +# end unless respond_to?('controller_path') +#end diff --git a/app/models/member.php b/app/models/member.php new file mode 100644 index 0000000..7058d2d --- /dev/null +++ b/app/models/member.php @@ -0,0 +1,43 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class Member < ActiveRecord::Base +# belongs_to :user +# belongs_to :role +# belongs_to :project +# +# validates_presence_of :role, :user, :project +# validates_uniqueness_of :user_id, :scope => :project_id +# +# def validate +# errors.add :role_id, :activerecord_error_invalid if role && !role.member? +# end +# +# def name +# self.user.name +# end +# +# def <=>(member) +# role == member.role ? (user <=> member.user) : (role <=> member.role) +# end +# +# def before_destroy +# # remove category based auto assignments for this member +# IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id] +# end +#end diff --git a/app/models/message.php b/app/models/message.php new file mode 100644 index 0000000..d0e63e2 --- /dev/null +++ b/app/models/message.php @@ -0,0 +1,90 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class Message < ActiveRecord::Base +# belongs_to :board +# belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' +# acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC" +# acts_as_attachable +# belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id' +# +# acts_as_searchable :columns => ['subject', 'content'], +# :include => {:board => :project}, +# :project_key => 'project_id', +# :date_column => "#{table_name}.created_on" +# acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"}, +# :description => :content, +# :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'}, +# :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} : +# {:id => o.parent_id, :anchor => "message-#{o.id}"})} +# +# acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]}, +# :author_key => :author_id +# acts_as_watchable +# +# attr_protected :locked, :sticky +# validates_presence_of :subject, :content +# validates_length_of :subject, :maximum => 255 +# +# after_create :add_author_as_watcher +# +# def validate_on_create +# # Can not reply to a locked topic +# errors.add_to_base 'Topic is locked' if root.locked? && self != root +# end +# +# def after_create +# board.update_attribute(:last_message_id, self.id) +# board.increment! :messages_count +# if parent +# parent.reload.update_attribute(:last_reply_id, self.id) +# else +# board.increment! :topics_count +# end +# end +# +# def after_destroy +# # The following line is required so that the previous counter +# # updates (due to children removal) are not overwritten +# board.reload +# board.decrement! :messages_count +# board.decrement! :topics_count unless parent +# end +# +# def sticky? +# sticky == 1 +# end +# +# def project +# board.project +# end +# +# def editable_by?(usr) +# usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project))) +# end +# +# def destroyable_by?(usr) +# usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project))) +# end +# +# private +# +# def add_author_as_watcher +# Watcher.create(:watchable => self.root, :user => author) +# end +#end diff --git a/app/models/message_observer.php b/app/models/message_observer.php new file mode 100644 index 0000000..90a3a6d --- /dev/null +++ b/app/models/message_observer.php @@ -0,0 +1,31 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class MessageObserver < ActiveRecord::Observer +# def after_create(message) +# recipients = [] +# # send notification to the topic watchers +# recipients += message.root.watcher_recipients +# # send notification to the board watchers +# recipients += message.board.watcher_recipients +# # send notification to project members who want to be notified +# recipients += message.board.project.recipients +# recipients = recipients.compact.uniq +# Mailer.deliver_message_posted(message, recipients) if !recipients.empty? && Setting.notified_events.include?('message_posted') +# end +#end diff --git a/app/models/news.php b/app/models/news.php new file mode 100644 index 0000000..ed31441 --- /dev/null +++ b/app/models/news.php @@ -0,0 +1,22 @@ +<?php +class News extends AppModel +{ +// belongs_to :project +// belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' +// has_many :comments, :as => :commented, :dependent => :delete_all, :order => "created_on" +// +// validates_presence_of :title, :description +// validates_length_of :title, :maximum => 60 +// validates_length_of :summary, :maximum => 255 +// +// acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project +// acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}} +// acts_as_activity_provider :find_options => {:include => [:project, :author]}, +// :author_key => :author_id +// +// # returns latest news for projects visible by user +// def self.latest(user = User.current, count = 5) +// find(:all, :limit => count, :conditions => Project.allowed_to_condition(user, :view_news), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") +// end +//end +} diff --git a/app/models/project.php b/app/models/project.php new file mode 100644 index 0000000..195986a --- /dev/null +++ b/app/models/project.php @@ -0,0 +1,277 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class Project < ActiveRecord::Base +# # Project statuses +# STATUS_ACTIVE = 1 +# STATUS_ARCHIVED = 9 +# +# has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}" +# has_many :users, :through => :members +# has_many :enabled_modules, :dependent => :delete_all +# has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" +# has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker] +# has_many :issue_changes, :through => :issues, :source => :journals +# has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC" +# has_many :time_entries, :dependent => :delete_all +# has_many :queries, :dependent => :delete_all +# has_many :documents, :dependent => :destroy +# has_many :news, :dependent => :delete_all, :include => :author +# has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name" +# has_many :boards, :dependent => :destroy, :order => "position ASC" +# has_one :repository, :dependent => :destroy +# has_many :changesets, :through => :repository +# has_one :wiki, :dependent => :destroy +# # Custom field for the project issues +# has_and_belongs_to_many :issue_custom_fields, +# :class_name => 'IssueCustomField', +# :order => "#{CustomField.table_name}.position", +# :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", +# :association_foreign_key => 'custom_field_id' +# +# acts_as_tree :order => "name", :counter_cache => true +# acts_as_attachable :view_permission => :view_files, +# :delete_permission => :manage_files +# +# acts_as_customizable +# acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil +# acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"}, +# :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}, +# :author => nil +# +# attr_protected :status, :enabled_module_names +# +# validates_presence_of :name, :identifier +# validates_uniqueness_of :name, :identifier +# validates_associated :repository, :wiki +# validates_length_of :name, :maximum => 30 +# validates_length_of :homepage, :maximum => 255 +# validates_length_of :identifier, :in => 2..20 +# validates_format_of :identifier, :with => /^[a-z0-9\-]*$/ +# +# before_destroy :delete_all_members +# +# named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } } +# +# def identifier=(identifier) +# super unless identifier_frozen? +# end +# +# def identifier_frozen? +# errors[:identifier].nil? && !(new_record? || identifier.blank?) +# end +# +# def issues_with_subprojects(include_subprojects=false) +# conditions = nil +# if include_subprojects +# ids = [id] + child_ids +# conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"] +# end +# conditions ||= ["#{Project.table_name}.id = ?", id] +# # Quick and dirty fix for Rails 2 compatibility +# Issue.send(:with_scope, :find => { :conditions => conditions }) do +# Version.send(:with_scope, :find => { :conditions => conditions }) do +# yield +# end +# end +# end +# +# # returns latest created projects +# # non public projects will be returned only if user is a member of those +# def self.latest(user=nil, count=5) +# find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC") +# end +# +# def self.visible_by(user=nil) +# user ||= User.current +# if user && user.admin? +# return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" +# elsif user && user.memberships.any? +# return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))" +# else +# return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}" +# end +# end +# +# def self.allowed_to_condition(user, permission, options={}) +# statements = [] +# base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" +# if perm = Redmine::AccessControl.permission(permission) +# unless perm.project_module.nil? +# # If the permission belongs to a project module, make sure the module is enabled +# base_statement << " AND EXISTS (SELECT em.id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}' AND em.project_id=#{Project.table_name}.id)" +# end +# end +# if options[:project] +# project_statement = "#{Project.table_name}.id = #{options[:project].id}" +# project_statement << " OR #{Project.table_name}.parent_id = #{options[:project].id}" if options[:with_subprojects] +# base_statement = "(#{project_statement}) AND (#{base_statement})" +# end +# if user.admin? +# # no restriction +# else +# statements << "1=0" +# if user.logged? +# statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission) +# allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id} +# statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any? +# elsif Role.anonymous.allowed_to?(permission) +# # anonymous user allowed on public project +# statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" +# else +# # anonymous user is not authorized +# end +# end +# statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))" +# end +# +# def project_condition(with_subprojects) +# cond = "#{Project.table_name}.id = #{id}" +# cond = "(#{cond} OR #{Project.table_name}.parent_id = #{id})" if with_subprojects +# cond +# end +# +# def self.find(*args) +# if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/) +# project = find_by_identifier(*args) +# raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil? +# project +# else +# super +# end +# end +# +# def to_param +# # id is used for projects with a numeric identifier (compatibility) +# @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier) +# end +# +# def active? +# self.status == STATUS_ACTIVE +# end +# +# def archive +# # Archive subprojects if any +# children.each do |subproject| +# subproject.archive +# end +# update_attribute :status, STATUS_ARCHIVED +# end +# +# def unarchive +# return false if parent && !parent.active? +# update_attribute :status, STATUS_ACTIVE +# end +# +# def active_children +# children.select {|child| child.active?} +# end +# +# # Returns an array of the trackers used by the project and its sub projects +# def rolled_up_trackers +# @rolled_up_trackers ||= +# Tracker.find(:all, :include => :projects, +# :select => "DISTINCT #{Tracker.table_name}.*", +# :conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id], +# :order => "#{Tracker.table_name}.position") +# end +# +# # Deletes all project's members +# def delete_all_members +# Member.delete_all(['project_id = ?', id]) +# end +# +# # Users issues can be assigned to +# def assignable_users +# members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort +# end +# +# # Returns the mail adresses of users that should be always notified on project events +# def recipients +# members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail} +# end +# +# # Returns an array of all custom fields enabled for project issues +# # (explictly associated custom fields and custom fields enabled for all projects) +# def all_issue_custom_fields +# @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort +# end +# +# def project +# self +# end +# +# def <=>(project) +# name.downcase <=> project.name.downcase +# end +# +# def to_s +# name +# end +# +# # Returns a short description of the projects (first lines) +# def short_description(length = 255) +# description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description +# end +# +# def allows_to?(action) +# if action.is_a? Hash +# allowed_actions.include? "#{action[:controller]}/#{action[:action]}" +# else +# allowed_permissions.include? action +# end +# end +# +# def module_enabled?(module_name) +# module_name = module_name.to_s +# enabled_modules.detect {|m| m.name == module_name} +# end +# +# def enabled_module_names=(module_names) +# enabled_modules.clear +# module_names = [] unless module_names && module_names.is_a?(Array) +# module_names.each do |name| +# enabled_modules << EnabledModule.new(:name => name.to_s) +# end +# end +# +# # Returns an auto-generated project identifier based on the last identifier used +# def self.next_identifier +# p = Project.find(:first, :order => 'created_on DESC') +# p.nil? ? nil : p.identifier.to_s.succ +# end +# +#protected +# def validate +# errors.add(parent_id, " must be a root project") if parent and parent.parent +# errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0 +# errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/) +# end +# +#private +# def allowed_permissions +# @allowed_permissions ||= begin +# module_names = enabled_modules.collect {|m| m.name} +# Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name} +# end +# end +# +# def allowed_actions +# @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten +# end +#end diff --git a/app/models/project_custom_field.php b/app/models/project_custom_field.php new file mode 100644 index 0000000..521b909 --- /dev/null +++ b/app/models/project_custom_field.php @@ -0,0 +1,23 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class ProjectCustomField < CustomField +# def type_name +# :label_project_plural +# end +#end diff --git a/app/models/query.php b/app/models/query.php new file mode 100644 index 0000000..bc95edc --- /dev/null +++ b/app/models/query.php @@ -0,0 +1,407 @@ +<?php +## Redmine - project management software +## Copyright (C) 2006-2008 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class QueryColumn +# attr_accessor :name, :sortable, :default_order +# include GLoc +# +# def initialize(name, options={}) +# self.name = name +# self.sortable = options[:sortable] +# self.default_order = options[:default_order] +# end +# +# def caption +# set_language_if_valid(User.current.language) +# l("field_#{name}") +# end +#end +# +#class QueryCustomFieldColumn < QueryColumn +# +# def initialize(custom_field) +# self.name = "cf_#{custom_field.id}".to_sym +# self.sortable = false +# @cf = custom_field +# end +# +# def caption +# @cf.name +# end +# +# def custom_field +# @cf +# end +#end +# +#class Query < ActiveRecord::Base +# belongs_to :project +# belongs_to :user +# serialize :filters +# serialize :column_names +# +# attr_protected :project_id, :user_id +# +# validates_presence_of :name, :on => :save +# validates_length_of :name, :maximum => 255 +# +# @@operators = { "=" => :label_equals, +# "!" => :label_not_equals, +# "o" => :label_open_issues, +# "c" => :label_closed_issues, +# "!*" => :label_none, +# "*" => :label_all, +# ">=" => '>=', +# "<=" => '<=', +# "<t+" => :label_in_less_than, +# ">t+" => :label_in_more_than, +# "t+" => :label_in, +# "t" => :label_today, +# "w" => :label_this_week, +# ">t-" => :label_less_than_ago, +# "<t-" => :label_more_than_ago, +# "t-" => :label_ago, +# "~" => :label_contains, +# "!~" => :label_not_contains } +# +# cattr_reader :operators +# +# @@operators_by_filter_type = { :list => [ "=", "!" ], +# :list_status => [ "o", "=", "!", "c", "*" ], +# :list_optional => [ "=", "!", "!*", "*" ], +# :list_subprojects => [ "*", "!*", "=" ], +# :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ], +# :date_past => [ ">t-", "<t-", "t-", "t", "w" ], +# :string => [ "=", "~", "!", "!~" ], +# :text => [ "~", "!~" ], +# :integer => [ "=", ">=", "<=", "!*", "*" ] } +# +# cattr_reader :operators_by_filter_type +# +# @@available_columns = [ +# QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"), +# QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"), +# QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'), +# QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"), +# QueryColumn.new(:author), +# QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"), +# QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'), +# QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"), +# QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'), +# QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), +# QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), +# QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), +# QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"), +# QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), +# ] +# cattr_reader :available_columns +# +# def initialize(attributes = nil) +# super attributes +# self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } +# set_language_if_valid(User.current.language) +# end +# +# def after_initialize +# # Store the fact that project is nil (used in #editable_by?) +# @is_for_all = project.nil? +# end +# +# def validate +# filters.each_key do |field| +# errors.add label_for(field), :activerecord_error_blank unless +# # filter requires one or more values +# (values_for(field) and !values_for(field).first.blank?) or +# # filter doesn't require any value +# ["o", "c", "!*", "*", "t", "w"].include? operator_for(field) +# end if filters +# end +# +# def editable_by?(user) +# return false unless user +# # Admin can edit them all and regular users can edit their private queries +# return true if user.admin? || (!is_public && self.user_id == user.id) +# # Members can not edit public queries that are for all project (only admin is allowed to) +# is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project) +# end +# +# def available_filters +# return @available_filters if @available_filters +# +# trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers +# +# @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } }, +# "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } }, +# "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } }, +# "subject" => { :type => :text, :order => 8 }, +# "created_on" => { :type => :date_past, :order => 9 }, +# "updated_on" => { :type => :date_past, :order => 10 }, +# "start_date" => { :type => :date, :order => 11 }, +# "due_date" => { :type => :date, :order => 12 }, +# "estimated_hours" => { :type => :integer, :order => 13 }, +# "done_ratio" => { :type => :integer, :order => 14 }} +# +# user_values = [] +# user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? +# if project +# user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] } +# else +# # members of the user's projects +# user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] } +# end +# @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty? +# @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty? +# +# if project +# # project specific filters +# unless @project.issue_categories.empty? +# @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } } +# end +# unless @project.versions.empty? +# @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } } +# end +# unless @project.active_children.empty? +# @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } } +# end +# add_custom_fields_filters(@project.all_issue_custom_fields) +# else +# # global filters for cross project issue list +# add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true})) +# end +# @available_filters +# end +# +# def add_filter(field, operator, values) +# # values must be an array +# return unless values and values.is_a? Array # and !values.first.empty? +# # check if field is defined as an available filter +# if available_filters.has_key? field +# filter_options = available_filters[field] +# # check if operator is allowed for that filter +# #if @@operators_by_filter_type[filter_options[:type]].include? operator +# # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]}) +# # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator +# #end +# filters[field] = {:operator => operator, :values => values } +# end +# end +# +# def add_short_filter(field, expression) +# return unless expression +# parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first +# add_filter field, (parms[0] || "="), [parms[1] || ""] +# end +# +# def has_filter?(field) +# filters and filters[field] +# end +# +# def operator_for(field) +# has_filter?(field) ? filters[field][:operator] : nil +# end +# +# def values_for(field) +# has_filter?(field) ? filters[field][:values] : nil +# end +# +# def label_for(field) +# label = available_filters[field][:name] if available_filters.has_key?(field) +# label ||= field.gsub(/\_id$/, "") +# end +# +# def available_columns +# return @available_columns if @available_columns +# @available_columns = Query.available_columns +# @available_columns += (project ? +# project.all_issue_custom_fields : +# IssueCustomField.find(:all, :conditions => {:is_for_all => true}) +# ).collect {|cf| QueryCustomFieldColumn.new(cf) } +# end +# +# def columns +# if has_default_columns? +# available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) } +# else +# # preserve the column_names order +# column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact +# end +# end +# +# def column_names=(names) +# names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names +# names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names +# write_attribute(:column_names, names) +# end +# +# def has_column?(column) +# column_names && column_names.include?(column.name) +# end +# +# def has_default_columns? +# column_names.nil? || column_names.empty? +# end +# +# def project_statement +# project_clauses = [] +# if project && !@project.active_children.empty? +# ids = [project.id] +# if has_filter?("subproject_id") +# case operator_for("subproject_id") +# when '=' +# # include the selected subprojects +# ids += values_for("subproject_id").each(&:to_i) +# when '!*' +# # main project only +# else +# # all subprojects +# ids += project.child_ids +# end +# elsif Setting.display_subprojects_issues? +# ids += project.child_ids +# end +# project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') +# elsif project +# project_clauses << "#{Project.table_name}.id = %d" % project.id +# end +# project_clauses << Project.allowed_to_condition(User.current, :view_issues) +# project_clauses.join(' AND ') +# end +# +# def statement +# # filters clauses +# filters_clauses = [] +# filters.each_key do |field| +# next if field == "subproject_id" +# v = values_for(field).clone +# next unless v and !v.empty? +# +# sql = '' +# is_custom_filter = false +# if field =~ /^cf_(\d+)$/ +# # custom field +# db_table = CustomValue.table_name +# db_field = 'value' +# is_custom_filter = true +# sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE " +# else +# # regular field +# db_table = Issue.table_name +# db_field = field +# sql << '(' +# end +# +# # "me" value subsitution +# if %w(assigned_to_id author_id).include?(field) +# v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me") +# end +# +# sql = sql + sql_for_field(field, v, db_table, db_field, is_custom_filter) +# +# sql << ')' +# filters_clauses << sql +# end if filters and valid? +# +# (filters_clauses << project_statement).join(' AND ') +# end +# +# private +# +# # Helper method to generate the WHERE sql for a +field+ with a +value+ +# def sql_for_field(field, value, db_table, db_field, is_custom_filter) +# sql = '' +# case operator_for field +# when "=" +# sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" +# when "!" +# sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))" +# when "!*" +# sql = "#{db_table}.#{db_field} IS NULL" +# sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter +# when "*" +# sql = "#{db_table}.#{db_field} IS NOT NULL" +# sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter +# when ">=" +# sql = "#{db_table}.#{db_field} >= #{value.first.to_i}" +# when "<=" +# sql = "#{db_table}.#{db_field} <= #{value.first.to_i}" +# when "o" +# sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id" +# when "c" +# sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id" +# when ">t-" +# sql = date_range_clause(db_table, db_field, - value.first.to_i, 0) +# when "<t-" +# sql = date_range_clause(db_table, db_field, nil, - value.first.to_i) +# when "t-" +# sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i) +# when ">t+" +# sql = date_range_clause(db_table, db_field, value.first.to_i, nil) +# when "<t+" +# sql = date_range_clause(db_table, db_field, 0, value.first.to_i) +# when "t+" +# sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i) +# when "t" +# sql = date_range_clause(db_table, db_field, 0, 0) +# when "w" +# from = l(:general_first_day_of_week) == '7' ? +# # week starts on sunday +# ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) : +# # week starts on monday (Rails default) +# Time.now.at_beginning_of_week +# sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)] +# when "~" +# sql = "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(value.first)}%'" +# when "!~" +# sql = "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(value.first)}%'" +# end +# +# return sql +# end +# +# def add_custom_fields_filters(custom_fields) +# @available_filters ||= {} +# +# custom_fields.select(&:is_filter?).each do |field| +# case field.field_format +# when "text" +# options = { :type => :text, :order => 20 } +# when "list" +# options = { :type => :list_optional, :values => field.possible_values, :order => 20} +# when "date" +# options = { :type => :date, :order => 20 } +# when "bool" +# options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 } +# else +# options = { :type => :string, :order => 20 } +# end +# @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name }) +# end +# end +# +# # Returns a SQL clause for a date or datetime field. +# def date_range_clause(table, field, from, to) +# s = [] +# if from +# s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)]) +# end +# if to +# s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)]) +# end +# s.join(' AND ') +# end +#end diff --git a/app/models/repository.php b/app/models/repository.php new file mode 100644 index 0000000..f3f6a44 --- /dev/null +++ b/app/models/repository.php @@ -0,0 +1,180 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class Repository < ActiveRecord::Base +# belongs_to :project +# has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC" +# has_many :changes, :through => :changesets +# +# # Raw SQL to delete changesets and changes in the database +# # has_many :changesets, :dependent => :destroy is too slow for big repositories +# before_destroy :clear_changesets +# +# # Checks if the SCM is enabled when creating a repository +# validate_on_create { |r| r.errors.add(:type, :activerecord_error_invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) } +# +# # Removes leading and trailing whitespace +# def url=(arg) +# write_attribute(:url, arg ? arg.to_s.strip : nil) +# end +# +# # Removes leading and trailing whitespace +# def root_url=(arg) +# write_attribute(:root_url, arg ? arg.to_s.strip : nil) +# end +# +# def scm +# @scm ||= self.scm_adapter.new url, root_url, login, password +# update_attribute(:root_url, @scm.root_url) if root_url.blank? +# @scm +# end +# +# def scm_name +# self.class.scm_name +# end +# +# def supports_cat? +# scm.supports_cat? +# end +# +# def supports_annotate? +# scm.supports_annotate? +# end +# +# def entry(path=nil, identifier=nil) +# scm.entry(path, identifier) +# end +# +# def entries(path=nil, identifier=nil) +# scm.entries(path, identifier) +# end +# +# def properties(path, identifier=nil) +# scm.properties(path, identifier) +# end +# +# def cat(path, identifier=nil) +# scm.cat(path, identifier) +# end +# +# def diff(path, rev, rev_to) +# scm.diff(path, rev, rev_to) +# end +# +# # Default behaviour: we search in cached changesets +# def changesets_for_path(path) +# path = "/#{path}" unless path.starts_with?('/') +# Change.find(:all, :include => {:changeset => :user}, +# :conditions => ["repository_id = ? AND path = ?", id, path], +# :order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset) +# end +# +# # Returns a path relative to the url of the repository +# def relative_path(path) +# path +# end +# +# def latest_changeset +# @latest_changeset ||= changesets.find(:first) +# end +# +# def scan_changesets_for_issue_ids +# self.changesets.each(&:scan_comment_for_issue_ids) +# end +# +# # Returns an array of committers usernames and associated user_id +# def committers +# @committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}") +# end +# +# # Maps committers username to a user ids +# def committer_ids=(h) +# if h.is_a?(Hash) +# committers.each do |committer, user_id| +# new_user_id = h[committer] +# if new_user_id && (new_user_id.to_i != user_id.to_i) +# new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil) +# Changeset.update_all("user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", ["repository_id = ? AND committer = ?", id, committer]) +# end +# end +# @committers = nil +# true +# else +# false +# end +# end +# +# # Returns the Redmine User corresponding to the given +committer+ +# # It will return nil if the committer is not yet mapped and if no User +# # with the same username or email was found +# def find_committer_user(committer) +# if committer +# c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user) +# if c && c.user +# c.user +# elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/ +# username, email = $1.strip, $3 +# u = User.find_by_login(username) +# u ||= User.find_by_mail(email) unless email.blank? +# u +# end +# end +# end +# +# # fetch new changesets for all repositories +# # can be called periodically by an external script +# # eg. ruby script/runner "Repository.fetch_changesets" +# def self.fetch_changesets +# find(:all).each(&:fetch_changesets) +# end +# +# # scan changeset comments to find related and fixed issues for all repositories +# def self.scan_changesets_for_issue_ids +# find(:all).each(&:scan_changesets_for_issue_ids) +# end +# +# def self.scm_name +# 'Abstract' +# end +# +# def self.available_scm +# subclasses.collect {|klass| [klass.scm_name, klass.name]} +# end +# +# def self.factory(klass_name, *args) +# klass = "Repository::#{klass_name}".constantize +# klass.new(*args) +# rescue +# nil +# end +# +# private +# +# def before_save +# # Strips url and root_url +# url.strip! +# root_url.strip! +# true +# end +# +# def clear_changesets +# connection.delete("DELETE FROM changes WHERE changes.changeset_id IN (SELECT changesets.id FROM changesets WHERE changesets.repository_id = #{id})") +# connection.delete("DELETE FROM changesets_issues WHERE changesets_issues.changeset_id IN (SELECT changesets.id FROM changesets WHERE changesets.repository_id = #{id})") +# connection.delete("DELETE FROM changesets WHERE changesets.repository_id = #{id}") +# end +#end diff --git a/app/models/role.php b/app/models/role.php new file mode 100644 index 0000000..ce30cc1 --- /dev/null +++ b/app/models/role.php @@ -0,0 +1,148 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class Role < ActiveRecord::Base +# # Built-in roles +# BUILTIN_NON_MEMBER = 1 +# BUILTIN_ANONYMOUS = 2 +# +# named_scope :builtin, lambda { |*args| +# compare = 'not' if args.first == true +# { :conditions => "#{compare} builtin = 0" } +# } +# +# before_destroy :check_deletable +# has_many :workflows, :dependent => :delete_all do +# def copy(role) +# raise "Can not copy workflow from a #{role.class}" unless role.is_a?(Role) +# raise "Can not copy workflow from/to an unsaved role" if proxy_owner.new_record? || role.new_record? +# clear +# connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, old_status_id, new_status_id, role_id)" + +# " SELECT tracker_id, old_status_id, new_status_id, #{proxy_owner.id}" + +# " FROM #{Workflow.table_name}" + +# " WHERE role_id = #{role.id}" +# end +# end +# +# has_many :members +# acts_as_list +# +# serialize :permissions, Array +# attr_protected :builtin +# +# validates_presence_of :name +# validates_uniqueness_of :name +# validates_length_of :name, :maximum => 30 +# validates_format_of :name, :with => /^[\w\s\'\-]*$/i +# +# def permissions +# read_attribute(:permissions) || [] +# end +# +# def permissions=(perms) +# perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms +# write_attribute(:permissions, perms) +# end +# +# def add_permission!(*perms) +# self.permissions = [] unless permissions.is_a?(Array) +# +# permissions_will_change! +# perms.each do |p| +# p = p.to_sym +# permissions << p unless permissions.include?(p) +# end +# save! +# end +# +# def remove_permission!(*perms) +# return unless permissions.is_a?(Array) +# permissions_will_change! +# perms.each { |p| permissions.delete(p.to_sym) } +# save! +# end +# +# # Returns true if the role has the given permission +# def has_permission?(perm) +# !permissions.nil? && permissions.include?(perm.to_sym) +# end +# +# def <=>(role) +# position <=> role.position +# end +# +# # Return true if the role is a builtin role +# def builtin? +# self.builtin != 0 +# end +# +# # Return true if the role is a project member role +# def member? +# !self.builtin? +# end +# +# # Return true if role is allowed to do the specified action +# # action can be: +# # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') +# # * a permission Symbol (eg. :edit_project) +# def allowed_to?(action) +# if action.is_a? Hash +# allowed_actions.include? "#{action[:controller]}/#{action[:action]}" +# else +# allowed_permissions.include? action +# end +# end +# +# # Return all the permissions that can be given to the role +# def setable_permissions +# setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions +# setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER +# setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS +# setable_permissions +# end +# +# # Find all the roles that can be given to a project member +# def self.find_all_givable +# find(:all, :conditions => {:builtin => 0}, :order => 'position') +# end +# +# # Return the builtin 'non member' role +# def self.non_member +# find(:first, :conditions => {:builtin => BUILTIN_NON_MEMBER}) || raise('Missing non-member builtin role.') +# end +# +# # Return the builtin 'anonymous' role +# def self.anonymous +# find(:first, :conditions => {:builtin => BUILTIN_ANONYMOUS}) || raise('Missing anonymous builtin role.') +# end +# +# +#private +# def allowed_permissions +# @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name} +# end +# +# def allowed_actions +# @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten +# end +# +# def check_deletable +# raise "Can't delete role" if members.any? +# raise "Can't delete builtin role" if builtin? +# end +#end diff --git a/app/models/setting.php b/app/models/setting.php new file mode 100644 index 0000000..a3cb30e --- /dev/null +++ b/app/models/setting.php @@ -0,0 +1,165 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class Setting < ActiveRecord::Base +# +# DATE_FORMATS = [ +# '%Y-%m-%d', +# '%d/%m/%Y', +# '%d.%m.%Y', +# '%d-%m-%Y', +# '%m/%d/%Y', +# '%d %b %Y', +# '%d %B %Y', +# '%b %d, %Y', +# '%B %d, %Y' +# ] +# +# TIME_FORMATS = [ +# '%H:%M', +# '%I:%M %p' +# ] +# +# ENCODINGS = %w(US-ASCII +# windows-1250 +# windows-1251 +# windows-1252 +# windows-1253 +# windows-1254 +# windows-1255 +# windows-1256 +# windows-1257 +# windows-1258 +# windows-31j +# ISO-2022-JP +# ISO-2022-KR +# ISO-8859-1 +# ISO-8859-2 +# ISO-8859-3 +# ISO-8859-4 +# ISO-8859-5 +# ISO-8859-6 +# ISO-8859-7 +# ISO-8859-8 +# ISO-8859-9 +# ISO-8859-13 +# ISO-8859-15 +# KOI8-R +# UTF-8 +# UTF-16 +# UTF-16BE +# UTF-16LE +# EUC-JP +# Shift_JIS +# GB18030 +# GBK +# ISCII91 +# EUC-KR +# Big5 +# Big5-HKSCS +# TIS-620) +# +# cattr_accessor :available_settings +# @@available_settings = YAML::load(File.open("#{RAILS_ROOT}/config/settings.yml")) +# Redmine::Plugin.all.each do |plugin| +# next unless plugin.settings +# @@available_settings["plugin_#{plugin.id}"] = {'default' => plugin.settings[:default], 'serialized' => true} +# end +# +# validates_uniqueness_of :name +# validates_inclusion_of :name, :in => @@available_settings.keys +# validates_numericality_of :value, :only_integer => true, :if => Proc.new { |setting| @@available_settings[setting.name]['format'] == 'int' } +# +# # Hash used to cache setting values +# @cached_settings = {} +# @cached_cleared_on = Time.now +# +# def value +# v = read_attribute(:value) +# # Unserialize serialized settings +# v = YAML::load(v) if @@available_settings[name]['serialized'] && v.is_a?(String) +# v = v.to_sym if @@available_settings[name]['format'] == 'symbol' && !v.blank? +# v +# end +# +# def value=(v) +# v = v.to_yaml if v && @@available_settings[name]['serialized'] +# write_attribute(:value, v.to_s) +# end +# +# # Returns the value of the setting named name +# def self.[](name) +# v = @cached_settings[name] +# v ? v : (@cached_settings[name] = find_or_default(name).value) +# end +# +# def self.[]=(name, v) +# setting = find_or_default(name) +# setting.value = (v ? v : "") +# @cached_settings[name] = nil +# setting.save +# setting.value +# end +# +# # Defines getter and setter for each setting +# # Then setting values can be read using: Setting.some_setting_name +# # or set using Setting.some_setting_name = "some value" +# @@available_settings.each do |name, params| +# src = <<-END_SRC +# def self.#{name} +# self[:#{name}] +# end +# +# def self.#{name}? +# self[:#{name}].to_i > 0 +# end +# +# def self.#{name}=(value) +# self[:#{name}] = value +# end +# END_SRC +# class_eval src, __FILE__, __LINE__ +# end +# +# # Helper that returns an array based on per_page_options setting +# def self.per_page_options_array +# per_page_options.split(%r{[\s,]}).collect(&:to_i).select {|n| n > 0}.sort +# end +# +# # Checks if settings have changed since the values were read +# # and clears the cache hash if it's the case +# # Called once per request +# def self.check_cache +# settings_updated_on = Setting.maximum(:updated_on) +# if settings_updated_on && @cached_cleared_on <= settings_updated_on +# @cached_settings.clear +# @cached_cleared_on = Time.now +# logger.info "Settings cache cleared." if logger +# end +# end +# +#private +# # Returns the Setting instance for the setting named name +# # (record found in database or new record with default value) +# def self.find_or_default(name) +# name = name.to_s +# raise "There's no setting named #{name}" unless @@available_settings.has_key?(name) +# setting = find_by_name(name) +# setting ||= new(:name => name, :value => @@available_settings[name]['default']) if @@available_settings.has_key? name +# end +#end diff --git a/app/models/time_entry.php b/app/models/time_entry.php new file mode 100644 index 0000000..b22f396 --- /dev/null +++ b/app/models/time_entry.php @@ -0,0 +1,80 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2008 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class TimeEntry < ActiveRecord::Base +# # could have used polymorphic association +# # project association here allows easy loading of time entries at project level with one database trip +# belongs_to :project +# belongs_to :issue +# belongs_to :user +# belongs_to :activity, :class_name => 'Enumeration', :foreign_key => :activity_id +# +# attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek +# +# acts_as_customizable +# acts_as_event :title => Proc.new {|o| "#{o.user}: #{lwr(:label_f_hour, o.hours)} (#{(o.issue || o.project).event_title})"}, +# :url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project}}, +# :author => :user, +# :description => :comments +# +# validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on +# validates_numericality_of :hours, :allow_nil => true, :message => :activerecord_error_invalid +# validates_length_of :comments, :maximum => 255, :allow_nil => true +# +# def after_initialize +# if new_record? && self.activity.nil? +# if default_activity = Enumeration.default('ACTI') +# self.activity_id = default_activity.id +# end +# end +# end +# +# def before_validation +# self.project = issue.project if issue && project.nil? +# end +# +# def validate +# errors.add :hours, :activerecord_error_invalid if hours && (hours < 0 || hours >= 1000) +# errors.add :project_id, :activerecord_error_invalid if project.nil? +# errors.add :issue_id, :activerecord_error_invalid if (issue_id && !issue) || (issue && project!=issue.project) +# end +# +# def hours=(h) +# write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h) +# end +# +# # tyear, tmonth, tweek assigned where setting spent_on attributes +# # these attributes make time aggregations easier +# def spent_on=(date) +# super +# self.tyear = spent_on ? spent_on.year : nil +# self.tmonth = spent_on ? spent_on.month : nil +# self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil +# end +# +# # Returns true if the time entry can be edited by usr, otherwise false +# def editable_by?(usr) +# (usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project) +# end +# +# def self.visible_by(usr) +# with_scope(:find => { :conditions => Project.allowed_to_condition(usr, :view_time_entries) }) do +# yield +# end +# end +#end diff --git a/app/models/time_entry_custom_field.php b/app/models/time_entry_custom_field.php new file mode 100644 index 0000000..1c2fc86 --- /dev/null +++ b/app/models/time_entry_custom_field.php @@ -0,0 +1,24 @@ +<?php +## redMine - project management software +## Copyright (C) 2008 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 TimeEntryCustomField < CustomField +# def type_name +# :label_spent_time +# end +#end +# diff --git a/app/models/token.php b/app/models/token.php new file mode 100644 index 0000000..80107d2 --- /dev/null +++ b/app/models/token.php @@ -0,0 +1,45 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class Token < ActiveRecord::Base +# belongs_to :user +# +# @@validity_time = 1.day +# +# def before_create +# self.value = Token.generate_token_value +# end +# +# # Return true if token has expired +# def expired? +# return Time.now > self.created_on + @@validity_time +# end +# +# # Delete all expired tokens +# def self.destroy_expired +# Token.delete_all ["action <> 'feeds' AND created_on < ?", Time.now - @@validity_time] +# end +# +#private +# def self.generate_token_value +# chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a +# token_value = '' +# 40.times { |i| token_value << chars[rand(chars.size-1)] } +# token_value +# end +#end diff --git a/app/models/tracker.php b/app/models/tracker.php new file mode 100644 index 0000000..7d06141 --- /dev/null +++ b/app/models/tracker.php @@ -0,0 +1,57 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class Tracker < ActiveRecord::Base +# before_destroy :check_integrity +# has_many :issues +# has_many :workflows, :dependent => :delete_all do +# def copy(tracker) +# raise "Can not copy workflow from a #{tracker.class}" unless tracker.is_a?(Tracker) +# raise "Can not copy workflow from/to an unsaved tracker" if proxy_owner.new_record? || tracker.new_record? +# clear +# connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, old_status_id, new_status_id, role_id)" + +# " SELECT #{proxy_owner.id}, old_status_id, new_status_id, role_id" + +# " FROM #{Workflow.table_name}" + +# " WHERE tracker_id = #{tracker.id}" +# end +# end +# +# has_and_belongs_to_many :projects +# has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id' +# acts_as_list +# +# validates_presence_of :name +# validates_uniqueness_of :name +# validates_length_of :name, :maximum => 30 +# validates_format_of :name, :with => /^[\w\s\'\-]*$/i +# +# def to_s; name end +# +# def <=>(tracker) +# name <=> tracker.name +# end +# +# def self.all +# find(:all, :order => 'position') +# end +# +#private +# def check_integrity +# raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id]) +# end +#end diff --git a/app/models/user.php b/app/models/user.php new file mode 100644 index 0000000..4b8191e --- /dev/null +++ b/app/models/user.php @@ -0,0 +1,295 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#require "digest/sha1" +# +#class User < ActiveRecord::Base +# +# # Account statuses +# STATUS_ANONYMOUS = 0 +# STATUS_ACTIVE = 1 +# STATUS_REGISTERED = 2 +# STATUS_LOCKED = 3 +# +# USER_FORMATS = { +# :firstname_lastname => '#{firstname} #{lastname}', +# :firstname => '#{firstname}', +# :lastname_firstname => '#{lastname} #{firstname}', +# :lastname_coma_firstname => '#{lastname}, #{firstname}', +# :username => '#{login}' +# } +# +# has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name" +# has_many :members, :dependent => :delete_all +# has_many :projects, :through => :memberships +# has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify +# has_many :changesets, :dependent => :nullify +# has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' +# has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'" +# belongs_to :auth_source +# +# # Active non-anonymous users scope +# named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}" +# +# acts_as_customizable +# +# attr_accessor :password, :password_confirmation +# attr_accessor :last_before_login_on +# # Prevents unauthorized assignments +# attr_protected :login, :admin, :password, :password_confirmation, :hashed_password +# +# validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } +# validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? } +# validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? } +# # Login must contain lettres, numbers, underscores only +# validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i +# validates_length_of :login, :maximum => 30 +# validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i +# validates_length_of :firstname, :lastname, :maximum => 30 +# validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true +# validates_length_of :mail, :maximum => 60, :allow_nil => true +# validates_length_of :password, :minimum => 4, :allow_nil => true +# validates_confirmation_of :password, :allow_nil => true +# +# def before_create +# self.mail_notification = false +# true +# end +# +# def before_save +# # update hashed_password if password was set +# self.hashed_password = User.hash_password(self.password) if self.password +# end +# +# def reload(*args) +# @name = nil +# super +# end +# +# # Returns the user that matches provided login and password, or nil +# def self.try_to_login(login, password) +# # Make sure no one can sign in with an empty password +# return nil if password.to_s.empty? +# user = find(:first, :conditions => ["login=?", login]) +# if user +# # user is already in local database +# return nil if !user.active? +# if user.auth_source +# # user has an external authentication method +# return nil unless user.auth_source.authenticate(login, password) +# else +# # authentication with local password +# return nil unless User.hash_password(password) == user.hashed_password +# end +# else +# # user is not yet registered, try to authenticate with available sources +# attrs = AuthSource.authenticate(login, password) +# if attrs +# user = new(*attrs) +# user.login = login +# user.language = Setting.default_language +# if user.save +# user.reload +# logger.info("User '#{user.login}' created from the LDAP") if logger +# end +# end +# end +# user.update_attribute(:last_login_on, Time.now) if user && !user.new_record? +# user +# rescue => text +# raise text +# end +# +# # Return user's full name for display +# def name(formatter = nil) +# if formatter +# eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"') +# else +# @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"') +# end +# end +# +# def active? +# self.status == STATUS_ACTIVE +# end +# +# def registered? +# self.status == STATUS_REGISTERED +# end +# +# def locked? +# self.status == STATUS_LOCKED +# end +# +# def check_password?(clear_password) +# User.hash_password(clear_password) == self.hashed_password +# end +# +# def pref +# self.preference ||= UserPreference.new(:user => self) +# end +# +# def time_zone +# @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone]) +# end +# +# def wants_comments_in_reverse_order? +# self.pref[:comments_sorting] == 'desc' +# end +# +# # Return user's RSS key (a 40 chars long string), used to access feeds +# def rss_key +# token = self.rss_token || Token.create(:user => self, :action => 'feeds') +# token.value +# end +# +# # Return an array of project ids for which the user has explicitly turned mail notifications on +# def notified_projects_ids +# @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id) +# end +# +# def notified_project_ids=(ids) +# Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id]) +# Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty? +# @notified_projects_ids = nil +# notified_projects_ids +# end +# +# def self.find_by_rss_key(key) +# token = Token.find_by_value(key) +# token && token.user.active? ? token.user : nil +# end +# +# def self.find_by_autologin_key(key) +# token = Token.find_by_action_and_value('autologin', key) +# token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil +# end +# +# # Makes find_by_mail case-insensitive +# def self.find_by_mail(mail) +# find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase]) +# end +# +# # Sort users by their display names +# def <=>(user) +# self.to_s.downcase <=> user.to_s.downcase +# end +# +# def to_s +# name +# end +# +# def logged? +# true +# end +# +# def anonymous? +# !logged? +# end +# +# # Return user's role for project +# def role_for_project(project) +# # No role on archived projects +# return nil unless project && project.active? +# if logged? +# # Find project membership +# membership = memberships.detect {|m| m.project_id == project.id} +# if membership +# membership.role +# else +# @role_non_member ||= Role.non_member +# end +# else +# @role_anonymous ||= Role.anonymous +# end +# end +# +# # Return true if the user is a member of project +# def member_of?(project) +# role_for_project(project).member? +# end +# +# # Return true if the user is allowed to do the specified action on project +# # action can be: +# # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') +# # * a permission Symbol (eg. :edit_project) +# def allowed_to?(action, project, options={}) +# if project +# # No action allowed on archived projects +# return false unless project.active? +# # No action allowed on disabled modules +# return false unless project.allows_to?(action) +# # Admin users are authorized for anything else +# return true if admin? +# +# role = role_for_project(project) +# return false unless role +# role.allowed_to?(action) && (project.is_public? || role.member?) +# +# elsif options[:global] +# # authorize if user has at least one role that has this permission +# roles = memberships.collect {|m| m.role}.uniq +# roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action)) +# else +# false +# end +# end +# +# def self.current=(user) +# @current_user = user +# end +# +# def self.current +# @current_user ||= User.anonymous +# end +# +# def self.anonymous +# anonymous_user = AnonymousUser.find(:first) +# if anonymous_user.nil? +# anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0) +# raise 'Unable to create the anonymous user.' if anonymous_user.new_record? +# end +# anonymous_user +# end +# +#private +# # Return password digest +# def self.hash_password(clear_password) +# Digest::SHA1.hexdigest(clear_password || "") +# end +#end +# +#class AnonymousUser < User +# +# def validate_on_create +# # There should be only one AnonymousUser in the database +# errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first) +# end +# +# def available_custom_fields +# [] +# end +# +# # Overrides a few properties +# def logged?; false end +# def admin; false end +# def name; 'Anonymous' end +# def mail; nil end +# def time_zone; nil end +# def rss_key; nil end +#end diff --git a/app/models/user_custom_field.php b/app/models/user_custom_field.php new file mode 100644 index 0000000..9b7ca16 --- /dev/null +++ b/app/models/user_custom_field.php @@ -0,0 +1,24 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class UserCustomField < CustomField +# def type_name +# :label_user_plural +# end +#end +# diff --git a/app/models/user_preference.php b/app/models/user_preference.php new file mode 100644 index 0000000..b0bd659 --- /dev/null +++ b/app/models/user_preference.php @@ -0,0 +1,55 @@ +<?php +## redMine - project management software +## Copyright (C) 2006 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class UserPreference < ActiveRecord::Base +# belongs_to :user +# serialize :others +# +# attr_protected :others +# +# def initialize(attributes = nil) +# super +# self.others ||= {} +# end +# +# def before_save +# self.others ||= {} +# end +# +# def [](attr_name) +# if attribute_present? attr_name +# super +# else +# others ? others[attr_name] : nil +# end +# end +# +# def []=(attr_name, value) +# if attribute_present? attr_name +# super +# else +# h = read_attribute(:others).dup || {} +# h.update(attr_name => value) +# write_attribute(:others, h) +# value +# end +# end +# +# def comments_sorting; self[:comments_sorting] end +# def comments_sorting=(order); self[:comments_sorting]=order end +#end diff --git a/app/models/version.php b/app/models/version.php new file mode 100644 index 0000000..0a0d281 --- /dev/null +++ b/app/models/version.php @@ -0,0 +1,108 @@ +<?php +## 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' +# acts_as_attachable :view_permission => :view_files, +# :delete_permission => :manage_files +# +# validates_presence_of :name +# validates_uniqueness_of :name, :scope => [:project_id] +# validates_length_of :name, :maximum => 60 +# validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => 'activerecord_error_not_a_date', :allow_nil => true +# +# def start_date +# effective_date +# end +# +# def due_date +# effective_date +# end +# +# # Returns the total estimated time for this version +# def estimated_hours +# @estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f +# end +# +# # Returns the total reported time for this version +# def spent_hours +# @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f +# end +# +# # Returns true if the version is completed: due date reached and no open issues +# def completed? +# effective_date && (effective_date <= Date.today) && (open_issues_count == 0) +# end +# +# def completed_pourcent +# if fixed_issues.count == 0 +# 0 +# elsif open_issues_count == 0 +# 100 +# else +# (closed_issues_count * 100 + Issue.sum('done_ratio', :include => 'status', :conditions => ["fixed_version_id = ? AND is_closed = ?", id, false]).to_f) / fixed_issues.count +# end +# end +# +# def closed_pourcent +# if fixed_issues.count == 0 +# 0 +# else +# closed_issues_count * 100.0 / fixed_issues.count +# end +# end +# +# # Returns true if the version is overdue: due date reached and some open issues +# def overdue? +# effective_date && (effective_date < Date.today) && (open_issues_count > 0) +# end +# +# def open_issues_count +# @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status) +# end +# +# def closed_issues_count +# @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status) +# end +# +# def wiki_page +# if project.wiki && !wiki_page_title.blank? +# @wiki_page ||= project.wiki.find_page(wiki_page_title) +# end +# @wiki_page +# end +# +# def to_s; name end +# +# # Versions are sorted by effective_date and name +# # Those with no effective_date are at the end, sorted by name +# def <=>(version) +# if self.effective_date +# version.effective_date ? (self.effective_date == version.effective_date ? self.name <=> version.name : self.effective_date <=> version.effective_date) : -1 +# else +# version.effective_date ? 1 : (self.name <=> version.name) +# end +# end +# +#private +# def check_integrity +# raise "Can't delete version" if self.fixed_issues.find(:first) +# end +#end diff --git a/app/models/watcher.php b/app/models/watcher.php new file mode 100644 index 0000000..32a2b7b --- /dev/null +++ b/app/models/watcher.php @@ -0,0 +1,31 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class Watcher < ActiveRecord::Base +# belongs_to :watchable, :polymorphic => true +# belongs_to :user +# +# validates_presence_of :user +# validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id] +# +# protected +# +# def validate +# errors.add :user_id, :activerecord_error_invalid unless user.nil? || user.active? +# end +#end diff --git a/app/models/wiki.php b/app/models/wiki.php new file mode 100644 index 0000000..0730e4a --- /dev/null +++ b/app/models/wiki.php @@ -0,0 +1,74 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class Wiki < ActiveRecord::Base +# belongs_to :project +# has_many :pages, :class_name => 'WikiPage', :dependent => :destroy, :order => 'title' +# has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all +# +# validates_presence_of :start_page +# validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/ +# +# # find the page with the given title +# # if page doesn't exist, return a new page +# def find_or_new_page(title) +# title = start_page if title.blank? +# find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title)) +# end +# +# # find the page with the given title +# def find_page(title, options = {}) +# title = start_page if title.blank? +# title = Wiki.titleize(title) +# page = pages.find_by_title(title) +# if !page && !(options[:with_redirect] == false) +# # search for a redirect +# redirect = redirects.find_by_title(title) +# page = find_page(redirect.redirects_to, :with_redirect => false) if redirect +# end +# page +# end +# +# # Finds a page by title +# # The given string can be of one of the forms: "title" or "project:title" +# # Examples: +# # Wiki.find_page("bar", project => foo) +# # Wiki.find_page("foo:bar") +# def self.find_page(title, options = {}) +# project = options[:project] +# if title.to_s =~ %r{^([^\:]+)\:(.*)$} +# project_identifier, title = $1, $2 +# project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier) +# end +# if project && project.wiki +# page = project.wiki.find_page(title) +# if page && page.content +# page +# end +# end +# end +# +# # turn a string into a valid page title +# def self.titleize(title) +# # replace spaces with _ and remove unwanted caracters +# title = title.gsub(/\s+/, '_').delete(',./?;|:') if title +# # upcase the first letter +# title = (title.slice(0..0).upcase + (title.slice(1..-1) || '')) if title +# title +# end +#end diff --git a/app/models/wiki_content.php b/app/models/wiki_content.php new file mode 100644 index 0000000..72577b5 --- /dev/null +++ b/app/models/wiki_content.php @@ -0,0 +1,91 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#require 'zlib' +# +#class WikiContent < ActiveRecord::Base +# set_locking_column :version +# belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id' +# belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' +# validates_presence_of :text +# +# acts_as_versioned +# class Version +# belongs_to :page, :class_name => '::WikiPage', :foreign_key => 'page_id' +# belongs_to :author, :class_name => '::User', :foreign_key => 'author_id' +# attr_protected :data +# +# acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"}, +# :description => :comments, +# :datetime => :updated_on, +# :type => 'wiki-page', +# :url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}} +# +# acts_as_activity_provider :type => 'wiki_edits', +# :timestamp => "#{WikiContent.versioned_table_name}.updated_on", +# :author_key => "#{WikiContent.versioned_table_name}.author_id", +# :permission => :view_wiki_edits, +# :find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " + +# "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " + +# "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " + +# "#{WikiContent.versioned_table_name}.id", +# :joins => "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " + +# "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " + +# "LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"} +# +# def text=(plain) +# case Setting.wiki_compression +# when 'gzip' +# begin +# self.data = Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION) +# self.compression = 'gzip' +# rescue +# self.data = plain +# self.compression = '' +# end +# else +# self.data = plain +# self.compression = '' +# end +# plain +# end +# +# def text +# @text ||= case compression +# when 'gzip' +# Zlib::Inflate.inflate(data) +# else +# # uncompressed data +# data +# end +# end +# +# def project +# page.project +# end +# +# # Returns the previous version or nil +# def previous +# @previous ||= WikiContent::Version.find(:first, +# :order => 'version DESC', +# :include => :author, +# :conditions => ["wiki_content_id = ? AND version < ?", wiki_content_id, version]) +# end +# end +# +#end diff --git a/app/models/wiki_page.php b/app/models/wiki_page.php new file mode 100644 index 0000000..7e9b012 --- /dev/null +++ b/app/models/wiki_page.php @@ -0,0 +1,189 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#require 'diff' +#require 'enumerator' +# +#class WikiPage < ActiveRecord::Base +# belongs_to :wiki +# has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy +# acts_as_attachable :delete_permission => :delete_wiki_pages_attachments +# acts_as_tree :order => 'title' +# +# acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"}, +# :description => :text, +# :datetime => :created_on, +# :url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}} +# +# acts_as_searchable :columns => ['title', 'text'], +# :include => [{:wiki => :project}, :content], +# :project_key => "#{Wiki.table_name}.project_id" +# +# attr_accessor :redirect_existing_links +# +# validates_presence_of :title +# validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/ +# validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false +# validates_associated :content +# +# def title=(value) +# value = Wiki.titleize(value) +# @previous_title = read_attribute(:title) if @previous_title.blank? +# write_attribute(:title, value) +# end +# +# def before_save +# self.title = Wiki.titleize(title) +# # Manage redirects if the title has changed +# if !@previous_title.blank? && (@previous_title != title) && !new_record? +# # Update redirects that point to the old title +# wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r| +# r.redirects_to = title +# r.title == r.redirects_to ? r.destroy : r.save +# end +# # Remove redirects for the new title +# wiki.redirects.find_all_by_title(title).each(&:destroy) +# # Create a redirect to the new title +# wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0" +# @previous_title = nil +# end +# end +# +# def before_destroy +# # Remove redirects to this page +# wiki.redirects.find_all_by_redirects_to(title).each(&:destroy) +# end +# +# def pretty_title +# WikiPage.pretty_title(title) +# end +# +# def content_for_version(version=nil) +# result = content.versions.find_by_version(version.to_i) if version +# result ||= content +# result +# end +# +# def diff(version_to=nil, version_from=nil) +# version_to = version_to ? version_to.to_i : self.content.version +# version_from = version_from ? version_from.to_i : version_to - 1 +# version_to, version_from = version_from, version_to unless version_from < version_to +# +# content_to = content.versions.find_by_version(version_to) +# content_from = content.versions.find_by_version(version_from) +# +# (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil +# end +# +# def annotate(version=nil) +# version = version ? version.to_i : self.content.version +# c = content.versions.find_by_version(version) +# c ? WikiAnnotate.new(c) : nil +# end +# +# def self.pretty_title(str) +# (str && str.is_a?(String)) ? str.tr('_', ' ') : str +# end +# +# def project +# wiki.project +# end +# +# def text +# content.text if content +# end +# +# # Returns true if usr is allowed to edit the page, otherwise false +# def editable_by?(usr) +# !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project) +# end +# +# def attachments_deletable?(usr=User.current) +# editable_by?(usr) && super(usr) +# end +# +# def parent_title +# @parent_title || (self.parent && self.parent.pretty_title) +# end +# +# def parent_title=(t) +# @parent_title = t +# parent_page = t.blank? ? nil : self.wiki.find_page(t) +# self.parent = parent_page +# end +# +# protected +# +# def validate +# errors.add(:parent_title, :activerecord_error_invalid) if !@parent_title.blank? && parent.nil? +# errors.add(:parent_title, :activerecord_error_circular_dependency) if parent && (parent == self || parent.ancestors.include?(self)) +# errors.add(:parent_title, :activerecord_error_not_same_project) if parent && (parent.wiki_id != wiki_id) +# end +#end +# +#class WikiDiff +# attr_reader :diff, :words, :content_to, :content_from +# +# def initialize(content_to, content_from) +# @content_to = content_to +# @content_from = content_from +# @words = content_to.text.split(/(\s+)/) +# @words = @words.select {|word| word != ' '} +# words_from = content_from.text.split(/(\s+)/) +# words_from = words_from.select {|word| word != ' '} +# @diff = words_from.diff @words +# end +#end +# +#class WikiAnnotate +# attr_reader :lines, :content +# +# def initialize(content) +# @content = content +# current = content +# current_lines = current.text.split(/\r?\n/) +# @lines = current_lines.collect {|t| [nil, nil, t]} +# positions = [] +# current_lines.size.times {|i| positions << i} +# while (current.previous) +# d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten +# d.each_slice(3) do |s| +# sign, line = s[0], s[1] +# if sign == '+' && positions[line] && positions[line] != -1 +# if @lines[positions[line]][0].nil? +# @lines[positions[line]][0] = current.version +# @lines[positions[line]][1] = current.author +# end +# end +# end +# d.each_slice(3) do |s| +# sign, line = s[0], s[1] +# if sign == '-' +# positions.insert(line, -1) +# else +# positions[line] = nil +# end +# end +# positions.compact! +# # Stop if every line is annotated +# break unless @lines.detect { |line| line[0].nil? } +# current = current.previous +# end +# @lines.each { |line| line[0] ||= current.version } +# end +#end diff --git a/app/models/wiki_redirect.php b/app/models/wiki_redirect.php new file mode 100644 index 0000000..e992c10 --- /dev/null +++ b/app/models/wiki_redirect.php @@ -0,0 +1,24 @@ +<?php +## redMine - project management software +## Copyright (C) 2006-2007 Jean-Philippe Lang +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version 2 +## of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#class WikiRedirect < ActiveRecord::Base +# belongs_to :wiki +# +# validates_presence_of :title, :redirects_to +# validates_length_of :title, :redirects_to, :maximum => 255 +#end diff --git a/app/models/workflow.php b/app/models/workflow.php new file mode 100644 index 0000000..e763433 --- /dev/null +++ b/app/models/workflow.php @@ -0,0 +1,44 @@ +<?php +## 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 +# +# # Returns workflow transitions count by tracker and role +# def self.count_by_tracker_and_role +# counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{Workflow.table_name} GROUP BY role_id, tracker_id") +# roles = Role.find(:all, :order => 'builtin, position') +# trackers = Tracker.find(:all, :order => 'position') +# +# result = [] +# trackers.each do |tracker| +# t = [] +# roles.each do |role| +# row = counts.detect {|c| c['role_id'] == role.id.to_s && c['tracker_id'] == tracker.id.to_s} +# t << [role, (row.nil? ? 0 : row['c'].to_i)] +# end +# result << [tracker, t] +# end +# +# result +# end +#end diff --git a/app/plugins/empty b/app/plugins/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/tests/cases/behaviors/empty b/app/tests/cases/behaviors/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/tests/cases/components/empty b/app/tests/cases/components/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/tests/cases/controllers/empty b/app/tests/cases/controllers/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/tests/cases/helpers/empty b/app/tests/cases/helpers/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/tests/cases/models/empty b/app/tests/cases/models/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/tests/fixtures/empty b/app/tests/fixtures/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/tests/groups/empty b/app/tests/groups/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/tmp/cache/models/empty b/app/tmp/cache/models/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/tmp/cache/persistent/empty b/app/tmp/cache/persistent/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/tmp/cache/views/empty b/app/tmp/cache/views/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/tmp/logs/empty b/app/tmp/logs/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/tmp/sessions/empty b/app/tmp/sessions/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/tmp/tests/empty b/app/tmp/tests/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/vendors/shells/tasks/empty b/app/vendors/shells/tasks/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/vendors/shells/templates/empty b/app/vendors/shells/templates/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/views/account/login.ctp b/app/views/account/login.ctp new file mode 100644 index 0000000..d8c1f31 --- /dev/null +++ b/app/views/account/login.ctp @@ -0,0 +1,34 @@ +<div id="login-form"> +<% form_tag({:action=> "login"}) do %> +<%= back_url_hidden_field_tag %> +<table> +<tr> + <td align="right"><label for="username"><%=l(:field_login)%>:</label></td> + <td align="left"><p><%= text_field_tag 'username', nil, :size => 40 %></p></td> +</tr> +<tr> + <td align="right"><label for="password"><%=l(:field_password)%>:</label></td> + <td align="left"><%= password_field_tag 'password', nil, :size => 40 %></td> +</tr> +<tr> + <td></td> + <td align="left"> + <% if Setting.autologin? %> + <label for="autologin"><%= check_box_tag 'autologin' %> <%= l(:label_stay_logged_in) %></label> + <% end %> + </td> +</tr> +<tr> + <td align="left"> + <% if Setting.lost_password? %> + <%= link_to l(:label_password_lost), :controller => 'account', :action => 'lost_password' %> + <% end %> + </td> + <td align="right"> + <input type="submit" name="login" value="<%=l(:button_login)%> &#187;" /> + </td> +</tr> +</table> +<%= javascript_tag "Form.Element.focus('username');" %> +<% end %> +</div> diff --git a/app/views/account/lost_password.ctp b/app/views/account/lost_password.ctp new file mode 100644 index 0000000..420e8f9 --- /dev/null +++ b/app/views/account/lost_password.ctp @@ -0,0 +1,11 @@ +<h2><%=l(:label_password_lost)%></h2> + +<div class="box"> +<% form_tag({:action=> "lost_password"}, :class => "tabular") do %> + +<p><label for="mail"><%=l(:field_mail)%> <span class="required">*</span></label> +<%= text_field_tag 'mail', nil, :size => 40 %> +<%= submit_tag l(:button_submit) %></p> + +<% end %> +</div> diff --git a/app/views/account/password_recovery.ctp b/app/views/account/password_recovery.ctp new file mode 100644 index 0000000..7fdd2b2 --- /dev/null +++ b/app/views/account/password_recovery.ctp @@ -0,0 +1,15 @@ +<h2><%=l(:label_password_lost)%></h2> + +<%= error_messages_for 'user' %> + +<% form_tag({:token => @token.value}) do %> +<div class="box tabular"> +<p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label> +<%= password_field_tag 'new_password', nil, :size => 25 %><br /> +<em><%= l(:text_caracters_minimum, 4) %></em></p> + +<p><label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label> +<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %></p> +</div> +<p><%= submit_tag l(:button_save) %></p> +<% end %> diff --git a/app/views/account/register.ctp b/app/views/account/register.ctp new file mode 100644 index 0000000..755a7ad --- /dev/null +++ b/app/views/account/register.ctp @@ -0,0 +1,39 @@ +<h2><%=l(:label_register)%></h2> + +<% form_tag({:action => 'register'}, :class => "tabular") do %> +<%= error_messages_for 'user' %> + +<div class="box"> +<!--[form:user]--> +<% if @user.auth_source_id.nil? %> +<p><label for="user_login"><%=l(:field_login)%> <span class="required">*</span></label> +<%= text_field 'user', 'login', :size => 25 %></p> + +<p><label for="password"><%=l(:field_password)%> <span class="required">*</span></label> +<%= password_field_tag 'password', nil, :size => 25 %><br /> +<em><%= l(:text_caracters_minimum, 4) %></em></p> + +<p><label for="password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label> +<%= password_field_tag 'password_confirmation', nil, :size => 25 %></p> +<% end %> + +<p><label for="user_firstname"><%=l(:field_firstname)%> <span class="required">*</span></label> +<%= text_field 'user', 'firstname' %></p> + +<p><label for="user_lastname"><%=l(:field_lastname)%> <span class="required">*</span></label> +<%= text_field 'user', 'lastname' %></p> + +<p><label for="user_mail"><%=l(:field_mail)%> <span class="required">*</span></label> +<%= text_field 'user', 'mail' %></p> + +<p><label for="user_language"><%=l(:field_language)%></label> +<%= select("user", "language", lang_options_for_select) %></p> + +<% @user.custom_field_values.each do |value| %> + <p><%= custom_field_tag_with_label :user, value %></p> +<% end %> +<!--[eoform:user]--> +</div> + +<%= submit_tag l(:button_submit) %> +<% end %> diff --git a/app/views/account/show.ctp b/app/views/account/show.ctp new file mode 100644 index 0000000..1ddabba --- /dev/null +++ b/app/views/account/show.ctp @@ -0,0 +1,69 @@ +<div class="contextual"> +<%= link_to(l(:button_edit), {:controller => 'users', :action => 'edit', :id => @user}, :class => 'icon icon-edit') if User.current.admin? %> +</div> + +<h2><%= avatar @user %> <%=h @user.name %></h2> + +<div class="splitcontentleft"> +<ul> + <% unless @user.pref.hide_mail %> + <li><%=l(:field_mail)%>: <%= mail_to(h(@user.mail), nil, :encode => 'javascript') %></li> + <% end %> + <% for custom_value in @custom_values %> + <% if !custom_value.value.empty? %> + <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li> + <% end %> + <% end %> + <li><%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %></li> + <% unless @user.last_login_on.nil? %> + <li><%=l(:field_last_login_on)%>: <%= format_date(@user.last_login_on) %></li> + <% end %> +</ul> + +<% unless @memberships.empty? %> +<h3><%=l(:label_project_plural)%></h3> +<ul> +<% for membership in @memberships %> + <li><%= link_to(h(membership.project.name), :controller => 'projects', :action => 'show', :id => membership.project) %> + (<%=h membership.role.name %>, <%= format_date(membership.created_on) %>)</li> +<% end %> +</ul> +<% end %> +</div> + +<div class="splitcontentright"> + +<% unless @events_by_day.empty? %> +<h3><%= link_to l(:label_activity), :controller => 'projects', :action => 'activity', :user_id => @user, :from => @events_by_day.keys.first %></h3> + +<p> +<%=l(:label_reported_issues)%>: <%= Issue.count(:conditions => ["author_id=?", @user.id]) %> +</p> + +<div id="activity"> +<% @events_by_day.keys.sort.reverse.each do |day| %> +<h4><%= format_activity_day(day) %></h4> +<dl> +<% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%> + <dt class="<%= e.event_type %>"> + <span class="time"><%= format_time(e.event_datetime, false) %></span> + <%= content_tag('span', h(e.project), :class => 'project') %> + <%= link_to format_activity_title(e.event_title), e.event_url %></dt> + <dd><span class="description"><%= format_activity_description(e.event_description) %></span></dd> +<% end -%> +</dl> +<% end -%> +</div> + +<p class="other-formats"> + <%= l(:label_export_to) %> + <%= link_to 'Atom', {:controller => 'projects', :action => 'activity', :user_id => @user, :format => :atom, :key => User.current.rss_key}, :class => 'feed' %> +</p> + +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :user_id => @user, :format => :atom, :key => User.current.rss_key) %> +<% end %> +<% end %> +</div> + +<% html_title @user.name %> diff --git a/app/views/admin/_menu.ctp b/app/views/admin/_menu.ctp new file mode 100644 index 0000000..ef2abbc --- /dev/null +++ b/app/views/admin/_menu.ctp @@ -0,0 +1,25 @@ +<div id="menuAdmin" class="menu" onmouseover="menuMouseover(event)"> + <a class="menuItem" href="#" onmouseover="menuItemMouseover(event,'menuProjects');" onclick="this.blur(); return false;"><span class="menuItemText"><%=l(:label_project_plural)%></span><span class="menuItemArrow">&#9654;</span></a> + <a class="menuItem" href="#" onmouseover="menuItemMouseover(event,'menuUsers');" onclick="this.blur(); return false;"><span class="menuItemText"><%=l(:label_user_plural)%></span><span class="menuItemArrow">&#9654;</span></a> + <%= link_to l(:label_role_and_permissions), {:controller => 'roles' }, :class => "menuItem" %> + <a class="menuItem" href="#" onmouseover="menuItemMouseover(event,'menuTrackers');" onclick="this.blur(); return false;"><span class="menuItemText"><%=l(:label_issue_tracking)%></span><span class="menuItemArrow">&#9654;</span></a> + <%= link_to l(:label_custom_field_plural), {:controller => 'custom_fields' }, :class => "menuItem" %> + <%= link_to l(:label_enumerations), {:controller => 'enumerations' }, :class => "menuItem" %> + <%= link_to l(:field_mail_notification), {:controller => 'admin', :action => 'mail_options' }, :class => "menuItem" %> + <%= link_to l(:label_authentication), {:controller => 'auth_sources' }, :class => "menuItem" %> + <%= link_to l(:label_settings), {:controller => 'settings' }, :class => "menuItem" %> + <%= link_to l(:label_information_plural), {:controller => 'admin', :action => 'info' }, :class => "menuItem" %> +</div> +<div id="menuTrackers" class="menu"> + <%= link_to l(:label_tracker_plural), {:controller => 'trackers' }, :class => "menuItem" %> + <%= link_to l(:label_issue_status_plural), {:controller => 'issue_statuses' }, :class => "menuItem" %> + <%= link_to l(:label_workflow), {:controller => 'roles', :action => 'workflow' }, :class => "menuItem" %> +</div> +<div id="menuProjects" class="menu"> + <%= link_to l(:button_list), {:controller => 'admin', :action => 'projects' }, :class => "menuItem" %> + <%= link_to l(:label_new), {:controller => 'projects', :action => 'add' }, :class => "menuItem" %> +</div> +<div id="menuUsers" class="menu"> + <%= link_to l(:button_list), {:controller => 'users' }, :class => "menuItem" %> + <%= link_to l(:label_new), {:controller => 'users', :action => 'add' }, :class => "menuItem" %> +</div> diff --git a/app/views/admin/_no_data.ctp b/app/views/admin/_no_data.ctp new file mode 100644 index 0000000..5d52dc0 --- /dev/null +++ b/app/views/admin/_no_data.ctp @@ -0,0 +1,8 @@ +<div class="nodata"> +<% form_tag({:action => 'default_configuration'}) do %> + <%= simple_format(l(:text_no_configuration_data)) %> + <p><%= l(:field_language) %>: + <%= select_tag 'lang', options_for_select(lang_options_for_select(false), current_language.to_s) %> + <%= submit_tag l(:text_load_default_configuration) %></p> +<% end %> +</div> diff --git a/app/views/admin/index.ctp b/app/views/admin/index.ctp new file mode 100644 index 0000000..3236417 --- /dev/null +++ b/app/views/admin/index.ctp @@ -0,0 +1,51 @@ +<h2><%=l(:label_administration)%></h2> + +<%= render :partial => 'no_data' if @no_configuration_data %> + +<p class="icon22 icon22-projects"> +<%= link_to l(:label_project_plural), :controller => 'admin', :action => 'projects' %> | +<%= link_to l(:label_new), :controller => 'projects', :action => 'add' %> +</p> + +<p class="icon22 icon22-users"> +<%= link_to l(:label_user_plural), :controller => 'users' %> | +<%= link_to l(:label_new), :controller => 'users', :action => 'add' %> +</p> + +<p class="icon22 icon22-role"> +<%= link_to l(:label_role_and_permissions), :controller => 'roles' %> +</p> + +<p class="icon22 icon22-tracker"> +<%= link_to l(:label_tracker_plural), :controller => 'trackers' %> | +<%= link_to l(:label_issue_status_plural), :controller => 'issue_statuses' %> | +<%= link_to l(:label_workflow), :controller => 'workflows', :action => 'edit' %> +</p> + +<p class="icon22 icon22-workflow"> +<%= link_to l(:label_custom_field_plural), :controller => 'custom_fields' %> +</p> + +<p class="icon22 icon22-options"> +<%= link_to l(:label_enumerations), :controller => 'enumerations' %> +</p> + +<p class="icon22 icon22-settings"> +<%= link_to l(:label_settings), :controller => 'settings' %> +</p> + +<% menu_items_for(:admin_menu) do |item, caption, url, selected| -%> + <%= content_tag 'p', + link_to(h(caption), item.url, item.html_options), + :class => ["icon22", "icon22-#{item.name}"].join(' ') %> +<% end -%> + +<p class="icon22 icon22-plugin"> +<%= link_to l(:label_plugins), :controller => 'admin', :action => 'plugins' %> +</p> + +<p class="icon22 icon22-info"> +<%= link_to l(:label_information_plural), :controller => 'admin', :action => 'info' %> +</p> + +<% html_title(l(:label_administration)) -%> diff --git a/app/views/admin/info.ctp b/app/views/admin/info.ctp new file mode 100644 index 0000000..8c126b5 --- /dev/null +++ b/app/views/admin/info.ctp @@ -0,0 +1,12 @@ +<h2><%=l(:label_information_plural)%></h2> + +<p><strong><%= Redmine::Info.versioned_name %></strong> (<%= @db_adapter_name %>)</p> + +<table class="list"> +<tr class="odd"><td><%= l(:text_default_administrator_account_changed) %></td><td><%= image_tag (@flags[:default_admin_changed] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr> +<tr class="even"><td><%= l(:text_file_repository_writable) %> (<%= Attachment.storage_path %>)</td><td><%= image_tag (@flags[:file_repository_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr> +<tr class="even"><td><%= l(:text_plugin_assets_writable) %> (<%= Engines.public_directory %>)</td><td><%= image_tag (@flags[:plugin_assets_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr> +<tr class="odd"><td><%= l(:text_rmagick_available) %></td><td><%= image_tag (@flags[:rmagick_available] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr> +</table> + +<% html_title(l(:label_information_plural)) -%> diff --git a/app/views/admin/plugins.ctp b/app/views/admin/plugins.ctp new file mode 100644 index 0000000..4ee6c14 --- /dev/null +++ b/app/views/admin/plugins.ctp @@ -0,0 +1,19 @@ +<h2><%= l(:label_plugins) %></h2> + +<% if @plugins.any? %> +<table class="list plugins"> + <% @plugins.each do |plugin| %> + <tr class="<%= cycle('odd', 'even') %>"> + <td><span class="name"><%=h plugin.name %></span> + <%= content_tag('span', h(plugin.description), :class => 'description') unless plugin.description.blank? %> + <%= content_tag('span', link_to(h(plugin.url), plugin.url), :class => 'url') unless plugin.url.blank? %> + </td> + <td class="author"><%= plugin.author_url.blank? ? h(plugin.author) : link_to(h(plugin.author), plugin.author_url) %></td> + <td class="version"><%=h plugin.version %></td> + <td class="configure"><%= link_to(l(:button_configure), :controller => 'settings', :action => 'plugin', :id => plugin.id) if plugin.configurable? %></td> + </tr> + <% end %> +</table> +<% else %> +<p class="nodata"><%= l(:label_no_data) %></p> +<% end %> diff --git a/app/views/admin/projects.ctp b/app/views/admin/projects.ctp new file mode 100644 index 0000000..6c7a21f --- /dev/null +++ b/app/views/admin/projects.ctp @@ -0,0 +1,52 @@ +<div class="contextual"> +<%= link_to l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add' %> +</div> + +<h2><%=l(:label_project_plural)%></h2> + +<% form_tag({}, :method => :get) do %> +<fieldset><legend><%= l(:label_filter_plural) %></legend> +<label><%= l(:field_status) %> :</label> +<%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %> +<label><%= l(:label_project) %>:</label> +<%= text_field_tag 'name', params[:name], :size => 30 %> +<%= submit_tag l(:button_apply), :class => "small", :name => nil %> +</fieldset> +<% end %> +&nbsp; + +<table class="list"> + <thead><tr> + <%= sort_header_tag('name', :caption => l(:label_project)) %> + <th><%=l(:field_description)%></th> + <th><%=l(:label_subproject_plural)%></th> + <%= sort_header_tag('is_public', :caption => l(:field_is_public), :default_order => 'desc') %> + <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %> + <th></th> + <th></th> + </tr></thead> + <tbody> +<% for project in @projects %> + <tr class="<%= cycle("odd", "even") %>"> + <td><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %> + <td><%= textilizable project.short_description, :project => project %> + <td align="center"><%= project.children.size %> + <td align="center"><%= image_tag 'true.png' if project.is_public? %> + <td align="center"><%= format_date(project.created_on) %> + <td align="center" style="width:10%"> + <small> + <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %> + <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %> + </small> + </td> + <td align="center" style="width:10%"> + <small><%= link_to(l(:button_delete), { :controller => 'projects', :action => 'destroy', :id => project }, :class => 'icon icon-del') %></small> + </td> + </tr> +<% end %> + </tbody> +</table> + +<p class="pagination"><%= pagination_links_full @project_pages, @project_count %></p> + +<% html_title(l(:label_project_plural)) -%> diff --git a/app/views/attachments/_form.ctp b/app/views/attachments/_form.ctp new file mode 100644 index 0000000..c98528b --- /dev/null +++ b/app/views/attachments/_form.ctp @@ -0,0 +1,9 @@ +<span id="attachments_fields"> +<%= file_field_tag 'attachments[1][file]', :size => 30, :id => nil -%> +<%= text_field_tag 'attachments[1][description]', '', :size => 60, :id => nil %> +<em><%= l(:label_optional_description) %></em> +</span> +<br /> +<small><%= link_to l(:label_add_another_file), '#', :onclick => 'addFileField(); return false;' %> +(<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>) +</small> diff --git a/app/views/attachments/_links.ctp b/app/views/attachments/_links.ctp new file mode 100644 index 0000000..19ab673 --- /dev/null +++ b/app/views/attachments/_links.ctp @@ -0,0 +1,18 @@ +<div class="attachments"> +<% for attachment in attachments %> +<p><%= link_to_attachment attachment, :class => 'icon icon-attachment' -%> +<%= h(" - #{attachment.description}") unless attachment.description.blank? %> + <span class="size">(<%= number_to_human_size attachment.filesize %>)</span> + <% if options[:deletable] %> + <%= link_to image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => attachment}, + :confirm => l(:text_are_you_sure), + :method => :post, + :class => 'delete', + :title => l(:button_delete) %> + <% end %> + <% if options[:author] %> + <span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span> + <% end %> + </p> +<% end %> +</div> diff --git a/app/views/attachments/diff.ctp b/app/views/attachments/diff.ctp new file mode 100644 index 0000000..7b64dca --- /dev/null +++ b/app/views/attachments/diff.ctp @@ -0,0 +1,15 @@ +<h2><%=h @attachment.filename %></h2> + +<div class="attachments"> +<p><%= h("#{@attachment.description} - ") unless @attachment.description.blank? %> + <span class="author"><%= @attachment.author %>, <%= format_time(@attachment.created_on) %></span></p> +<p><%= link_to_attachment @attachment, :text => l(:button_download), :download => true -%> + <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p> + +</div> +&nbsp; +<%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %> + +<% content_for :header_tags do -%> + <%= stylesheet_link_tag "scm" -%> +<% end -%> diff --git a/app/views/attachments/file.ctp b/app/views/attachments/file.ctp new file mode 100644 index 0000000..468c6b6 --- /dev/null +++ b/app/views/attachments/file.ctp @@ -0,0 +1,15 @@ +<h2><%=h @attachment.filename %></h2> + +<div class="attachments"> +<p><%= h("#{@attachment.description} - ") unless @attachment.description.blank? %> + <span class="author"><%= @attachment.author %>, <%= format_time(@attachment.created_on) %></span></p> +<p><%= link_to_attachment @attachment, :text => l(:button_download), :download => true -%> + <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p> + +</div> +&nbsp; +<%= render :partial => 'common/file', :locals => {:content => @content, :filename => @attachment.filename} %> + +<% content_for :header_tags do -%> + <%= stylesheet_link_tag "scm" -%> +<% end -%> diff --git a/app/views/auth_sources/_form.ctp b/app/views/auth_sources/_form.ctp new file mode 100644 index 0000000..9ffffaf --- /dev/null +++ b/app/views/auth_sources/_form.ctp @@ -0,0 +1,44 @@ +<%= error_messages_for 'auth_source' %> + +<div class="box"> +<!--[form:auth_source]--> +<p><label for="auth_source_name"><%=l(:field_name)%> <span class="required">*</span></label> +<%= text_field 'auth_source', 'name' %></p> + +<p><label for="auth_source_host"><%=l(:field_host)%> <span class="required">*</span></label> +<%= text_field 'auth_source', 'host' %></p> + +<p><label for="auth_source_port"><%=l(:field_port)%> <span class="required">*</span></label> +<%= text_field 'auth_source', 'port', :size => 6 %> <%= check_box 'auth_source', 'tls' %> LDAPS</p> + +<p><label for="auth_source_account"><%=l(:field_account)%></label> +<%= text_field 'auth_source', 'account' %></p> + +<p><label for="auth_source_account_password"><%=l(:field_password)%></label> +<%= password_field 'auth_source', 'account_password', :name => 'ignore', + :value => ((@auth_source.new_record? || @auth_source.account_password.blank?) ? '' : ('x'*15)), + :onfocus => "this.value=''; this.name='auth_source[account_password]';", + :onchange => "this.name='auth_source[account_password]';" %></p> + +<p><label for="auth_source_base_dn"><%=l(:field_base_dn)%> <span class="required">*</span></label> +<%= text_field 'auth_source', 'base_dn', :size => 60 %></p> + +<p><label for="auth_source_onthefly_register"><%=l(:field_onthefly)%></label> +<%= check_box 'auth_source', 'onthefly_register' %></p> +</div> + +<fieldset class="box"><legend><%=l(:label_attribute_plural)%></legend> +<p><label for="auth_source_attr_login"><%=l(:field_login)%> <span class="required">*</span></label> +<%= text_field 'auth_source', 'attr_login', :size => 20 %></p> + +<p><label for="auth_source_attr_firstname"><%=l(:field_firstname)%></label> +<%= text_field 'auth_source', 'attr_firstname', :size => 20 %></p> + +<p><label for="auth_source_attr_lastname"><%=l(:field_lastname)%></label> +<%= text_field 'auth_source', 'attr_lastname', :size => 20 %></p> + +<p><label for="auth_source_attr_mail"><%=l(:field_mail)%></label> +<%= text_field 'auth_source', 'attr_mail', :size => 20 %></p> +</fieldset> +<!--[eoform:auth_source]--> + diff --git a/app/views/auth_sources/edit.ctp b/app/views/auth_sources/edit.ctp new file mode 100644 index 0000000..165fd4f --- /dev/null +++ b/app/views/auth_sources/edit.ctp @@ -0,0 +1,7 @@ +<h2><%=l(:label_auth_source)%> (<%= @auth_source.auth_method_name %>)</h2> + +<% form_tag({:action => 'update', :id => @auth_source}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_save) %> +<% end %> + diff --git a/app/views/auth_sources/list.ctp b/app/views/auth_sources/list.ctp new file mode 100644 index 0000000..5729ec7 --- /dev/null +++ b/app/views/auth_sources/list.ctp @@ -0,0 +1,33 @@ +<div class="contextual"> +<%= link_to l(:label_auth_source_new), {:action => 'new'}, :class => 'icon icon-add' %> +</div> + +<h2><%=l(:label_auth_source_plural)%></h2> + +<table class="list"> + <thead><tr> + <th><%=l(:field_name)%></th> + <th><%=l(:field_type)%></th> + <th><%=l(:field_host)%></th> + <th><%=l(:label_user_plural)%></th> + <th></th> + <th></th> + </tr></thead> + <tbody> +<% for source in @auth_sources %> + <tr class="<%= cycle("odd", "even") %>"> + <td><%= link_to source.name, :action => 'edit', :id => source%></td> + <td align="center"><%= source.auth_method_name %></td> + <td align="center"><%= source.host %></td> + <td align="center"><%= source.users.count %></td> + <td align="center"><%= link_to l(:button_test), :action => 'test_connection', :id => source %></td> + <td align="center"><%= button_to l(:button_delete), { :action => 'destroy', :id => source }, + :confirm => l(:text_are_you_sure), + :class => "button-small", + :disabled => source.users.any? %></td> + </tr> +<% end %> + </tbody> +</table> + +<p class="pagination"><%= pagination_links_full @auth_source_pages %></p> diff --git a/app/views/auth_sources/new.ctp b/app/views/auth_sources/new.ctp new file mode 100644 index 0000000..2d493dc --- /dev/null +++ b/app/views/auth_sources/new.ctp @@ -0,0 +1,6 @@ +<h2><%=l(:label_auth_source_new)%> (<%= @auth_source.auth_method_name %>)</h2> + +<% form_tag({:action => 'create'}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_create) %> +<% end %> diff --git a/app/views/boards/_form.ctp b/app/views/boards/_form.ctp new file mode 100644 index 0000000..7ede589 --- /dev/null +++ b/app/views/boards/_form.ctp @@ -0,0 +1,8 @@ +<%= error_messages_for 'board' %> + +<!--[form:board]--> +<div class="box"> +<p><%= f.text_field :name, :required => true %></p> +<p><%= f.text_field :description, :required => true, :size => 80 %></p> +</div> +<!--[eoform:board]--> diff --git a/app/views/boards/edit.ctp b/app/views/boards/edit.ctp new file mode 100644 index 0000000..ba4c8b5 --- /dev/null +++ b/app/views/boards/edit.ctp @@ -0,0 +1,6 @@ +<h2><%= l(:label_board) %></h2> + +<% labelled_tabular_form_for :board, @board, :url => {:action => 'edit', :id => @board} do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/boards/index.ctp b/app/views/boards/index.ctp new file mode 100644 index 0000000..655352a --- /dev/null +++ b/app/views/boards/index.ctp @@ -0,0 +1,42 @@ +<h2><%= l(:label_board_plural) %></h2> + +<table class="list boards"> + <thead><tr> + <th><%= l(:label_board) %></th> + <th><%= l(:label_topic_plural) %></th> + <th><%= l(:label_message_plural) %></th> + <th><%= l(:label_message_last) %></th> + </tr></thead> + <tbody> +<% for board in @boards %> + <tr class="<%= cycle 'odd', 'even' %>"> + <td> + <%= link_to h(board.name), {:action => 'show', :id => board}, :class => "icon22 icon22-comment" %><br /> + <%=h board.description %> + </td> + <td align="center"><%= board.topics_count %></td> + <td align="center"><%= board.messages_count %></td> + <td> + <small> + <% if board.last_message %> + <%= authoring board.last_message.created_on, board.last_message.author %><br /> + <%= link_to_message board.last_message %> + <% end %> + </small> + </td> + </tr> +<% end %> + </tbody> +</table> + +<p class="other-formats"> +<%= l(:label_export_to) %> +<span><%= link_to 'Atom', {:controller => 'projects', :action => 'activity', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}, + :class => 'feed' %></span> +</p> + +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, {:controller => 'projects', :action => 'activity', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}) %> +<% end %> + +<% html_title l(:label_board_plural) %> diff --git a/app/views/boards/new.ctp b/app/views/boards/new.ctp new file mode 100644 index 0000000..b891218 --- /dev/null +++ b/app/views/boards/new.ctp @@ -0,0 +1,6 @@ +<h2><%= l(:label_board_new) %></h2> + +<% labelled_tabular_form_for :board, @board, :url => {:action => 'new'} do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_create) %> +<% end %> diff --git a/app/views/boards/show.ctp b/app/views/boards/show.ctp new file mode 100644 index 0000000..adf0b46 --- /dev/null +++ b/app/views/boards/show.ctp @@ -0,0 +1,62 @@ +<%= breadcrumb link_to(l(:label_board_plural), {:controller => 'boards', :action => 'index', :project_id => @project}) %> + +<div class="contextual"> +<%= link_to_if_authorized l(:label_message_new), + {:controller => 'messages', :action => 'new', :board_id => @board}, + :class => 'icon icon-add', + :onclick => 'Element.show("add-message"); return false;' %> +<%= watcher_tag(@board, User.current) %> +</div> + +<div id="add-message" style="display:none;"> +<h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%= l(:label_message_new) %></h2> +<% form_for :message, @message, :url => {:controller => 'messages', :action => 'new', :board_id => @board}, :html => {:multipart => true, :id => 'message-form'} do |f| %> + <%= render :partial => 'messages/form', :locals => {:f => f} %> + <p><%= submit_tag l(:button_create) %> + <%= link_to_remote l(:label_preview), + { :url => { :controller => 'messages', :action => 'preview', :board_id => @board }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('message-form')", + :complete => "Element.scrollTo('preview')" + }, :accesskey => accesskey(:preview) %> | + <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-message")' %></p> +<% end %> +<div id="preview" class="wiki"></div> +</div> + +<h2><%=h @board.name %></h2> +<p class="subtitle"><%=h @board.description %></p> + +<% if @topics.any? %> +<table class="list messages"> + <thead><tr> + <th><%= l(:field_subject) %></th> + <th><%= l(:field_author) %></th> + <%= sort_header_tag('created_on', :caption => l(:field_created_on)) %> + <%= sort_header_tag('replies', :caption => l(:label_reply_plural)) %> + <%= sort_header_tag('updated_on', :caption => l(:label_message_last)) %> + </tr></thead> + <tbody> + <% @topics.each do |topic| %> + <tr class="message <%= cycle 'odd', 'even' %> <%= topic.sticky? ? 'sticky' : '' %> <%= topic.locked? ? 'locked' : '' %>"> + <td class="subject"><%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => topic }, :class => 'icon' %></td> + <td class="author" align="center"><%= topic.author %></td> + <td class="created_on" align="center"><%= format_time(topic.created_on) %></td> + <td class="replies" align="center"><%= topic.replies_count %></td> + <td class="last_message"> + <% if topic.last_reply %> + <%= authoring topic.last_reply.created_on, topic.last_reply.author %><br /> + <%= link_to_message topic.last_reply %> + <% end %> + </td> + </tr> + <% end %> + </tbody> +</table> +<p class="pagination"><%= pagination_links_full @topic_pages, @topic_count %></p> +<% else %> +<p class="nodata"><%= l(:label_no_data) %></p> +<% end %> + +<% html_title h(@board.name) %> diff --git a/app/views/common/403.ctp b/app/views/common/403.ctp new file mode 100644 index 0000000..d1173a1 --- /dev/null +++ b/app/views/common/403.ctp @@ -0,0 +1,6 @@ +<h2>403</h2> + +<p><%= l(:notice_not_authorized) %></p> +<p><a href="javascript:history.back()">Back</a></p> + +<% html_title '403' %> diff --git a/app/views/common/404.ctp b/app/views/common/404.ctp new file mode 100644 index 0000000..753e716 --- /dev/null +++ b/app/views/common/404.ctp @@ -0,0 +1,6 @@ +<h2>404</h2> + +<p><%= l(:notice_file_not_found) %></p> +<p><a href="javascript:history.back()">Back</a></p> + +<% html_title '404' %> diff --git a/app/views/common/_calendar.ctp b/app/views/common/_calendar.ctp new file mode 100644 index 0000000..1095cd5 --- /dev/null +++ b/app/views/common/_calendar.ctp @@ -0,0 +1,39 @@ +<table class="cal"> +<thead> +<tr><td></td><% 7.times do |i| %><th><%= day_name( (calendar.first_wday+i)%7 ) %></th><% end %></tr> +</thead> +<tbody> +<tr> +<% day = calendar.startdt +while day <= calendar.enddt %> +<%= "<th>#{day.cweek}</th>" if day.cwday == calendar.first_wday %> +<td class="<%= day.month==calendar.month ? 'even' : 'odd' %><%= ' today' if Date.today == day %>"> +<p class="day-num"><%= day.day %></p> +<% calendar.events_on(day).each do |i| %> + <% if i.is_a? Issue %> + <div class="tooltip"> + <%= if day == i.start_date && day == i.due_date + image_tag('arrow_bw.png') + elsif day == i.start_date + image_tag('arrow_from.png') + elsif day == i.due_date + image_tag('arrow_to.png') + end %> + <%= h("#{i.project} -") unless @project && @project == i.project %> + <%= link_to_issue i %>: <%= h(truncate(i.subject, 30)) %> + <span class="tip"><%= render_issue_tooltip i %></span> + </div> + <% else %> + <span class="icon icon-package"> + <%= h("#{i.project} -") unless @project && @project == i.project %> + <%= link_to_version i%> + </span> + <% end %> +<% end %> +</td> +<%= '</tr><tr>' if day.cwday==calendar.last_wday and day!=calendar.enddt %> +<% day = day + 1 +end %> +</tr> +</tbody> +</table> diff --git a/app/views/common/_diff.ctp b/app/views/common/_diff.ctp new file mode 100644 index 0000000..104845b --- /dev/null +++ b/app/views/common/_diff.ctp @@ -0,0 +1,67 @@ +<% diff = Redmine::UnifiedDiff.new(diff, :type => diff_type, :max_lines => Setting.diff_max_lines_displayed.to_i) -%> +<% diff.each do |table_file| -%> +<div class="autoscroll"> +<% if diff_type == 'sbs' -%> +<table class="filecontent CodeRay"> +<thead> +<tr><th colspan="4" class="filename"><%= table_file.file_name %></th></tr> +</thead> +<tbody> +<% prev_line_left, prev_line_right = nil, nil -%> +<% table_file.keys.sort.each do |key| -%> +<% if prev_line_left && prev_line_right && (table_file[key].nb_line_left != prev_line_left+1) && (table_file[key].nb_line_right != prev_line_right+1) -%> +<tr class="spacing"> +<th class="line-num">...</th><td></td><th class="line-num">...</th><td></td> +<% end -%> +<tr> + <th class="line-num"><%= table_file[key].nb_line_left %></th> + <td class="line-code <%= table_file[key].type_diff_left %>"> + <pre><%=to_utf8 table_file[key].line_left %></pre> + </td> + <th class="line-num"><%= table_file[key].nb_line_right %></th> + <td class="line-code <%= table_file[key].type_diff_right %>"> + <pre><%=to_utf8 table_file[key].line_right %></pre> + </td> +</tr> +<% prev_line_left, prev_line_right = table_file[key].nb_line_left.to_i, table_file[key].nb_line_right.to_i -%> +<% end -%> +</tbody> +</table> + +<% else -%> +<table class="filecontent CodeRay"> +<thead> +<tr><th colspan="3" class="filename"><%= table_file.file_name %></th></tr> +</thead> +<tbody> +<% prev_line_left, prev_line_right = nil, nil -%> +<% table_file.keys.sort.each do |key, line| %> +<% if prev_line_left && prev_line_right && (table_file[key].nb_line_left != prev_line_left+1) && (table_file[key].nb_line_right != prev_line_right+1) -%> +<tr class="spacing"> +<th class="line-num">...</th><th class="line-num">...</th><td></td> +</tr> +<% end -%> +<tr> + <th class="line-num"><%= table_file[key].nb_line_left %></th> + <th class="line-num"><%= table_file[key].nb_line_right %></th> + <% if table_file[key].line_left.empty? -%> + <td class="line-code <%= table_file[key].type_diff_right %>"> + <pre><%=to_utf8 table_file[key].line_right %></pre> + </td> + <% else -%> + <td class="line-code <%= table_file[key].type_diff_left %>"> + <pre><%=to_utf8 table_file[key].line_left %></pre> + </td> + <% end -%> +</tr> +<% prev_line_left = table_file[key].nb_line_left.to_i if table_file[key].nb_line_left.to_i > 0 -%> +<% prev_line_right = table_file[key].nb_line_right.to_i if table_file[key].nb_line_right.to_i > 0 -%> +<% end -%> +</tbody> +</table> +<% end -%> + +</div> +<% end -%> + +<%= l(:text_diff_truncated) if diff.truncated? %> diff --git a/app/views/common/_file.ctp b/app/views/common/_file.ctp new file mode 100644 index 0000000..43f5c6c --- /dev/null +++ b/app/views/common/_file.ctp @@ -0,0 +1,11 @@ +<div class="autoscroll"> +<table class="filecontent CodeRay"> +<tbody> +<% line_num = 1 %> +<% syntax_highlight(filename, to_utf8(content)).each_line do |line| %> +<tr><th class="line-num" id="L<%= line_num %>"><%= line_num %></th><td class="line-code"><pre><%= line %></pre></td></tr> +<% line_num += 1 %> +<% end %> +</tbody> +</table> +</div> diff --git a/app/views/common/_preview.ctp b/app/views/common/_preview.ctp new file mode 100644 index 0000000..fd95f11 --- /dev/null +++ b/app/views/common/_preview.ctp @@ -0,0 +1,3 @@ +<fieldset class="preview"><legend><%= l(:label_preview) %></legend> +<%= textilizable @text, :attachments => @attachements, :object => @previewed %> +</fieldset> diff --git a/app/views/common/feed.atom.rxml b/app/views/common/feed.atom.rxml new file mode 100644 index 0000000..8d07cf2 --- /dev/null +++ b/app/views/common/feed.atom.rxml @@ -0,0 +1,31 @@ +xml.instruct! +xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do + xml.title truncate_single_line(@title, 100) + xml.link "rel" => "self", "href" => url_for(params.merge({:format => nil, :only_path => false})) + xml.link "rel" => "alternate", "href" => url_for(:controller => 'welcome', :only_path => false) + xml.id url_for(:controller => 'welcome', :only_path => false) + xml.updated((@items.first ? @items.first.event_datetime : Time.now).xmlschema) + xml.author { xml.name "#{Setting.app_title}" } + xml.generator(:uri => Redmine::Info.url) { xml.text! Redmine::Info.app_name; } + @items.each do |item| + xml.entry do + url = url_for(item.event_url(:only_path => false)) + if @project + xml.title truncate_single_line(item.event_title, 100) + else + xml.title truncate_single_line("#{item.project} - #{item.event_title}", 100) + end + xml.link "rel" => "alternate", "href" => url + xml.id url + xml.updated item.event_datetime.xmlschema + author = item.event_author if item.respond_to?(:event_author) + xml.author do + xml.name(author) + xml.email(author.mail) if author.respond_to?(:mail) && !author.mail.blank? + end if author + xml.content "type" => "html" do + xml.text! textilizable(item.event_description) + end + end + end +end diff --git a/app/views/custom_fields/_form.ctp b/app/views/custom_fields/_form.ctp new file mode 100644 index 0000000..f4aee68 --- /dev/null +++ b/app/views/custom_fields/_form.ctp @@ -0,0 +1,113 @@ +<%= error_messages_for 'custom_field' %> + +<script type="text/javascript"> +//<![CDATA[ +function toggle_custom_field_format() { + format = $("custom_field_field_format"); + p_length = $("custom_field_min_length"); + p_regexp = $("custom_field_regexp"); + p_values = $("custom_field_possible_values"); + p_searchable = $("custom_field_searchable"); + p_default = $("custom_field_default_value"); + + p_default.setAttribute('type','text'); + Element.show(p_default.parentNode); + + switch (format.value) { + case "list": + Element.hide(p_length.parentNode); + Element.hide(p_regexp.parentNode); + if (p_searchable) Element.show(p_searchable.parentNode); + Element.show(p_values); + break; + case "bool": + p_default.setAttribute('type','checkbox'); + Element.hide(p_length.parentNode); + Element.hide(p_regexp.parentNode); + if (p_searchable) Element.hide(p_searchable.parentNode); + Element.hide(p_values); + break; + case "date": + Element.hide(p_length.parentNode); + Element.hide(p_regexp.parentNode); + if (p_searchable) Element.hide(p_searchable.parentNode); + Element.hide(p_values); + break; + case "float": + case "int": + Element.show(p_length.parentNode); + Element.show(p_regexp.parentNode); + if (p_searchable) Element.hide(p_searchable.parentNode); + Element.hide(p_values); + break; + default: + Element.show(p_length.parentNode); + Element.show(p_regexp.parentNode); + if (p_searchable) Element.show(p_searchable.parentNode); + Element.hide(p_values); + break; + } +} + +function addValueField() { + var f = $$('p#custom_field_possible_values span'); + p = document.getElementById("custom_field_possible_values"); + var v = f[0].cloneNode(true); + v.childNodes[0].value = ""; + p.appendChild(v); +} + +function deleteValueField(e) { + var f = $$('p#custom_field_possible_values span'); + if (f.length == 1) { + e.parentNode.childNodes[0].value = ""; + } else { + Element.remove(e.parentNode); + } +} + +//]]> +</script> + +<div class="box"> +<p><%= f.text_field :name, :required => true %></p> +<p><%= f.select :field_format, custom_field_formats_for_select, {}, :onchange => "toggle_custom_field_format();" %></p> +<p><label for="custom_field_min_length"><%=l(:label_min_max_length)%></label> + <%= f.text_field :min_length, :size => 5, :no_label => true %> - + <%= f.text_field :max_length, :size => 5, :no_label => true %><br>(<%=l(:text_min_max_length_info)%>)</p> +<p><%= f.text_field :regexp, :size => 50 %><br>(<%=l(:text_regexp_info)%>)</p> +<p id="custom_field_possible_values"><label><%= l(:field_possible_values) %> <%= image_to_function "add.png", "addValueField();return false" %></label> +<% (@custom_field.possible_values.to_a + [""]).each do |value| %> +<span><%= text_field_tag 'custom_field[possible_values][]', value, :size => 30 %> <%= image_to_function "delete.png", "deleteValueField(this);return false" %><br /></span> +<% end %> +</p> +<p><%= @custom_field.field_format == 'bool' ? f.check_box(:default_value) : f.text_field(:default_value) %></p> +</div> + +<div class="box"> +<% case @custom_field.type.to_s +when "IssueCustomField" %> + + <fieldset><legend><%=l(:label_tracker_plural)%></legend> + <% for tracker in @trackers %> + <%= check_box_tag "tracker_ids[]", tracker.id, (@custom_field.trackers.include? tracker) %> <%= tracker.name %> + <% end %> + </fieldset> + &nbsp; + <p><%= f.check_box :is_required %></p> + <p><%= f.check_box :is_for_all %></p> + <p><%= f.check_box :is_filter %></p> + <p><%= f.check_box :searchable %></p> + +<% when "UserCustomField" %> + <p><%= f.check_box :is_required %></p> + +<% when "ProjectCustomField" %> + <p><%= f.check_box :is_required %></p> + +<% when "TimeEntryCustomField" %> + <p><%= f.check_box :is_required %></p> + +<% end %> +</div> +<%= javascript_tag "toggle_custom_field_format();" %> diff --git a/app/views/custom_fields/edit.ctp b/app/views/custom_fields/edit.ctp new file mode 100644 index 0000000..ef056fa --- /dev/null +++ b/app/views/custom_fields/edit.ctp @@ -0,0 +1,6 @@ +<h2><%=l(:label_custom_field)%> (<%=l(@custom_field.type_name)%>)</h2> + +<% labelled_tabular_form_for :custom_field, @custom_field, :url => { :action => "edit", :id => @custom_field } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/custom_fields/list.ctp b/app/views/custom_fields/list.ctp new file mode 100644 index 0000000..43ddd99 --- /dev/null +++ b/app/views/custom_fields/list.ctp @@ -0,0 +1,58 @@ +<h2><%=l(:label_custom_field_plural)%></h2> + +<% selected_tab = params[:tab] ? params[:tab].to_s : custom_fields_tabs.first[:name] %> + +<div class="tabs"> +<ul> +<% custom_fields_tabs.each do |tab| -%> + <li><%= link_to l(tab[:label]), { :tab => tab[:name] }, + :id => "tab-#{tab[:name]}", + :class => (tab[:name] != selected_tab ? nil : 'selected'), + :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %></li> +<% end -%> +</ul> +</div> + +<% custom_fields_tabs.each do |tab| %> +<div id="tab-content-<%= tab[:name] %>" class="tab-content" style="<%= tab[:name] != selected_tab ? 'display:none' : nil %>"> +<table class="list"> + <thead><tr> + <th width="30%"><%=l(:field_name)%></th> + <th><%=l(:field_field_format)%></th> + <th><%=l(:field_is_required)%></th> + <% if tab[:name] == 'IssueCustomField' %> + <th><%=l(:field_is_for_all)%></th> + <th><%=l(:label_used_by)%></th> + <% end %> + <th><%=l(:button_sort)%></th> + <th width="10%"></th> + </tr></thead> + <tbody> +<% (@custom_fields_by_type[tab[:name]] || []).sort.each do |custom_field| -%> + <tr class="<%= cycle("odd", "even") %>"> + <td><%= link_to custom_field.name, :action => 'edit', :id => custom_field %></td> + <td align="center"><%= l(CustomField::FIELD_FORMATS[custom_field.field_format][:name]) %></td> + <td align="center"><%= image_tag 'true.png' if custom_field.is_required? %></td> + <% if tab[:name] == 'IssueCustomField' %> + <td align="center"><%= image_tag 'true.png' if custom_field.is_for_all? %></td> + <td align="center"><%= custom_field.projects.count.to_s + ' ' + lwr(:label_project, custom_field.projects.count) if custom_field.is_a? IssueCustomField and !custom_field.is_for_all? %></td> + <% end %> + <td align="center" style="width:15%;"> + <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => custom_field, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> + <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => custom_field, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> - + <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => custom_field, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %> + <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => custom_field, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %> + </td> + <td align="center"> + <%= button_to l(:button_delete), { :action => 'destroy', :id => custom_field }, :confirm => l(:text_are_you_sure), :class => "button-small" %> + </td> + </tr> +<% end; reset_cycle %> + </tbody> +</table> + +<p><%= link_to l(:label_custom_field_new), {:action => 'new', :type => tab[:name]}, :class => 'icon icon-add' %></p> +</div> +<% end %> + +<% html_title(l(:label_custom_field_plural)) -%> diff --git a/app/views/custom_fields/new.ctp b/app/views/custom_fields/new.ctp new file mode 100644 index 0000000..2e8aa27 --- /dev/null +++ b/app/views/custom_fields/new.ctp @@ -0,0 +1,7 @@ +<h2><%=l(:label_custom_field_new)%> (<%=l(@custom_field.type_name)%>)</h2> + +<% labelled_tabular_form_for :custom_field, @custom_field, :url => { :action => "new" } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= hidden_field_tag 'type', @custom_field.type %> +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/documents/_document.ctp b/app/views/documents/_document.ctp new file mode 100644 index 0000000..ddfdb9e --- /dev/null +++ b/app/views/documents/_document.ctp @@ -0,0 +1,3 @@ +<p><%= link_to h(document.title), :controller => 'documents', :action => 'show', :id => document %><br /> +<% unless document.description.blank? %><%=h(truncate(document.description, 250)) %><br /><% end %> +<em><%= format_time(document.created_on) %></em></p> \ No newline at end of file diff --git a/app/views/documents/_form.ctp b/app/views/documents/_form.ctp new file mode 100644 index 0000000..d45e295 --- /dev/null +++ b/app/views/documents/_form.ctp @@ -0,0 +1,15 @@ +<%= error_messages_for 'document' %> +<div class="box"> +<!--[form:document]--> +<p><label for="document_category_id"><%=l(:field_category)%></label> +<%= select('document', 'category_id', Enumeration.get_values('DCAT').collect {|c| [c.name, c.id]}) %></p> + +<p><label for="document_title"><%=l(:field_title)%> <span class="required">*</span></label> +<%= text_field 'document', 'title', :size => 60 %></p> + +<p><label for="document_description"><%=l(:field_description)%></label> +<%= text_area 'document', 'description', :cols => 60, :rows => 15, :class => 'wiki-edit' %></p> +<!--[eoform:document]--> +</div> + +<%= wikitoolbar_for 'document_description' %> diff --git a/app/views/documents/edit.ctp b/app/views/documents/edit.ctp new file mode 100644 index 0000000..0b9f31f --- /dev/null +++ b/app/views/documents/edit.ctp @@ -0,0 +1,8 @@ +<h2><%=l(:label_document)%></h2> + +<% form_tag({:action => 'edit', :id => @document}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_save) %> +<% end %> + + diff --git a/app/views/documents/index.ctp b/app/views/documents/index.ctp new file mode 100644 index 0000000..14d9973 --- /dev/null +++ b/app/views/documents/index.ctp @@ -0,0 +1,39 @@ +<div class="contextual"> +<%= link_to_if_authorized l(:label_document_new), + {:controller => 'documents', :action => 'new', :project_id => @project}, + :class => 'icon icon-add', + :onclick => 'Element.show("add-document"); return false;' %> +</div> + +<div id="add-document" style="display:none;"> +<h2><%=l(:label_document_new)%></h2> +<% form_tag({:controller => 'documents', :action => 'new', :project_id => @project}, :class => "tabular", :multipart => true) do %> +<%= render :partial => 'documents/form' %> +<div class="box"> +<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p> +</div> +<%= submit_tag l(:button_create) %> +<%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-document")' %> +<% end %> +</div> + +<h2><%=l(:label_document_plural)%></h2> + +<% if @grouped.empty? %><p class="nodata"><%= l(:label_no_data) %></p><% end %> + +<% @grouped.keys.sort.each do |group| %> + <h3><%= group %></h3> + <%= render :partial => 'documents/document', :collection => @grouped[group] %> +<% end %> + +<% content_for :sidebar do %> + <h3><%= l(:label_sort_by, '') %></h3> + <% form_tag({}, :method => :get) do %> + <label><%= radio_button_tag 'sort_by', 'category', (@sort_by == 'category'), :onclick => 'this.form.submit();' %> <%= l(:field_category) %></label><br /> + <label><%= radio_button_tag 'sort_by', 'date', (@sort_by == 'date'), :onclick => 'this.form.submit();' %> <%= l(:label_date) %></label><br /> + <label><%= radio_button_tag 'sort_by', 'title', (@sort_by == 'title'), :onclick => 'this.form.submit();' %> <%= l(:field_title) %></label><br /> + <label><%= radio_button_tag 'sort_by', 'author', (@sort_by == 'author'), :onclick => 'this.form.submit();' %> <%= l(:field_author) %></label> + <% end %> +<% end %> + +<% html_title(l(:label_document_plural)) -%> diff --git a/app/views/documents/new.ctp b/app/views/documents/new.ctp new file mode 100644 index 0000000..639b4f2 --- /dev/null +++ b/app/views/documents/new.ctp @@ -0,0 +1,13 @@ +<h2><%=l(:label_document_new)%></h2> + +<% form_tag({:controller => 'documents', :action => 'new', :project_id => @project}, :class => "tabular", :multipart => true) do %> +<%= render :partial => 'documents/form' %> + +<div class="box"> +<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p> +</div> + +<%= submit_tag l(:button_create) %> +<% end %> + + diff --git a/app/views/documents/show.ctp b/app/views/documents/show.ctp new file mode 100644 index 0000000..4d18a77 --- /dev/null +++ b/app/views/documents/show.ctp @@ -0,0 +1,28 @@ +<div class="contextual"> +<%= link_to_if_authorized l(:button_edit), {:controller => 'documents', :action => 'edit', :id => @document}, :class => 'icon icon-edit', :accesskey => accesskey(:edit) %> +<%= link_to_if_authorized l(:button_delete), {:controller => 'documents', :action => 'destroy', :id => @document}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> +</div> + +<h2><%=h @document.title %></h2> + +<p><em><%=h @document.category.name %><br /> +<%= format_date @document.created_on %></em></p> +<div class="wiki"> +<%= textilizable @document.description, :attachments => @document.attachments %> +</div> + +<h3><%= l(:label_attachment_plural) %></h3> +<%= link_to_attachments @document %> + +<% if authorize_for('documents', 'add_attachment') %> +<p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;", + :id => 'attach_files_link' %></p> + <% form_tag({ :controller => 'documents', :action => 'add_attachment', :id => @document }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %> + <div class="box"> + <p><%= render :partial => 'attachments/form' %></p> + </div> + <%= submit_tag l(:button_add) %> + <% end %> +<% end %> + +<% html_title @document.title -%> diff --git a/app/views/elements/account_menu.ctp b/app/views/elements/account_menu.ctp new file mode 100755 index 0000000..3541d73 --- /dev/null +++ b/app/views/elements/account_menu.ctp @@ -0,0 +1,2 @@ +<ul><li><a href="/my/account" class="my-account"><?php __('myaccount') ?></a></li> +<li><a href="/logout" class="logout"><?php __('logout') ?></a></li></ul> \ No newline at end of file diff --git a/app/views/elements/empty b/app/views/elements/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/views/elements/top_menu.ctp b/app/views/elements/top_menu.ctp new file mode 100755 index 0000000..8466ccb --- /dev/null +++ b/app/views/elements/top_menu.ctp @@ -0,0 +1,5 @@ +<ul><li><a href="/" class="home"><?php __('home') ?></a></li> +<li><a href="/my/page" class="my-page"><?php __('mypage') ?></a></li> +<li><a href="/projects" class="projects"><?php __('projects') ?></a></li> +<li><a href="/admin" class="administration"><?php __('administration')?></a></li> +<li><a href="http://www.redmine.org/guide" class="help"><?php __('help') ?></a></li></ul> \ No newline at end of file diff --git a/app/views/enumerations/_form.ctp b/app/views/enumerations/_form.ctp new file mode 100644 index 0000000..3f98f52 --- /dev/null +++ b/app/views/enumerations/_form.ctp @@ -0,0 +1,12 @@ +<%= error_messages_for 'enumeration' %> +<div class="box"> +<!--[form:optvalue]--> +<%= hidden_field 'enumeration', 'opt' %> + +<p><label for="enumeration_name"><%=l(:field_name)%></label> +<%= text_field 'enumeration', 'name' %></p> + +<p><label for="enumeration_is_default"><%=l(:field_is_default)%></label> +<%= check_box 'enumeration', 'is_default' %></p> +<!--[eoform:optvalue]--> +</div> \ No newline at end of file diff --git a/app/views/enumerations/destroy.ctp b/app/views/enumerations/destroy.ctp new file mode 100644 index 0000000..657df83 --- /dev/null +++ b/app/views/enumerations/destroy.ctp @@ -0,0 +1,12 @@ +<h2><%= l(@enumeration.option_name) %>: <%=h @enumeration %></h2> + +<% form_tag({}) do %> +<div class="box"> +<p><strong><%= l(:text_enumeration_destroy_question, @enumeration.objects_count) %></strong></p> +<p><%= l(:text_enumeration_category_reassign_to) %> +<%= select_tag 'reassign_to_id', ("<option>--- #{l(:actionview_instancetag_blank_option)} ---</option>" + options_from_collection_for_select(@enumerations, 'id', 'name')) %></p> +</div> + +<%= submit_tag l(:button_apply) %> +<%= link_to l(:button_cancel), :controller => 'enumerations', :action => 'index' %> +<% end %> diff --git a/app/views/enumerations/edit.ctp b/app/views/enumerations/edit.ctp new file mode 100644 index 0000000..7baea02 --- /dev/null +++ b/app/views/enumerations/edit.ctp @@ -0,0 +1,10 @@ +<h2><%=l(:label_enumerations)%></h2> + +<% form_tag({:action => 'update', :id => @enumeration}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_save) %> +<% end %> + +<% form_tag({:action => 'destroy', :id => @enumeration}) do %> + <%= submit_tag l(:button_delete) %> +<% end %> \ No newline at end of file diff --git a/app/views/enumerations/list.ctp b/app/views/enumerations/list.ctp new file mode 100644 index 0000000..7f3886b --- /dev/null +++ b/app/views/enumerations/list.ctp @@ -0,0 +1,31 @@ +<h2><%=l(:label_enumerations)%></h2> + +<% Enumeration::OPTIONS.each do |option, params| %> +<h3><%= l(params[:label]) %></h3> + +<% enumerations = Enumeration.get_values(option) %> +<% if enumerations.any? %> +<table class="list"> +<% enumerations.each do |enumeration| %> +<tr class="<%= cycle('odd', 'even') %>"> + <td><%= link_to h(enumeration), :action => 'edit', :id => enumeration %></td> + <td style="width:15%;"><%= image_tag('true.png') if enumeration.is_default? %></td> + <td style="width:15%;"> + <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => enumeration, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> + <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => enumeration, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> - + <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => enumeration, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %> + <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => enumeration, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %> + </td> + <td align="center" style="width:10%;"> + <%= link_to l(:button_delete), { :action => 'destroy', :id => enumeration }, :method => :post, :confirm => l(:text_are_you_sure), :class => "icon icon-del" %> + </td> +</tr> +<% end %> +</table> +<% reset_cycle %> +<% end %> + +<p><%= link_to l(:label_enumeration_new), { :action => 'new', :opt => option } %></p> +<% end %> + +<% html_title(l(:label_enumerations)) -%> diff --git a/app/views/enumerations/new.ctp b/app/views/enumerations/new.ctp new file mode 100644 index 0000000..5c2ccd1 --- /dev/null +++ b/app/views/enumerations/new.ctp @@ -0,0 +1,6 @@ +<h2><%= l(@enumeration.option_name) %>: <%=l(:label_enumeration_new)%></h2> + +<% form_tag({:action => 'create'}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_create) %> +<% end %> diff --git a/app/views/errors/empty b/app/views/errors/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/views/helpers/candy.php b/app/views/helpers/candy.php new file mode 100755 index 0000000..8c8de5e --- /dev/null +++ b/app/views/helpers/candy.php @@ -0,0 +1,628 @@ +<?php +class CandyHelper extends AppHelper +{ + var $helpers = array('Html'); + function link($user){ + return $this->Html->link($user['name'],'/account/show/'.$user['id']); + } + function accesskey($key) + { + $map = array( + 'quick_search' => 'f', + 'search' => 4, + ); + return $map[$key]; + } +} + +#require 'coderay' +#require 'coderay/helpers/file_type' +#require 'forwardable' +#require 'cgi' +# +#module ApplicationHelper +# include Redmine::WikiFormatting::Macros::Definitions +# include GravatarHelper::PublicMethods +# +# extend Forwardable +# def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter +# +# def current_role +# @current_role ||= User.current.role_for_project(@project) +# end +# +# # Return true if user is authorized for controller/action, otherwise false +# def authorize_for(controller, action) +# User.current.allowed_to?({:controller => controller, :action => action}, @project) +# end +# +# # Display a link if user is authorized +# def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference) +# link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) +# end +# +# # Display a link to remote if user is authorized +# def link_to_remote_if_authorized(name, options = {}, html_options = nil) +# url = options[:url] || {} +# link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action]) +# end +# +# # Display a link to user's account page +# def link_to_user(user, options={}) +# (user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous' +# end +# +# def link_to_issue(issue, options={}) +# options[:class] ||= '' +# options[:class] << ' issue' +# options[:class] << ' closed' if issue.closed? +# link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options +# end +# +# # Generates a link to an attachment. +# # Options: +# # * :text - Link text (default to attachment filename) +# # * :download - Force download (default: false) +# def link_to_attachment(attachment, options={}) +# text = options.delete(:text) || attachment.filename +# action = options.delete(:download) ? 'download' : 'show' +# +# link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options) +# end +# +# def toggle_link(name, id, options={}) +# onclick = "Element.toggle('#{id}'); " +# onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ") +# onclick << "return false;" +# link_to(name, "#", :onclick => onclick) +# end +# +# def image_to_function(name, function, html_options = {}) +# html_options.symbolize_keys! +# tag(:input, html_options.merge({ +# :type => "image", :src => image_path(name), +# :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" +# })) +# end +# +# def prompt_to_remote(name, text, param, url, html_options = {}) +# html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;" +# link_to name, {}, html_options +# end +# +# def format_date(date) +# return nil unless date +# # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed) +# @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format) +# date.strftime(@date_format) +# end +# +# def format_time(time, include_date = true) +# return nil unless time +# time = time.to_time if time.is_a?(String) +# zone = User.current.time_zone +# local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time) +# @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format) +# @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format) +# include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format) +# end +# +# def format_activity_title(text) +# h(truncate_single_line(text, 100)) +# end +# +# def format_activity_day(date) +# date == Date.today ? l(:label_today).titleize : format_date(date) +# end +# +# def format_activity_description(text) +# h(truncate(text.to_s, 250).gsub(%r{<(pre|code)>.*$}m, '...')) +# end +# +# def distance_of_date_in_words(from_date, to_date = 0) +# from_date = from_date.to_date if from_date.respond_to?(:to_date) +# to_date = to_date.to_date if to_date.respond_to?(:to_date) +# distance_in_days = (to_date - from_date).abs +# lwr(:actionview_datehelper_time_in_words_day, distance_in_days) +# end +# +# def due_date_distance_in_words(date) +# if date +# l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date)) +# end +# end +# +# def render_page_hierarchy(pages, node=nil) +# content = '' +# if pages[node] +# content << "<ul class=\"pages-hierarchy\">\n" +# pages[node].each do |page| +# content << "<li>" +# content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title}, +# :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil)) +# content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id] +# content << "</li>\n" +# end +# content << "</ul>\n" +# end +# content +# end +# +# # Renders flash messages +# def render_flash_messages +# s = '' +# flash.each do |k,v| +# s << content_tag('div', v, :class => "flash #{k}") +# end +# s +# end +# +# # Truncates and returns the string as a single line +# def truncate_single_line(string, *args) +# truncate(string, *args).gsub(%r{[\r\n]+}m, ' ') +# end +# +# def html_hours(text) +# text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>') +# end +# +# def authoring(created, author, options={}) +# time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) : +# link_to(distance_of_time_in_words(Time.now, created), +# {:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date}, +# :title => format_time(created)) +# author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous') +# l(options[:label] || :label_added_time_by, author_tag, time_tag) +# end +# +# def l_or_humanize(s, options={}) +# k = "#{options[:prefix]}#{s}".to_sym +# l_has_string?(k) ? l(k) : s.to_s.humanize +# end +# +# def day_name(day) +# l(:general_day_names).split(',')[day-1] +# end +# +# def month_name(month) +# l(:actionview_datehelper_select_month_names).split(',')[month-1] +# end +# +# def syntax_highlight(name, content) +# type = CodeRay::FileType[name] +# type ? CodeRay.scan(content, type).html : h(content) +# end +# +# def to_path_param(path) +# path.to_s.split(%r{[/\\]}).select {|p| !p.blank?} +# end +# +# def pagination_links_full(paginator, count=nil, options={}) +# page_param = options.delete(:page_param) || :page +# url_param = params.dup +# # don't reuse params if filters are present +# url_param.clear if url_param.has_key?(:set_filter) +# +# html = '' +# html << link_to_remote(('&#171; ' + l(:label_previous)), +# {:update => 'content', +# :url => url_param.merge(page_param => paginator.current.previous), +# :complete => 'window.scrollTo(0,0)'}, +# {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous +# +# html << (pagination_links_each(paginator, options) do |n| +# link_to_remote(n.to_s, +# {:url => {:params => url_param.merge(page_param => n)}, +# :update => 'content', +# :complete => 'window.scrollTo(0,0)'}, +# {:href => url_for(:params => url_param.merge(page_param => n))}) +# end || '') +# +# html << ' ' + link_to_remote((l(:label_next) + ' &#187;'), +# {:update => 'content', +# :url => url_param.merge(page_param => paginator.current.next), +# :complete => 'window.scrollTo(0,0)'}, +# {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next +# +# unless count.nil? +# html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ') +# end +# +# html +# end +# +# def per_page_links(selected=nil) +# url_param = params.dup +# url_param.clear if url_param.has_key?(:set_filter) +# +# links = Setting.per_page_options_array.collect do |n| +# n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)}, +# {:href => url_for(url_param.merge(:per_page => n))}) +# end +# links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil +# end +# +# def breadcrumb(*args) +# elements = args.flatten +# elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil +# end +# +# def html_title(*args) +# if args.empty? +# title = [] +# title << @project.name if @project +# title += @html_title if @html_title +# title << Setting.app_title +# title.compact.join(' - ') +# else +# @html_title ||= [] +# @html_title += args +# end +# end +# +# def accesskey(s) +# Redmine::AccessKeys.key_for s +# end +# +# # Formats text according to system settings. +# # 2 ways to call this method: +# # * with a String: textilizable(text, options) +# # * with an object and one of its attribute: textilizable(issue, :description, options) +# def textilizable(*args) +# options = args.last.is_a?(Hash) ? args.pop : {} +# case args.size +# when 1 +# obj = options[:object] +# text = args.shift +# when 2 +# obj = args.shift +# text = obj.send(args.shift).to_s +# else +# raise ArgumentError, 'invalid arguments to textilizable' +# end +# return '' if text.blank? +# +# only_path = options.delete(:only_path) == false ? false : true +# +# # when using an image link, try to use an attachment, if possible +# attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) +# +# if attachments +# attachments = attachments.sort_by(&:created_on).reverse +# text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m| +# style = $1 +# filename = $6.downcase +# # search for the picture in attachments +# if found = attachments.detect { |att| att.filename.downcase == filename } +# image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found +# desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1") +# alt = desc.blank? ? nil : "(#{desc})" +# "!#{style}#{image_url}#{alt}!" +# else +# m +# end +# end +# end +# +# text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) } +# +# # different methods for formatting wiki links +# case options[:wiki_links] +# when :local +# # used for local links to html files +# format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" } +# when :anchor +# # used for single-file wiki export +# format_wiki_link = Proc.new {|project, title, anchor| "##{title}" } +# else +# format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) } +# end +# +# project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) +# +# # Wiki links +# # +# # Examples: +# # [[mypage]] +# # [[mypage|mytext]] +# # wiki links can refer other project wikis, using project name or identifier: +# # [[project:]] -> wiki starting page +# # [[project:|mytext]] +# # [[project:mypage]] +# # [[project:mypage|mytext]] +# text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m| +# link_project = project +# esc, all, page, title = $1, $2, $3, $5 +# if esc.nil? +# if page =~ /^([^\:]+)\:(.*)$/ +# link_project = Project.find_by_name($1) || Project.find_by_identifier($1) +# page = $2 +# title ||= $1 if page.blank? +# end +# +# if link_project && link_project.wiki +# # extract anchor +# anchor = nil +# if page =~ /^(.+?)\#(.+)$/ +# page, anchor = $1, $2 +# end +# # check if page exists +# wiki_page = link_project.wiki.find_page(page) +# link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor), +# :class => ('wiki-page' + (wiki_page ? '' : ' new'))) +# else +# # project or wiki doesn't exist +# title || page +# end +# else +# all +# end +# end +# +# # Redmine links +# # +# # Examples: +# # Issues: +# # #52 -> Link to issue #52 +# # Changesets: +# # r52 -> Link to revision 52 +# # commit:a85130f -> Link to scmid starting with a85130f +# # Documents: +# # document#17 -> Link to document with id 17 +# # document:Greetings -> Link to the document with title "Greetings" +# # document:"Some document" -> Link to the document with title "Some document" +# # Versions: +# # version#3 -> Link to version with id 3 +# # version:1.0.0 -> Link to version named "1.0.0" +# # version:"1.0 beta 2" -> Link to version named "1.0 beta 2" +# # Attachments: +# # attachment:file.zip -> Link to the attachment of the current object named file.zip +# # Source files: +# # source:some/file -> Link to the file located at /some/file in the project's repository +# # source:some/file@52 -> Link to the file's revision 52 +# # source:some/file#L120 -> Link to line 120 of the file +# # source:some/file@52#L120 -> Link to line 120 of the file's revision 52 +# # export:some/file -> Force the download of the file +# # Forum messages: +# # message#1218 -> Link to message with id 1218 +# text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m| +# leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8 +# link = nil +# if esc.nil? +# if prefix.nil? && sep == 'r' +# if project && (changeset = project.changesets.find_by_revision(oid)) +# link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid}, +# :class => 'changeset', +# :title => truncate_single_line(changeset.comments, 100)) +# end +# elsif sep == '#' +# oid = oid.to_i +# case prefix +# when nil +# if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current)) +# link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid}, +# :class => (issue.closed? ? 'issue closed' : 'issue'), +# :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})") +# link = content_tag('del', link) if issue.closed? +# end +# when 'document' +# if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current)) +# link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, +# :class => 'document' +# end +# when 'version' +# if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current)) +# link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, +# :class => 'version' +# end +# when 'message' +# if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current)) +# link = link_to h(truncate(message.subject, 60)), {:only_path => only_path, +# :controller => 'messages', +# :action => 'show', +# :board_id => message.board, +# :id => message.root, +# :anchor => (message.parent ? "message-#{message.id}" : nil)}, +# :class => 'message' +# end +# end +# elsif sep == ':' +# # removes the double quotes if any +# name = oid.gsub(%r{^"(.*)"$}, "\\1") +# case prefix +# when 'document' +# if project && document = project.documents.find_by_title(name) +# link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, +# :class => 'document' +# end +# when 'version' +# if project && version = project.versions.find_by_name(name) +# link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, +# :class => 'version' +# end +# when 'commit' +# if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"])) +# link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision}, +# :class => 'changeset', +# :title => truncate_single_line(changeset.comments, 100) +# end +# when 'source', 'export' +# if project && project.repository +# name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$} +# path, rev, anchor = $1, $3, $5 +# link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, +# :path => to_path_param(path), +# :rev => rev, +# :anchor => anchor, +# :format => (prefix == 'export' ? 'raw' : nil)}, +# :class => (prefix == 'export' ? 'source download' : 'source') +# end +# when 'attachment' +# if attachments && attachment = attachments.detect {|a| a.filename == name } +# link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment}, +# :class => 'attachment' +# end +# end +# end +# end +# leading + (link || "#{prefix}#{sep}#{oid}") +# end +# +# text +# end +# +# # Same as Rails' simple_format helper without using paragraphs +# def simple_format_without_paragraph(text) +# text.to_s. +# gsub(/\r\n?/, "\n"). # \r\n and \r -> \n +# gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br +# gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br +# end +# +# def error_messages_for(object_name, options = {}) +# options = options.symbolize_keys +# object = instance_variable_get("@#{object_name}") +# if object && !object.errors.empty? +# # build full_messages here with controller current language +# full_messages = [] +# object.errors.each do |attr, msg| +# next if msg.nil? +# msg = msg.first if msg.is_a? Array +# if attr == "base" +# full_messages << l(msg) +# else +# full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values" +# end +# end +# # retrieve custom values error messages +# if object.errors[:custom_values] +# object.custom_values.each do |v| +# v.errors.each do |attr, msg| +# next if msg.nil? +# msg = msg.first if msg.is_a? Array +# full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg) +# end +# end +# end +# content_tag("div", +# content_tag( +# options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":" +# ) + +# content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }), +# "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation" +# ) +# else +# "" +# end +# end +# +# def lang_options_for_select(blank=true) +# (blank ? [["(auto)", ""]] : []) + +# GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last } +# end +# +# def label_tag_for(name, option_tags = nil, options = {}) +# label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "") +# content_tag("label", label_text) +# end +# +# def labelled_tabular_form_for(name, object, options, &proc) +# options[:html] ||= {} +# options[:html][:class] = 'tabular' unless options[:html].has_key?(:class) +# form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc) +# end +# +# def back_url_hidden_field_tag +# back_url = params[:back_url] || request.env['HTTP_REFERER'] +# back_url = CGI.unescape(back_url.to_s) +# hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank? +# end +# +# def check_all_links(form_name) +# link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + +# " | " + +# link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") +# end +# +# def progress_bar(pcts, options={}) +# pcts = [pcts, pcts] unless pcts.is_a?(Array) +# pcts[1] = pcts[1] - pcts[0] +# pcts << (100 - pcts[1] - pcts[0]) +# width = options[:width] || '100px;' +# legend = options[:legend] || '' +# content_tag('table', +# content_tag('tr', +# (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') + +# (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') + +# (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '') +# ), :class => 'progress', :style => "width: #{width};") + +# content_tag('p', legend, :class => 'pourcent') +# end +# +# def context_menu_link(name, url, options={}) +# options[:class] ||= '' +# if options.delete(:selected) +# options[:class] << ' icon-checked disabled' +# options[:disabled] = true +# end +# if options.delete(:disabled) +# options.delete(:method) +# options.delete(:confirm) +# options.delete(:onclick) +# options[:class] << ' disabled' +# url = '#' +# end +# link_to name, url, options +# end +# +# def calendar_for(field_id) +# include_calendar_headers_tags +# image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) + +# javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });") +# end +# +# def include_calendar_headers_tags +# unless @calendar_headers_tags_included +# @calendar_headers_tags_included = true +# content_for :header_tags do +# javascript_include_tag('calendar/calendar') + +# javascript_include_tag("calendar/lang/calendar-#{current_language}.js") + +# javascript_include_tag('calendar/calendar-setup') + +# stylesheet_link_tag('calendar') +# end +# end +# end +# +# def content_for(name, content = nil, &block) +# @has_content ||= {} +# @has_content[name] = true +# super(name, content, &block) +# end +# +# def has_content?(name) +# (@has_content && @has_content[name]) || false +# end +# +# # Returns the avatar image tag for the given +user+ if avatars are enabled +# # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>') +# def avatar(user, options = { }) +# if Setting.gravatar_enabled? +# email = nil +# if user.respond_to?(:mail) +# email = user.mail +# elsif user.to_s =~ %r{<(.+?)>} +# email = $1 +# end +# return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil +# end +# end +# +# private +# +# def wiki_helper +# helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) +# extend helper +# return self +# end +#end +# \ No newline at end of file diff --git a/app/views/helpers/empty b/app/views/helpers/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/views/issue_categories/_form.ctp b/app/views/issue_categories/_form.ctp new file mode 100644 index 0000000..dc62c20 --- /dev/null +++ b/app/views/issue_categories/_form.ctp @@ -0,0 +1,6 @@ +<%= error_messages_for 'category' %> + +<div class="box"> +<p><%= f.text_field :name, :size => 30, :required => true %></p> +<p><%= f.select :assigned_to_id, @project.users.collect{|u| [u.name, u.id]}, :include_blank => true %></p> +</div> diff --git a/app/views/issue_categories/destroy.ctp b/app/views/issue_categories/destroy.ctp new file mode 100644 index 0000000..2b61810 --- /dev/null +++ b/app/views/issue_categories/destroy.ctp @@ -0,0 +1,15 @@ +<h2><%=l(:label_issue_category)%>: <%=h @category.name %></h2> + +<% form_tag({}) do %> +<div class="box"> +<p><strong><%= l(:text_issue_category_destroy_question, @issue_count) %></strong></p> +<p><label><%= radio_button_tag 'todo', 'nullify', true %> <%= l(:text_issue_category_destroy_assignments) %></label><br /> +<% if @categories.size > 0 %> +<label><%= radio_button_tag 'todo', 'reassign', false %> <%= l(:text_issue_category_reassign_to) %></label>: +<%= select_tag 'reassign_to_id', options_from_collection_for_select(@categories, 'id', 'name') %></p> +<% end %> +</div> + +<%= submit_tag l(:button_apply) %> +<%= link_to l(:button_cancel), :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' %> +<% end %> diff --git a/app/views/issue_categories/edit.ctp b/app/views/issue_categories/edit.ctp new file mode 100644 index 0000000..bc62779 --- /dev/null +++ b/app/views/issue_categories/edit.ctp @@ -0,0 +1,6 @@ +<h2><%=l(:label_issue_category)%></h2> + +<% labelled_tabular_form_for :category, @category, :url => { :action => 'edit', :id => @category } do |f| %> +<%= render :partial => 'issue_categories/form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/issue_relations/_form.ctp b/app/views/issue_relations/_form.ctp new file mode 100644 index 0000000..0de3863 --- /dev/null +++ b/app/views/issue_relations/_form.ctp @@ -0,0 +1,12 @@ +<%= error_messages_for 'relation' %> + +<p><%= f.select :relation_type, collection_for_relation_type_select, {}, :onchange => "setPredecessorFieldsVisibility();" %> +<%= l(:label_issue) %> #<%= f.text_field :issue_to_id, :size => 6 %> +<span id="predecessor_fields" style="display:none;"> +<%= l(:field_delay) %>: <%= f.text_field :delay, :size => 3 %> <%= l(:label_day_plural) %> +</span> +<%= submit_tag l(:button_add) %> +<%= toggle_link l(:button_cancel), 'new-relation-form'%> +</p> + +<%= javascript_tag "setPredecessorFieldsVisibility();" %> diff --git a/app/views/issue_statuses/_form.ctp b/app/views/issue_statuses/_form.ctp new file mode 100644 index 0000000..6ae0a7c --- /dev/null +++ b/app/views/issue_statuses/_form.ctp @@ -0,0 +1,15 @@ +<%= error_messages_for 'issue_status' %> + +<div class="box"> +<!--[form:issue_status]--> +<p><label for="issue_status_name"><%=l(:field_name)%><span class="required"> *</span></label> +<%= text_field 'issue_status', 'name' %></p> + +<p><label for="issue_status_is_closed"><%=l(:field_is_closed)%></label> +<%= check_box 'issue_status', 'is_closed' %></p> + +<p><label for="issue_status_is_default"><%=l(:field_is_default)%></label> +<%= check_box 'issue_status', 'is_default' %></p> + +<!--[eoform:issue_status]--> +</div> \ No newline at end of file diff --git a/app/views/issue_statuses/edit.ctp b/app/views/issue_statuses/edit.ctp new file mode 100644 index 0000000..b81426a --- /dev/null +++ b/app/views/issue_statuses/edit.ctp @@ -0,0 +1,6 @@ +<h2><%=l(:label_issue_status)%></h2> + +<% form_tag({:action => 'update', :id => @issue_status}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/issue_statuses/list.ctp b/app/views/issue_statuses/list.ctp new file mode 100644 index 0000000..e359118 --- /dev/null +++ b/app/views/issue_statuses/list.ctp @@ -0,0 +1,37 @@ +<div class="contextual"> +<%= link_to l(:label_issue_status_new), {:action => 'new'}, :class => 'icon icon-add' %> +</div> + +<h2><%=l(:label_issue_status_plural)%></h2> + +<table class="list"> + <thead><tr> + <th><%=l(:field_status)%></th> + <th><%=l(:field_is_default)%></th> + <th><%=l(:field_is_closed)%></th> + <th><%=l(:button_sort)%></th> + <th></th> + </tr></thead> + <tbody> +<% for status in @issue_statuses %> + <tr class="<%= cycle("odd", "even") %>"> + <td><%= link_to status.name, :action => 'edit', :id => status %></td> + <td align="center"><%= image_tag 'true.png' if status.is_default? %></td> + <td align="center"><%= image_tag 'true.png' if status.is_closed? %></td> + <td align="center" style="width:15%;"> + <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => status, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> + <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => status, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> - + <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => status, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %> + <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => status, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %> + </td> + <td align="center" style="width:10%;"> + <%= button_to l(:button_delete), { :action => 'destroy', :id => status }, :confirm => l(:text_are_you_sure), :class => "button-small" %> + </td> + </tr> +<% end %> + </tbody> +</table> + +<p class="pagination"><%= pagination_links_full @issue_status_pages %></p> + +<% html_title(l(:label_issue_status_plural)) -%> diff --git a/app/views/issue_statuses/new.ctp b/app/views/issue_statuses/new.ctp new file mode 100644 index 0000000..ede1699 --- /dev/null +++ b/app/views/issue_statuses/new.ctp @@ -0,0 +1,6 @@ +<h2><%=l(:label_issue_status_new)%></h2> + +<% form_tag({:action => 'create'}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_create) %> +<% end %> diff --git a/app/views/issues/_changesets.ctp b/app/views/issues/_changesets.ctp new file mode 100644 index 0000000..15b75c6 --- /dev/null +++ b/app/views/issues/_changesets.ctp @@ -0,0 +1,8 @@ +<% changesets.each do |changeset| %> + <div class="changeset <%= cycle('odd', 'even') %>"> + <p><%= link_to("#{l(:label_revision)} #{changeset.revision}", + :controller => 'repositories', :action => 'revision', :id => @project, :rev => changeset.revision) %><br /> + <span class="author"><%= authoring(changeset.committed_on, changeset.author) %></span></p> + <%= textilizable(changeset, :comments) %> + </div> +<% end %> diff --git a/app/views/issues/_edit.ctp b/app/views/issues/_edit.ctp new file mode 100644 index 0000000..413f217 --- /dev/null +++ b/app/views/issues/_edit.ctp @@ -0,0 +1,55 @@ +<% labelled_tabular_form_for :issue, @issue, + :url => {:action => 'edit', :id => @issue}, + :html => {:id => 'issue-form', + :class => nil, + :multipart => true} do |f| %> + <%= error_messages_for 'issue' %> + <%= error_messages_for 'time_entry' %> + <div class="box"> + <% if @edit_allowed || !@allowed_statuses.empty? %> + <fieldset class="tabular"><legend><%= l(:label_change_properties) %> + <% if !@issue.new_record? && !@issue.errors.any? && @edit_allowed %> + <small>(<%= link_to l(:label_more), {}, :onclick => 'Effect.toggle("issue_descr_fields", "appear", {duration:0.3}); return false;' %>)</small> + <% end %> + </legend> + <%= render :partial => (@edit_allowed ? 'form' : 'form_update'), :locals => {:f => f} %> + </fieldset> + <% end %> + <% if authorize_for('timelog', 'edit') %> + <fieldset class="tabular"><legend><%= l(:button_log_time) %></legend> + <% fields_for :time_entry, @time_entry, { :builder => TabularFormBuilder, :lang => current_language} do |time_entry| %> + <div class="splitcontentleft"> + <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p> + </div> + <div class="splitcontentright"> + <p><%= time_entry.select :activity_id, activity_collection_for_select_options %></p> + </div> + <p><%= time_entry.text_field :comments, :size => 60 %></p> + <% @time_entry.custom_field_values.each do |value| %> + <p><%= custom_field_tag_with_label :time_entry, value %></p> + <% end %> + <% end %> + </fieldset> + <% end %> + + <fieldset><legend><%= l(:field_notes) %></legend> + <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %> + <%= wikitoolbar_for 'notes' %> + <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %> + + <p><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form' %></p> + </fieldset> + </div> + + <%= f.hidden_field :lock_version %> + <%= submit_tag l(:button_submit) %> + <%= link_to_remote l(:label_preview), + { :url => { :controller => 'issues', :action => 'preview', :project_id => @project, :id => @issue }, + :method => 'post', + :update => 'preview', + :with => 'Form.serialize("issue-form")', + :complete => "Element.scrollTo('preview')" + }, :accesskey => accesskey(:preview) %> +<% end %> + +<div id="preview" class="wiki"></div> diff --git a/app/views/issues/_form.ctp b/app/views/issues/_form.ctp new file mode 100644 index 0000000..b447190 --- /dev/null +++ b/app/views/issues/_form.ctp @@ -0,0 +1,63 @@ +<% if @issue.new_record? %> +<p><%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p> +<%= observe_field :issue_tracker_id, :url => { :action => :new }, + :update => :content, + :with => "Form.serialize('issue-form')" %> +<hr /> +<% end %> + +<div id="issue_descr_fields" <%= 'style="display:none"' unless @issue.new_record? || @issue.errors.any? %>> +<p><%= f.text_field :subject, :size => 80, :required => true %></p> +<p><%= f.text_area :description, + :cols => 60, + :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min), + :accesskey => accesskey(:edit), + :class => 'wiki-edit' %></p> +</div> + +<div class="splitcontentleft"> +<% if @issue.new_record? || @allowed_statuses.any? %> +<p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p> +<% else %> +<p><label><%= l(:field_status) %></label> <%= @issue.status.name %></p> +<% end %> + +<p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %></p> +<p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p> +<% unless @project.issue_categories.empty? %> +<p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %> +<%= prompt_to_remote(l(:label_issue_category_new), + l(:label_issue_category_new), 'category[name]', + {:controller => 'projects', :action => 'add_issue_category', :id => @project}, + :class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %></p> +<% end %> +<%= content_tag('p', f.select(:fixed_version_id, + (@project.versions.sort.collect {|v| [v.name, v.id]}), + { :include_blank => true })) unless @project.versions.empty? %> +</div> + +<div class="splitcontentright"> +<p><%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %></p> +<p><%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %></p> +<p><%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %></p> +<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p> +</div> + +<div style="clear:both;"> </div> +<%= render :partial => 'form_custom_fields' %> + +<% if @issue.new_record? %> +<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p> +<% end %> + +<% if @issue.new_record? && User.current.allowed_to?(:add_issue_watchers, @project) -%> +<p><label><%= l(:label_issue_watchers) %></label> +<% @issue.project.users.sort.each do |user| -%> +<label class="floating"><%= check_box_tag 'issue[watcher_user_ids][]', user.id, @issue.watcher_user_ids.include?(user.id) %> <%=h user %></label> +<% end -%> +</p> +<% end %> + +<%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %> + +<%= wikitoolbar_for 'issue_description' %> diff --git a/app/views/issues/_form_custom_fields.ctp b/app/views/issues/_form_custom_fields.ctp new file mode 100644 index 0000000..752fb4d --- /dev/null +++ b/app/views/issues/_form_custom_fields.ctp @@ -0,0 +1,12 @@ +<div class="splitcontentleft"> +<% i = 0 %> +<% split_on = @issue.custom_field_values.size / 2 %> +<% @issue.custom_field_values.each do |value| %> + <p><%= custom_field_tag_with_label :issue, value %></p> +<% if i == split_on -%> +</div><div class="splitcontentright"> +<% end -%> +<% i += 1 -%> +<% end -%> +</div> +<div style="clear:both;"> </div> diff --git a/app/views/issues/_form_update.ctp b/app/views/issues/_form_update.ctp new file mode 100644 index 0000000..25e81a7 --- /dev/null +++ b/app/views/issues/_form_update.ctp @@ -0,0 +1,10 @@ +<div class="splitcontentleft"> +<p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p> +<p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p> +</div> +<div class="splitcontentright"> +<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p> +<%= content_tag('p', f.select(:fixed_version_id, + (@project.versions.sort.collect {|v| [v.name, v.id]}), + { :include_blank => true })) unless @project.versions.empty? %> +</div> diff --git a/app/views/issues/_history.ctp b/app/views/issues/_history.ctp new file mode 100644 index 0000000..267263b --- /dev/null +++ b/app/views/issues/_history.ctp @@ -0,0 +1,16 @@ +<% reply_links = authorize_for('issues', 'edit') -%> +<% for journal in journals %> + <div id="change-<%= journal.id %>" class="journal"> + <h4><div style="float:right;"><%= link_to "##{journal.indice}", :anchor => "note-#{journal.indice}" %></div> + <%= content_tag('a', '', :name => "note-#{journal.indice}")%> + <%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %></h4> + <%= avatar(journal.user, :size => "32") %> + <ul> + <% for detail in journal.details %> + <li><%= show_detail(detail) %></li> + <% end %> + </ul> + <%= render_notes(journal, :reply_links => reply_links) unless journal.notes.blank? %> + </div> + <%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %> +<% end %> diff --git a/app/views/issues/_list.ctp b/app/views/issues/_list.ctp new file mode 100644 index 0000000..9326760 --- /dev/null +++ b/app/views/issues/_list.ctp @@ -0,0 +1,22 @@ +<% form_tag({}) do -%> +<table class="list issues"> + <thead><tr> + <th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;', + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> + </th> + <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %> + <% query.columns.each do |column| %> + <%= column_header(column) %> + <% end %> + </tr></thead> + <tbody> + <% issues.each do |issue| -%> + <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= css_issue_classes(issue) %>"> + <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td> + <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td> + <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %> + </tr> + <% end -%> + </tbody> +</table> +<% end -%> diff --git a/app/views/issues/_list_simple.ctp b/app/views/issues/_list_simple.ctp new file mode 100644 index 0000000..e401a4a --- /dev/null +++ b/app/views/issues/_list_simple.ctp @@ -0,0 +1,28 @@ +<% if issues && issues.any? %> +<% form_tag({}) do %> + <table class="list issues"> + <thead><tr> + <th>#</th> + <th><%=l(:field_tracker)%></th> + <th><%=l(:field_subject)%></th> + </tr></thead> + <tbody> + <% for issue in issues %> + <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= css_issue_classes(issue) %>"> + <td class="id"> + <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %> + <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %> + </td> + <td><%=h issue.project.name %> - <%= issue.tracker.name %><br /> + <%= issue.status.name %> - <%= format_time(issue.updated_on) %></td> + <td class="subject"> + <%= link_to h(issue.subject), :controller => 'issues', :action => 'show', :id => issue %> + </td> + </tr> + <% end %> + </tbody> + </table> +<% end %> +<% else %> + <p class="nodata"><%= l(:label_no_data) %></p> +<% end %> diff --git a/app/views/issues/_relations.ctp b/app/views/issues/_relations.ctp new file mode 100644 index 0000000..7139210 --- /dev/null +++ b/app/views/issues/_relations.ctp @@ -0,0 +1,32 @@ +<div class="contextual"> +<% if authorize_for('issue_relations', 'new') %> + <%= toggle_link l(:button_add), 'new-relation-form'%> +<% end %> +</div> + +<p><strong><%=l(:label_related_issues)%></strong></p> + +<% if @issue.relations.any? %> +<table style="width:100%"> +<% @issue.relations.each do |relation| %> +<tr> +<td><%= l(relation.label_for(@issue)) %> <%= "(#{lwr(:actionview_datehelper_time_in_words_day, relation.delay)})" if relation.delay && relation.delay != 0 %> + <%= h(relation.other_issue(@issue).project) + ' - ' if Setting.cross_project_issue_relations? %> <%= link_to_issue relation.other_issue(@issue) %></td> +<td><%=h relation.other_issue(@issue).subject %></td> +<td><%= relation.other_issue(@issue).status.name %></td> +<td><%= format_date(relation.other_issue(@issue).start_date) %></td> +<td><%= format_date(relation.other_issue(@issue).due_date) %></td> +<td><%= link_to_remote(image_tag('delete.png'), { :url => {:controller => 'issue_relations', :action => 'destroy', :issue_id => @issue, :id => relation}, + :method => :post + }, :title => l(:label_relation_delete)) if authorize_for('issue_relations', 'destroy') %></td> +</tr> +<% end %> +</table> +<% end %> + +<% remote_form_for(:relation, @relation, + :url => {:controller => 'issue_relations', :action => 'new', :issue_id => @issue}, + :method => :post, + :html => {:id => 'new-relation-form', :style => (@relation ? '' : 'display: none;')}) do |f| %> +<%= render :partial => 'issue_relations/form', :locals => {:f => f}%> +<% end %> diff --git a/app/views/issues/_sidebar.ctp b/app/views/issues/_sidebar.ctp new file mode 100644 index 0000000..bbc00f0 --- /dev/null +++ b/app/views/issues/_sidebar.ctp @@ -0,0 +1,26 @@ +<h3><%= l(:label_issue_plural) %></h3> +<%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %><br /> +<% if @project %> +<%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %><br /> +<%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %><br /> +<% end %> +<%= call_hook(:view_issues_sidebar_issues_bottom) %> + +<% planning_links = [] + planning_links << link_to(l(:label_calendar), :action => 'calendar', :project_id => @project) if User.current.allowed_to?(:view_calendar, @project, :global => true) + planning_links << link_to(l(:label_gantt), :action => 'gantt', :project_id => @project) if User.current.allowed_to?(:view_gantt, @project, :global => true) +%> +<% unless planning_links.empty? %> +<h3><%= l(:label_planning) %></h3> +<p><%= planning_links.join(' | ') %></p> +<%= call_hook(:view_issues_sidebar_planning_bottom) %> +<% end %> + +<% unless sidebar_queries.empty? -%> +<h3><%= l(:label_query_plural) %></h3> + +<% sidebar_queries.each do |query| -%> +<%= link_to(h(query.name), :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query) %><br /> +<% end -%> +<%= call_hook(:view_issues_sidebar_queries_bottom) %> +<% end -%> diff --git a/app/views/issues/bulk_edit.ctp b/app/views/issues/bulk_edit.ctp new file mode 100644 index 0000000..01e4e2e --- /dev/null +++ b/app/views/issues/bulk_edit.ctp @@ -0,0 +1,51 @@ +<h2><%= l(:label_bulk_edit_selected_issues) %></h2> + +<ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul> + +<% form_tag() do %> +<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %> +<div class="box"> +<fieldset> +<legend><%= l(:label_change_properties) %></legend> +<p> +<% if @available_statuses.any? %> +<label><%= l(:field_status) %>: +<%= select_tag('status_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@available_statuses, :id, :name)) %></label> +<% end %> +<label><%= l(:field_priority) %>: +<%= select_tag('priority_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(Enumeration.get_values('IPRI'), :id, :name)) %></label> +<label><%= l(:field_category) %>: +<%= select_tag('category_id', content_tag('option', l(:label_no_change_option), :value => '') + + content_tag('option', l(:label_none), :value => 'none') + + options_from_collection_for_select(@project.issue_categories, :id, :name)) %></label> +</p> +<p> +<label><%= l(:field_assigned_to) %>: +<%= select_tag('assigned_to_id', content_tag('option', l(:label_no_change_option), :value => '') + + content_tag('option', l(:label_nobody), :value => 'none') + + options_from_collection_for_select(@project.assignable_users, :id, :name)) %></label> +<label><%= l(:field_fixed_version) %>: +<%= select_tag('fixed_version_id', content_tag('option', l(:label_no_change_option), :value => '') + + content_tag('option', l(:label_none), :value => 'none') + + options_from_collection_for_select(@project.versions, :id, :name)) %></label> +</p> + +<p> +<label><%= l(:field_start_date) %>: +<%= text_field_tag 'start_date', '', :size => 10 %><%= calendar_for('start_date') %></label> +<label><%= l(:field_due_date) %>: +<%= text_field_tag 'due_date', '', :size => 10 %><%= calendar_for('due_date') %></label> +<label><%= l(:field_done_ratio) %>: +<%= select_tag 'done_ratio', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></label> +</p> +<%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %> +</fieldset> + +<fieldset><legend><%= l(:field_notes) %></legend> +<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %> +<%= wikitoolbar_for 'notes' %> +</fieldset> +</div> + +<p><%= submit_tag l(:button_submit) %> +<% end %> diff --git a/app/views/issues/calendar.ctp b/app/views/issues/calendar.ctp new file mode 100644 index 0000000..0894829 --- /dev/null +++ b/app/views/issues/calendar.ctp @@ -0,0 +1,55 @@ +<% form_tag({}, :id => 'query_form') do %> +<% if @query.new_record? %> + <h2><%= l(:label_calendar) %></h2> + <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend> + <%= render :partial => 'queries/filters', :locals => {:query => @query} %> + </fieldset> +<% else %> + <h2><%=h @query.name %></h2> + <% html_title @query.name %> +<% end %> + +<fieldset id="date-range"><legend><%= l(:label_date_range) %></legend> + <%= select_month(@month, :prefix => "month", :discard_type => true) %> + <%= select_year(@year, :prefix => "year", :discard_type => true) %> +</fieldset> + +<p style="float:right; margin:0px;"> +<%= link_to_remote ('&#171; ' + (@month==1 ? "#{month_name(12)} #{@year-1}" : "#{month_name(@month-1)}")), + {:update => "content", :url => { :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1) }}, + {:href => url_for(:action => 'calendar', :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1))} + %> | +<%= link_to_remote ((@month==12 ? "#{month_name(1)} #{@year+1}" : "#{month_name(@month+1)}") + ' &#187;'), + {:update => "content", :url => { :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1) }}, + {:href => url_for(:action => 'calendar', :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1))} + %> +</p> + +<p class="buttons"> +<%= link_to_remote l(:button_apply), + { :url => { :set_filter => (@query.new_record? ? 1 : nil) }, + :update => "content", + :with => "Form.serialize('query_form')" + }, :class => 'icon icon-checked' %> + +<%= link_to_remote l(:button_clear), + { :url => { :set_filter => (@query.new_record? ? 1 : nil) }, + :update => "content", + }, :class => 'icon icon-reload' if @query.new_record? %> +</p> +<% end %> + +<%= error_messages_for 'query' %> +<% if @query.valid? %> +<%= render :partial => 'common/calendar', :locals => {:calendar => @calendar} %> + +<%= image_tag 'arrow_from.png' %>&nbsp;&nbsp;<%= l(:text_tip_task_begin_day) %><br /> +<%= image_tag 'arrow_to.png' %>&nbsp;&nbsp;<%= l(:text_tip_task_end_day) %><br /> +<%= image_tag 'arrow_bw.png' %>&nbsp;&nbsp;<%= l(:text_tip_task_begin_end_day) %><br /> +<% end %> + +<% content_for :sidebar do %> + <%= render :partial => 'issues/sidebar' %> +<% end %> + +<% html_title(l(:label_calendar)) -%> diff --git a/app/views/issues/changes.rxml b/app/views/issues/changes.rxml new file mode 100644 index 0000000..239d2d6 --- /dev/null +++ b/app/views/issues/changes.rxml @@ -0,0 +1,30 @@ +xml.instruct! +xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do + xml.title @title + xml.link "rel" => "self", "href" => url_for(:format => 'atom', :key => User.current.rss_key, :only_path => false) + xml.link "rel" => "alternate", "href" => home_url(:only_path => false) + xml.id url_for(:controller => 'welcome', :only_path => false) + xml.updated((@journals.first ? @journals.first.event_datetime : Time.now).xmlschema) + xml.author { xml.name "#{Setting.app_title}" } + @journals.each do |change| + issue = change.issue + xml.entry do + xml.title "#{issue.project.name} - #{issue.tracker.name} ##{issue.id}: #{issue.subject}" + xml.link "rel" => "alternate", "href" => url_for(:controller => 'issues' , :action => 'show', :id => issue, :only_path => false) + xml.id url_for(:controller => 'issues' , :action => 'show', :id => issue, :journal_id => change, :only_path => false) + xml.updated change.created_on.xmlschema + xml.author do + xml.name change.user.name + xml.email(change.user.mail) + end + xml.content "type" => "html" do + xml.text! '<ul>' + change.details.each do |detail| + xml.text! '<li>' + show_detail(detail, false) + '</li>' + end + xml.text! '</ul>' + xml.text! textilizable(change.notes) unless change.notes.blank? + end + end + end +end \ No newline at end of file diff --git a/app/views/issues/context_menu.ctp b/app/views/issues/context_menu.ctp new file mode 100644 index 0000000..671655d --- /dev/null +++ b/app/views/issues/context_menu.ctp @@ -0,0 +1,90 @@ +<ul> +<% if !@issue.nil? -%> + <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue}, + :class => 'icon-edit', :disabled => !@can[:edit] %></li> + <li class="folder"> + <a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a> + <ul> + <% @statuses.each do |s| -%> + <li><%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}, :back_to => @back}, :method => :post, + :selected => (s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %></li> + <% end -%> + </ul> + </li> +<% else %> + <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)}, + :class => 'icon-edit', :disabled => !@can[:edit] %></li> +<% end %> + + <li class="folder"> + <a href="#" class="submenu"><%= l(:field_priority) %></a> + <ul> + <% @priorities.each do |p| -%> + <li><%= context_menu_link p.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'priority_id' => p, :back_to => @back}, :method => :post, + :selected => (@issue && p == @issue.priority), :disabled => !@can[:edit] %></li> + <% end -%> + </ul> + </li> + <% unless @project.nil? || @project.versions.empty? -%> + <li class="folder"> + <a href="#" class="submenu"><%= l(:field_fixed_version) %></a> + <ul> + <% @project.versions.sort.each do |v| -%> + <li><%= context_menu_link v.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'fixed_version_id' => v, :back_to => @back}, :method => :post, + :selected => (@issue && v == @issue.fixed_version), :disabled => !@can[:update] %></li> + <% end -%> + <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'fixed_version_id' => 'none', :back_to => @back}, :method => :post, + :selected => (@issue && @issue.fixed_version.nil?), :disabled => !@can[:update] %></li> + </ul> + </li> + <% end %> + <% unless @assignables.nil? || @assignables.empty? -%> + <li class="folder"> + <a href="#" class="submenu"><%= l(:field_assigned_to) %></a> + <ul> + <% @assignables.each do |u| -%> + <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'assigned_to_id' => u, :back_to => @back}, :method => :post, + :selected => (@issue && u == @issue.assigned_to), :disabled => !@can[:update] %></li> + <% end -%> + <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'assigned_to_id' => 'none', :back_to => @back}, :method => :post, + :selected => (@issue && @issue.assigned_to.nil?), :disabled => !@can[:update] %></li> + </ul> + </li> + <% end %> + <% unless @project.nil? || @project.issue_categories.empty? -%> + <li class="folder"> + <a href="#" class="submenu"><%= l(:field_category) %></a> + <ul> + <% @project.issue_categories.each do |u| -%> + <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'category_id' => u, :back_to => @back}, :method => :post, + :selected => (@issue && u == @issue.category), :disabled => !@can[:update] %></li> + <% end -%> + <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'category_id' => 'none', :back_to => @back}, :method => :post, + :selected => (@issue && @issue.category.nil?), :disabled => !@can[:update] %></li> + </ul> + </li> + <% end -%> + <li class="folder"> + <a href="#" class="submenu"><%= l(:field_done_ratio) %></a> + <ul> + <% (0..10).map{|x|x*10}.each do |p| -%> + <li><%= context_menu_link "#{p}%", {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'done_ratio' => p, :back_to => @back}, :method => :post, + :selected => (@issue && p == @issue.done_ratio), :disabled => !@can[:edit] %></li> + <% end -%> + </ul> + </li> + +<% if !@issue.nil? %> + <li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue}, + :class => 'icon-copy', :disabled => !@can[:copy] %></li> + <% if @can[:log_time] -%> + <li><%= context_menu_link l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, + :class => 'icon-time' %></li> + <% end %> +<% end %> + + <li><%= context_menu_link l(:button_move), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id)}, + :class => 'icon-move', :disabled => !@can[:move] %></li> + <li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id)}, + :method => :post, :confirm => l(:text_issues_destroy_confirmation), :class => 'icon-del', :disabled => !@can[:delete] %></li> +</ul> diff --git a/app/views/issues/destroy.ctp b/app/views/issues/destroy.ctp new file mode 100644 index 0000000..2f3c036 --- /dev/null +++ b/app/views/issues/destroy.ctp @@ -0,0 +1,15 @@ +<h2><%= l(:confirmation) %></h2> + +<% form_tag do %> +<%= @issues.collect {|i| hidden_field_tag 'ids[]', i.id } %> +<div class="box"> +<p><strong><%= l(:text_destroy_time_entries_question, @hours) %></strong></p> +<p> +<label><%= radio_button_tag 'todo', 'destroy', true %> <%= l(:text_destroy_time_entries) %></label><br /> +<label><%= radio_button_tag 'todo', 'nullify', false %> <%= l(:text_assign_time_entries_to_project) %></label><br /> +<label><%= radio_button_tag 'todo', 'reassign', false, :onchange => 'if (this.checked) { $("reassign_to_id").focus(); }' %> <%= l(:text_reassign_time_entries) %></label> +<%= text_field_tag 'reassign_to_id', params[:reassign_to_id], :size => 6, :onfocus => '$("todo_reassign").checked=true;' %> +</p> +</div> +<%= submit_tag l(:button_apply) %> +<% end %> diff --git a/app/views/issues/edit.ctp b/app/views/issues/edit.ctp new file mode 100644 index 0000000..97f26a2 --- /dev/null +++ b/app/views/issues/edit.ctp @@ -0,0 +1,3 @@ +<h2><%=h "#{@issue.tracker.name} ##{@issue.id}" %></h2> + +<%= render :partial => 'edit' %> diff --git a/app/views/issues/gantt.ctp b/app/views/issues/gantt.ctp new file mode 100644 index 0000000..b9af1f9 --- /dev/null +++ b/app/views/issues/gantt.ctp @@ -0,0 +1,257 @@ +<% form_tag(params.merge(:month => nil, :year => nil, :months => nil), :id => 'query_form') do %> +<% if @query.new_record? %> + <h2><%=l(:label_gantt)%></h2> + <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend> + <%= render :partial => 'queries/filters', :locals => {:query => @query} %> + </fieldset> +<% else %> + <h2><%=h @query.name %></h2> + <% html_title @query.name %> +<% end %> + +<fieldset id="date-range"><legend><%= l(:label_date_range) %></legend> + <%= text_field_tag 'months', @gantt.months, :size => 2 %> + <%= l(:label_months_from) %> + <%= select_month(@gantt.month_from, :prefix => "month", :discard_type => true) %> + <%= select_year(@gantt.year_from, :prefix => "year", :discard_type => true) %> + <%= hidden_field_tag 'zoom', @gantt.zoom %> +</fieldset> + +<p style="float:right; margin:0px;"> +<%= if @gantt.zoom < 4 + link_to_remote image_tag('zoom_in.png'), {:url => @gantt.params.merge(:zoom => (@gantt.zoom+1)), :update => 'content'}, {:href => url_for(@gantt.params.merge(:zoom => (@gantt.zoom+1)))} + else + image_tag 'zoom_in_g.png' + end %> +<%= if @gantt.zoom > 1 + link_to_remote image_tag('zoom_out.png'), {:url => @gantt.params.merge(:zoom => (@gantt.zoom-1)), :update => 'content'}, {:href => url_for(@gantt.params.merge(:zoom => (@gantt.zoom-1)))} + else + image_tag 'zoom_out_g.png' + end %> +</p> + +<p class="buttons"> +<%= link_to_remote l(:button_apply), + { :url => { :set_filter => (@query.new_record? ? 1 : nil) }, + :update => "content", + :with => "Form.serialize('query_form')" + }, :class => 'icon icon-checked' %> + +<%= link_to_remote l(:button_clear), + { :url => { :set_filter => (@query.new_record? ? 1 : nil) }, + :update => "content", + }, :class => 'icon icon-reload' if @query.new_record? %> +</p> +<% end %> + +<%= error_messages_for 'query' %> +<% if @query.valid? %> +<% zoom = 1 +@gantt.zoom.times { zoom = zoom * 2 } + +subject_width = 330 +header_heigth = 18 + +headers_height = header_heigth +show_weeks = false +show_days = false + +if @gantt.zoom >1 + show_weeks = true + headers_height = 2*header_heigth + if @gantt.zoom > 2 + show_days = true + headers_height = 3*header_heigth + end +end + +g_width = (@gantt.date_to - @gantt.date_from + 1)*zoom +g_height = [(20 * @gantt.events.length + 6)+150, 206].max +t_height = g_height + headers_height +%> + +<table width="100%" style="border:0; border-collapse: collapse;"> +<tr> +<td style="width:<%= subject_width %>px; padding:0px;"> + +<div style="position:relative;height:<%= t_height + 24 %>px;width:<%= subject_width + 1 %>px;"> +<div style="right:-2px;width:<%= subject_width %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr"></div> +<div style="right:-2px;width:<%= subject_width %>px;height:<%= t_height %>px;border-left: 1px solid #c0c0c0;overflow:hidden;" class="gantt_hdr"></div> +<% +# +# Tasks subjects +# +top = headers_height + 8 +@gantt.events.each do |i| %> + <div style="position: absolute;line-height:1.2em;height:16px;top:<%= top %>px;left:4px;overflow:hidden;"><small> + <% if i.is_a? Issue %> + <%= h("#{i.project} -") unless @project && @project == i.project %> + <%= link_to_issue i %>: <%=h i.subject %> + <% else %> + <span class="icon icon-package"> + <%= h("#{i.project} -") unless @project && @project == i.project %> + <%= link_to_version i %> + </span> + <% end %> + </small></div> + <% top = top + 20 +end %> +</div> +</td> +<td> + +<div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;"> +<div style="width:<%= g_width-1 %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr">&nbsp;</div> +<% +# +# Months headers +# +month_f = @gantt.date_from +left = 0 +height = (show_weeks ? header_heigth : header_heigth + g_height) +@gantt.months.times do + width = ((month_f >> 1) - month_f) * zoom - 1 + %> + <div style="left:<%= left %>px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr"> + <%= link_to "#{month_f.year}-#{month_f.month}", @gantt.params.merge(:year => month_f.year, :month => month_f.month), :title => "#{month_name(month_f.month)} #{month_f.year}"%> + </div> + <% + left = left + width + 1 + month_f = month_f >> 1 +end %> + +<% +# +# Weeks headers +# +if show_weeks + left = 0 + height = (show_days ? header_heigth-1 : header_heigth-1 + g_height) + if @gantt.date_from.cwday == 1 + # @date_from is monday + week_f = @gantt.date_from + else + # find next monday after @date_from + week_f = @gantt.date_from + (7 - @gantt.date_from.cwday + 1) + width = (7 - @gantt.date_from.cwday + 1) * zoom-1 + %> + <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">&nbsp;</div> + <% + left = left + width+1 + end %> + <% + while week_f <= @gantt.date_to + width = (week_f + 6 <= @gantt.date_to) ? 7 * zoom -1 : (@gantt.date_to - week_f + 1) * zoom-1 + %> + <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr"> + <small><%= week_f.cweek if width >= 16 %></small> + </div> + <% + left = left + width+1 + week_f = week_f+7 + end +end %> + +<% +# +# Days headers +# +if show_days + left = 0 + height = g_height + header_heigth - 1 + wday = @gantt.date_from.cwday + (@gantt.date_to - @gantt.date_from + 1).to_i.times do + width = zoom - 1 + %> + <div style="left:<%= left %>px;top:37px;width:<%= width %>px;height:<%= height %>px;font-size:0.7em;<%= "background:#f1f1f1;" if wday > 5 %>" class="gantt_hdr"> + <%= day_name(wday).first %> + </div> + <% + left = left + width+1 + wday = wday + 1 + wday = 1 if wday > 7 + end +end %> + +<% +# +# Tasks +# +top = headers_height + 10 +@gantt.events.each do |i| + if i.is_a? Issue + i_start_date = (i.start_date >= @gantt.date_from ? i.start_date : @gantt.date_from ) + i_end_date = (i.due_before <= @gantt.date_to ? i.due_before : @gantt.date_to ) + + i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor + i_done_date = (i_done_date <= @gantt.date_from ? @gantt.date_from : i_done_date ) + i_done_date = (i_done_date >= @gantt.date_to ? @gantt.date_to : i_done_date ) + + i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today + + i_left = ((i_start_date - @gantt.date_from)*zoom).floor + i_width = ((i_end_date - i_start_date + 1)*zoom).floor - 2 # total width of the issue (- 2 for left and right borders) + d_width = ((i_done_date - i_start_date)*zoom).floor - 2 # done width + l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor - 2 : 0 # delay width + %> + <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;" class="task task_todo">&nbsp;</div> + <% if l_width > 0 %> + <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= l_width %>px;" class="task task_late">&nbsp;</div> + <% end %> + <% if d_width > 0 %> + <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= d_width %>px;" class="task task_done">&nbsp;</div> + <% end %> + <div style="top:<%= top %>px;left:<%= i_left + i_width + 5 %>px;background:#fff;" class="task"> + <%= i.status.name %> + <%= (i.done_ratio).to_i %>% + </div> + <div class="tooltip" style="position: absolute;top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;height:12px;"> + <span class="tip"> + <%= render_issue_tooltip i %> + </span></div> +<% else + i_left = ((i.start_date - @gantt.date_from)*zoom).floor + %> + <div style="top:<%= top %>px;left:<%= i_left %>px;width:15px;" class="task milestone">&nbsp;</div> + <div style="top:<%= top %>px;left:<%= i_left + 12 %>px;background:#fff;" class="task"> + <%= h("#{i.project} -") unless @project && @project == i.project %> + <strong><%=h i %></strong> + </div> +<% end %> + <% top = top + 20 +end %> + +<% +# +# Today red line (excluded from cache) +# +if Date.today >= @gantt.date_from and Date.today <= @gantt.date_to %> + <div style="position: absolute;height:<%= g_height %>px;top:<%= headers_height + 1 %>px;left:<%= ((Date.today-@gantt.date_from+1)*zoom).floor()-1 %>px;width:10px;border-left: 1px dashed red;">&nbsp;</div> +<% end %> + +</div> +</td> +</tr> +</table> + +<table width="100%"> +<tr> +<td align="left"><%= link_to_remote ('&#171; ' + l(:label_previous)), {:url => @gantt.params_previous, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_previous)} %></td> +<td align="right"><%= link_to_remote (l(:label_next) + ' &#187;'), {:url => @gantt.params_next, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_next)} %></td> +</tr> +</table> + +<p class="other-formats"> +<%= l(:label_export_to) %> +<span><%= link_to 'PDF', @gantt.params.merge(:format => 'pdf'), :class => 'pdf' %></span> +<% if @gantt.respond_to?('to_image') %> +<span><%= link_to 'PNG', @gantt.params.merge(:format => 'png'), :class => 'image' %></span> +<% end %> +</p> +<% end # query.valid? %> + +<% content_for :sidebar do %> + <%= render :partial => 'issues/sidebar' %> +<% end %> + +<% html_title(l(:label_gantt)) -%> diff --git a/app/views/issues/index.ctp b/app/views/issues/index.ctp new file mode 100644 index 0000000..973f3eb --- /dev/null +++ b/app/views/issues/index.ctp @@ -0,0 +1,67 @@ +<% if @query.new_record? %> + <h2><%=l(:label_issue_plural)%></h2> + <% html_title(l(:label_issue_plural)) %> + + <% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %> + <%= hidden_field_tag('project_id', @project.id) if @project %> + <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend> + <%= render :partial => 'queries/filters', :locals => {:query => @query} %> + <p class="buttons"> + <%= link_to_remote l(:button_apply), + { :url => { :set_filter => 1 }, + :update => "content", + :with => "Form.serialize('query_form')" + }, :class => 'icon icon-checked' %> + + <%= link_to_remote l(:button_clear), + { :url => { :set_filter => 1 }, + :update => "content", + }, :class => 'icon icon-reload' %> + + <% if User.current.allowed_to?(:save_queries, @project, :global => true) %> + <%= link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save' %> + <% end %> + </p> + </fieldset> + <% end %> +<% else %> + <div class="contextual"> + <% if @query.editable_by?(User.current) %> + <%= link_to l(:button_edit), {:controller => 'queries', :action => 'edit', :id => @query}, :class => 'icon icon-edit' %> + <%= link_to l(:button_delete), {:controller => 'queries', :action => 'destroy', :id => @query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> + <% end %> + </div> + <h2><%=h @query.name %></h2> + <div id="query_form"></div> + <% html_title @query.name %> +<% end %> +<%= error_messages_for 'query' %> +<% if @query.valid? %> +<% if @issues.empty? %> +<p class="nodata"><%= l(:label_no_data) %></p> +<% else %> +<%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %> +<p class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></p> + +<p class="other-formats"> +<%= l(:label_export_to) %> +<span><%= link_to 'Atom', {:query_id => @query, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span> +<span><%= link_to 'CSV', {:format => 'csv'}, :class => 'csv' %></span> +<span><%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %></span> +</p> +<% end %> +<% end %> + +<% content_for :sidebar do %> + <%= render :partial => 'issues/sidebar' %> +<% end %> + +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, {:query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_issue_plural)) %> + <%= auto_discovery_link_tag(:atom, {:action => 'changes', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %> + <%= javascript_include_tag 'context_menu' %> + <%= stylesheet_link_tag 'context_menu' %> +<% end %> + +<div id="context-menu" style="display: none;"></div> +<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %> diff --git a/app/views/issues/move.ctp b/app/views/issues/move.ctp new file mode 100644 index 0000000..35761e1 --- /dev/null +++ b/app/views/issues/move.ctp @@ -0,0 +1,22 @@ +<h2><%= l(:button_move) %></h2> + +<ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul> + +<% form_tag({}, :id => 'move_form') do %> +<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %> + +<div class="box tabular"> +<p><label for="new_project_id"><%=l(:field_project)%> :</label> +<%= select_tag "new_project_id", + options_from_collection_for_select(@allowed_projects, 'id', 'name', @target_project.id), + :onchange => remote_function(:url => { :action => 'move' }, + :method => :get, + :update => 'content', + :with => "Form.serialize('move_form')") %></p> + +<p><label for="new_tracker_id"><%=l(:field_tracker)%> :</label> +<%= select_tag "new_tracker_id", "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@trackers, "id", "name") %></p> +</div> + +<%= submit_tag l(:button_move) %> +<% end %> diff --git a/app/views/issues/new.ctp b/app/views/issues/new.ctp new file mode 100644 index 0000000..94f86dd --- /dev/null +++ b/app/views/issues/new.ctp @@ -0,0 +1,26 @@ +<h2><%=l(:label_issue_new)%></h2> + +<% labelled_tabular_form_for :issue, @issue, + :html => {:multipart => true, :id => 'issue-form'} do |f| %> + <%= error_messages_for 'issue' %> + <div class="box"> + <%= render :partial => 'issues/form', :locals => {:f => f} %> + </div> + <%= submit_tag l(:button_create) %> + <%= submit_tag l(:button_create_and_continue), :name => 'continue' %> + <%= link_to_remote l(:label_preview), + { :url => { :controller => 'issues', :action => 'preview', :project_id => @project }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('issue-form')", + :complete => "Element.scrollTo('preview')" + }, :accesskey => accesskey(:preview) %> + + <%= javascript_tag "Form.Element.focus('issue_subject');" %> +<% end %> + +<div id="preview" class="wiki"></div> + +<% content_for :header_tags do %> + <%= stylesheet_link_tag 'scm' %> +<% end %> diff --git a/app/views/issues/show.ctp b/app/views/issues/show.ctp new file mode 100644 index 0000000..41d2abe --- /dev/null +++ b/app/views/issues/show.ctp @@ -0,0 +1,126 @@ +<div class="contextual"> +<%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %> +<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %> +<%= watcher_tag(@issue, User.current) %> +<%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %> +<%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %> +<%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> +</div> + +<h2><%= @issue.tracker.name %> #<%= @issue.id %></h2> + +<div class="<%= css_issue_classes(@issue) %>"> + <%= avatar(@issue.author, :size => "64") %> + <h3><%=h @issue.subject %></h3> + <p class="author"> + <%= authoring @issue.created_on, @issue.author %>. + <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @issue.updated_on)) + '.' if @issue.created_on != @issue.updated_on %> + </p> + +<table width="100%"> +<tr> + <td style="width:15%" class="status"><b><%=l(:field_status)%>:</b></td><td style="width:35%" class="status"><%= @issue.status.name %></td> + <td style="width:15%" class="start-date"><b><%=l(:field_start_date)%>:</b></td><td style="width:35%"><%= format_date(@issue.start_date) %></td> +</tr> +<tr> + <td class="priority"><b><%=l(:field_priority)%>:</b></td><td class="priority"><%= @issue.priority.name %></td> + <td class="due-date"><b><%=l(:field_due_date)%>:</b></td><td class="due-date"><%= format_date(@issue.due_date) %></td> +</tr> +<tr> + <td class="assigned-to"><b><%=l(:field_assigned_to)%>:</b></td><td><%= avatar(@issue.assigned_to, :size => "14") %><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td> + <td class="progress"><b><%=l(:field_done_ratio)%>:</b></td><td class="progress"><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td> +</tr> +<tr> + <td class="category"><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td> + <% if User.current.allowed_to?(:view_time_entries, @project) %> + <td class="spent-time"><b><%=l(:label_spent_time)%>:</b></td> + <td class="spent-hours"><%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time') : "-" %></td> + <% end %> +</tr> +<tr> + <td class="fixed-version"><b><%=l(:field_fixed_version)%>:</b></td><td><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td> + <% if @issue.estimated_hours %> + <td class="estimated-hours"><b><%=l(:field_estimated_hours)%>:</b></td><td><%= lwr(:label_f_hour, @issue.estimated_hours) %></td> + <% end %> +</tr> +<tr> +<% n = 0 -%> +<% @issue.custom_values.each do |value| -%> + <td valign="top"><b><%=h value.custom_field.name %>:</b></td><td valign="top"><%= simple_format(h(show_value(value))) %></td> +<% n = n + 1 + if (n > 1) + n = 0 %> + </tr><tr> + <%end +end %> +</tr> +<%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %> +</table> +<hr /> + +<div class="contextual"> +<%= link_to_remote_if_authorized(l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment') unless @issue.description.blank? %> +</div> + +<p><strong><%=l(:field_description)%></strong></p> +<div class="wiki"> +<%= textilizable @issue, :description, :attachments => @issue.attachments %> +</div> + +<%= link_to_attachments @issue %> + +<% if authorize_for('issue_relations', 'new') || @issue.relations.any? %> +<hr /> +<div id="relations"> +<%= render :partial => 'relations' %> +</div> +<% end %> + +<% if User.current.allowed_to?(:add_issue_watchers, @project) || + (@issue.watchers.any? && User.current.allowed_to?(:view_issue_watchers, @project)) %> +<hr /> +<div id="watchers"> +<%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %> +</div> +<% end %> + +</div> + +<% if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @project) %> +<div id="issue-changesets"> +<h3><%=l(:label_associated_revisions)%></h3> +<%= render :partial => 'changesets', :locals => { :changesets => @issue.changesets} %> +</div> +<% end %> + +<% if @journals.any? %> +<div id="history"> +<h3><%=l(:label_history)%></h3> +<%= render :partial => 'history', :locals => { :journals => @journals } %> +</div> +<% end %> +<div style="clear: both;"></div> + +<% if authorize_for('issues', 'edit') %> + <div id="update" style="display:none;"> + <h3><%= l(:button_update) %></h3> + <%= render :partial => 'edit' %> + </div> +<% end %> + +<p class="other-formats"> +<%= l(:label_export_to) %> +<span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span> +<span><%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %></span> +</p> + +<% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %> + +<% content_for :sidebar do %> + <%= render :partial => 'issues/sidebar' %> +<% end %> + +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %> + <%= stylesheet_link_tag 'scm' %> +<% end %> diff --git a/app/views/journals/_notes_form.ctp b/app/views/journals/_notes_form.ctp new file mode 100644 index 0000000..6e6ad0f --- /dev/null +++ b/app/views/journals/_notes_form.ctp @@ -0,0 +1,8 @@ +<% form_remote_tag(:url => {}, :html => { :id => "journal-#{@journal.id}-form" }) do %> + <%= text_area_tag :notes, h(@journal.notes), :class => 'wiki-edit', + :rows => (@journal.notes.blank? ? 10 : [[10, @journal.notes.length / 50].max, 100].min) %> + <%= call_hook(:view_journals_notes_form_after_notes, { :journal => @journal}) %> + <p><%= submit_tag l(:button_save) %> + <%= link_to l(:button_cancel), '#', :onclick => "Element.remove('journal-#{@journal.id}-form'); " + + "Element.show('journal-#{@journal.id}-notes'); return false;" %></p> +<% end %> diff --git a/app/views/journals/edit.rjs b/app/views/journals/edit.rjs new file mode 100644 index 0000000..798cb0f --- /dev/null +++ b/app/views/journals/edit.rjs @@ -0,0 +1,3 @@ +page.hide "journal-#{@journal.id}-notes" +page.insert_html :after, "journal-#{@journal.id}-notes", + :partial => 'notes_form' diff --git a/app/views/journals/update.rjs b/app/views/journals/update.rjs new file mode 100644 index 0000000..55efb9b --- /dev/null +++ b/app/views/journals/update.rjs @@ -0,0 +1,10 @@ +if @journal.frozen? + # journal was destroyed + page.remove "change-#{@journal.id}" +else + page.replace "journal-#{@journal.id}-notes", render_notes(@journal) + page.show "journal-#{@journal.id}-notes" + page.remove "journal-#{@journal.id}-form" +end + +call_hook(:view_journals_update_rjs_bottom, { :page => page, :journal => @journal }) diff --git a/app/views/layouts/_project_selector.ctp b/app/views/layouts/_project_selector.ctp new file mode 100644 index 0000000..d8e2a72 --- /dev/null +++ b/app/views/layouts/_project_selector.ctp @@ -0,0 +1,12 @@ +<% user_projects_by_root = User.current.projects.find(:all, :include => :parent).group_by(&:root) %> +<select onchange="if (this.value != '') { window.location = this.value; }"> +<option selected="selected"><%= l(:label_jump_to_a_project) %></option> +<option disabled="disabled">---</option> +<% user_projects_by_root.keys.sort.each do |root| %> + <%= content_tag('option', h(root.name), :value => url_for(:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item)) %> + <% user_projects_by_root[root].sort.each do |project| %> + <% next if project == root %> + <%= content_tag('option', ('&#187; ' + h(project.name)), :value => url_for(:controller => 'projects', :action => 'show', :id => project, :jump => current_menu_item)) %> + <% end %> +<% end %> +</select> diff --git a/app/views/layouts/base.ctp b/app/views/layouts/base.ctp new file mode 100644 index 0000000..57344f5 --- /dev/null +++ b/app/views/layouts/base.ctp @@ -0,0 +1,77 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> +<head> +<title><?php echo $title_for_layout; ?></title> +<meta http-equiv="content-type" content="text/html; charset=utf-8" /> +<meta name="description" content="<?php echo Configure::read('app_title'); ?>" /> +<meta name="keywords" content="issue,bug,tracker" /> +<?php echo $html->css('application') ?> +<?php echo $javascript->link(a('prototype','effects','dragdrop','controls','application')) ?> +<!-- <%= heads_for_wiki_formatter %> --> +<?php echo $html->css('jstoolbar') ?> +<!--[if IE]> + <style type="text/css"> + * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); } + body {behavior: url(<%= stylesheet_path "csshover.htc" %>);} + </style> +<![endif]--> +<!-- <%= call_hook :view_layouts_base_html_head %> --> +<!-- page specific tags --> +<!-- <%= yield :header_tags -%> --> +</head> +<body> +<div id="wrapper"> +<div id="top-menu"> + <div id="account"> + <!-- <%= render_menu :account_menu -%> --> + <?php echo $this->renderElement('account_menu') ?> + </div> + <?php if ($currentuser['logged']) echo $html->tag('div',__('label_logged_as',true).' '.$candy->link($currentuser),array('id'=>'loggedas')); ?> + <?php echo $this->renderElement('top_menu') ?> +</div> + +<div id="header"> + <div id="quick-search"> + <?php echo $form->create(null,aa('url','/search/index','type','get')) ?> + <!-- <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %> --> + <?php echo $html->link(__('search',true).':','/search/index',$candy->accesskey('search')) ?> + <?php echo $form->input('q',aa('type','text','size',20,'class','small','accesskey',$candy->accesskey('quick_search'),'div',false,'label',false)) ?> +<!-- <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %> --> + <?php echo $form->end() ?> + <!-- <%= render :partial => 'layouts/project_selector' if User.current.memberships.any? %> --> + </div> + <h1><?php if (isset($Project)) { h($Project['name']); } else { echo Configure::read('app_title'); } ?></h1> +<!-- <h1><%= h(@project && !@project.new_record? ? @project.name : Setting.app_title) %></h1> --> + + <div id="main-menu"> +<!-- <%= render_main_menu(@project) %> --> + </div> +</div> + +<!-- <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %> --> +<?php echo $html->tag('div',null,aa('id','main','class',isset($Sidebar) ? '' : 'nosidebar')) ?> + <div id="sidebar"> + <?php if (isset($Sidebar)) echo $Sidebar; ?> + </div> + + <div id="content"> + <?php $session->flash(); ?> + <?php echo $content_for_layout; ?> + </div> +</div> + +<div id="ajax-indicator" style="display:none;"><span><?php __('label_loading') ?></span></div> + +<div id="footer"> + Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2009 candycane team + <br/><?php echo $html->link( + $html->image('cake.power.gif', array('alt'=> __("CakePHP: the rapid development php framework", true), 'border'=>"0")), + 'http://www.cakephp.org/', + array('target'=>'_blank'), null, false + ); + ?> +</div> +</div> +<%= call_hook :view_layouts_base_body_bottom %> +</body> +</html> diff --git a/app/views/layouts/js/empty b/app/views/layouts/js/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/views/layouts/rss/empty b/app/views/layouts/rss/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/views/layouts/xml/empty b/app/views/layouts/xml/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/views/mailer/_issue_text_html.ctp b/app/views/mailer/_issue_text_html.ctp new file mode 100644 index 0000000..d0f2478 --- /dev/null +++ b/app/views/mailer/_issue_text_html.ctp @@ -0,0 +1,15 @@ +<h1><%= link_to "#{issue.tracker.name} ##{issue.id}: #{issue.subject}", issue_url %></h1> + +<ul> +<li><%=l(:field_author)%>: <%= issue.author %></li> +<li><%=l(:field_status)%>: <%= issue.status %></li> +<li><%=l(:field_priority)%>: <%= issue.priority %></li> +<li><%=l(:field_assigned_to)%>: <%= issue.assigned_to %></li> +<li><%=l(:field_category)%>: <%= issue.category %></li> +<li><%=l(:field_fixed_version)%>: <%= issue.fixed_version %></li> +<% issue.custom_values.each do |c| %> + <li><%= c.custom_field.name %>: <%= show_value(c) %></li> +<% end %> +</ul> + +<%= textilizable(issue, :description, :only_path => false) %> diff --git a/app/views/mailer/_issue_text_plain.ctp b/app/views/mailer/_issue_text_plain.ctp new file mode 100644 index 0000000..6b87c18 --- /dev/null +++ b/app/views/mailer/_issue_text_plain.ctp @@ -0,0 +1,13 @@ +<%= "#{issue.tracker.name} ##{issue.id}: #{issue.subject}" %> +<%= issue_url %> + +<%=l(:field_author)%>: <%= issue.author %> +<%=l(:field_status)%>: <%= issue.status %> +<%=l(:field_priority)%>: <%= issue.priority %> +<%=l(:field_assigned_to)%>: <%= issue.assigned_to %> +<%=l(:field_category)%>: <%= issue.category %> +<%=l(:field_fixed_version)%>: <%= issue.fixed_version %> +<% issue.custom_values.each do |c| %><%= c.custom_field.name %>: <%= show_value(c) %> +<% end %> + +<%= issue.description %> diff --git a/app/views/mailer/account_activation_request.text.html.ctp b/app/views/mailer/account_activation_request.text.html.ctp new file mode 100644 index 0000000..145ecfc --- /dev/null +++ b/app/views/mailer/account_activation_request.text.html.ctp @@ -0,0 +1,2 @@ +<p><%= l(:mail_body_account_activation_request, @user.login) %></p> +<p><%= link_to @url, @url %></p> diff --git a/app/views/mailer/account_activation_request.text.plain.ctp b/app/views/mailer/account_activation_request.text.plain.ctp new file mode 100644 index 0000000..f431e22 --- /dev/null +++ b/app/views/mailer/account_activation_request.text.plain.ctp @@ -0,0 +1,2 @@ +<%= l(:mail_body_account_activation_request, @user.login) %> +<%= @url %> diff --git a/app/views/mailer/account_information.text.html.ctp b/app/views/mailer/account_information.text.html.ctp new file mode 100644 index 0000000..3b6ab6a --- /dev/null +++ b/app/views/mailer/account_information.text.html.ctp @@ -0,0 +1,11 @@ +<% if @user.auth_source %> +<p><%= l(:mail_body_account_information_external, @user.auth_source.name) %></p> +<% else %> +<p><%= l(:mail_body_account_information) %>:</p> +<ul> + <li><%= l(:field_login) %>: <%= @user.login %></li> + <li><%= l(:field_password) %>: <%= @password %></li> +</ul> +<% end %> + +<p><%= l(:label_login) %>: <%= auto_link(@login_url) %></p> diff --git a/app/views/mailer/account_information.text.plain.ctp b/app/views/mailer/account_information.text.plain.ctp new file mode 100644 index 0000000..0a02566 --- /dev/null +++ b/app/views/mailer/account_information.text.plain.ctp @@ -0,0 +1,6 @@ +<% if @user.auth_source %><%= l(:mail_body_account_information_external, @user.auth_source.name) %> +<% else %><%= l(:mail_body_account_information) %>: +* <%= l(:field_login) %>: <%= @user.login %> +* <%= l(:field_password) %>: <%= @password %> +<% end %> +<%= l(:label_login) %>: <%= @login_url %> diff --git a/app/views/mailer/attachments_added.text.html.ctp b/app/views/mailer/attachments_added.text.html.ctp new file mode 100644 index 0000000..d2355b1 --- /dev/null +++ b/app/views/mailer/attachments_added.text.html.ctp @@ -0,0 +1,5 @@ +<%= link_to @added_to, @added_to_url %><br /> + +<ul><% @attachments.each do |attachment | %> +<li><%= attachment.filename %></li> +<% end %></ul> diff --git a/app/views/mailer/attachments_added.text.plain.ctp b/app/views/mailer/attachments_added.text.plain.ctp new file mode 100644 index 0000000..28cb828 --- /dev/null +++ b/app/views/mailer/attachments_added.text.plain.ctp @@ -0,0 +1,4 @@ +<%= @added_to %><% @attachments.each do |attachment | %> +- <%= attachment.filename %><% end %> + +<%= @added_to_url %> diff --git a/app/views/mailer/document_added.text.html.ctp b/app/views/mailer/document_added.text.html.ctp new file mode 100644 index 0000000..dc1f659 --- /dev/null +++ b/app/views/mailer/document_added.text.html.ctp @@ -0,0 +1,3 @@ +<%= link_to @document.title, @document_url %> (<%= @document.category.name %>)<br /> +<br /> +<%= textilizable(@document, :description, :only_path => false) %> diff --git a/app/views/mailer/document_added.text.plain.ctp b/app/views/mailer/document_added.text.plain.ctp new file mode 100644 index 0000000..a6a7282 --- /dev/null +++ b/app/views/mailer/document_added.text.plain.ctp @@ -0,0 +1,4 @@ +<%= @document.title %> (<%= @document.category.name %>) +<%= @document_url %> + +<%= @document.description %> diff --git a/app/views/mailer/issue_add.text.html.ctp b/app/views/mailer/issue_add.text.html.ctp new file mode 100644 index 0000000..b1c4605 --- /dev/null +++ b/app/views/mailer/issue_add.text.html.ctp @@ -0,0 +1,3 @@ +<%= l(:text_issue_added, "##{@issue.id}", @issue.author) %> +<hr /> +<%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/app/views/mailer/issue_add.text.plain.ctp b/app/views/mailer/issue_add.text.plain.ctp new file mode 100644 index 0000000..c6cb083 --- /dev/null +++ b/app/views/mailer/issue_add.text.plain.ctp @@ -0,0 +1,4 @@ +<%= l(:text_issue_added, "##{@issue.id}", @issue.author) %> + +---------------------------------------- +<%= render :partial => "issue_text_plain", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/app/views/mailer/issue_edit.text.html.ctp b/app/views/mailer/issue_edit.text.html.ctp new file mode 100644 index 0000000..48affaf --- /dev/null +++ b/app/views/mailer/issue_edit.text.html.ctp @@ -0,0 +1,11 @@ +<%= l(:text_issue_updated, "##{@issue.id}", @journal.user) %> + +<ul> +<% for detail in @journal.details %> + <li><%= show_detail(detail, true) %></li> +<% end %> +</ul> + +<%= textilizable(@journal, :notes, :only_path => false) %> +<hr /> +<%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/app/views/mailer/issue_edit.text.plain.ctp b/app/views/mailer/issue_edit.text.plain.ctp new file mode 100644 index 0000000..b5a5ec9 --- /dev/null +++ b/app/views/mailer/issue_edit.text.plain.ctp @@ -0,0 +1,9 @@ +<%= l(:text_issue_updated, "##{@issue.id}", @journal.user) %> + +<% for detail in @journal.details -%> +<%= show_detail(detail, true) %> +<% end -%> + +<%= @journal.notes if @journal.notes? %> +---------------------------------------- +<%= render :partial => "issue_text_plain", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/app/views/mailer/layout.text.html.ctp b/app/views/mailer/layout.text.html.ctp new file mode 100644 index 0000000..83d9678 --- /dev/null +++ b/app/views/mailer/layout.text.html.ctp @@ -0,0 +1,32 @@ +<html> +<head> +<style> +body { + font-family: Verdana, sans-serif; + font-size: 0.8em; + color:#484848; +} +h1, h2, h3 { font-family: "Trebuchet MS", Verdana, sans-serif; margin: 0px; } +h1 { font-size: 1.2em; } +h2, h3 { font-size: 1.1em; } +a, a:link, a:visited { color: #2A5685;} +a:hover, a:active { color: #c61a1a; } +a.wiki-anchor { display: none; } +hr { + width: 100%; + height: 1px; + background: #ccc; + border: 0; +} +.footer { + font-size: 0.8em; + font-style: italic; +} +</style> +</head> +<body> +<%= yield %> +<hr /> +<span class="footer"><%= Redmine::WikiFormatting.to_html(Setting.text_formatting, Setting.emails_footer) %></span> +</body> +</html> diff --git a/app/views/mailer/layout.text.plain.ctp b/app/views/mailer/layout.text.plain.ctp new file mode 100644 index 0000000..ec3e1bf --- /dev/null +++ b/app/views/mailer/layout.text.plain.ctp @@ -0,0 +1,3 @@ +<%= yield %> +---------------------------------------- +<%= Setting.emails_footer %> diff --git a/app/views/mailer/lost_password.text.html.ctp b/app/views/mailer/lost_password.text.html.ctp new file mode 100644 index 0000000..4dd570c --- /dev/null +++ b/app/views/mailer/lost_password.text.html.ctp @@ -0,0 +1,4 @@ +<p><%= l(:mail_body_lost_password) %><br /> +<%= auto_link(@url) %></p> + +<p><%= l(:field_login) %>: <b><%= @token.user.login %></b></p> diff --git a/app/views/mailer/lost_password.text.plain.ctp b/app/views/mailer/lost_password.text.plain.ctp new file mode 100644 index 0000000..f5000ed --- /dev/null +++ b/app/views/mailer/lost_password.text.plain.ctp @@ -0,0 +1,4 @@ +<%= l(:mail_body_lost_password) %> +<%= @url %> + +<%= l(:field_login) %>: <%= @token.user.login %> diff --git a/app/views/mailer/message_posted.text.html.ctp b/app/views/mailer/message_posted.text.html.ctp new file mode 100644 index 0000000..d91ce5a --- /dev/null +++ b/app/views/mailer/message_posted.text.html.ctp @@ -0,0 +1,4 @@ +<h1><%=h @message.board.project.name %> - <%=h @message.board.name %>: <%= link_to @message.subject, @message_url %></h1> +<em><%= @message.author %></em> + +<%= textilizable(@message, :content, :only_path => false) %> diff --git a/app/views/mailer/message_posted.text.plain.ctp b/app/views/mailer/message_posted.text.plain.ctp new file mode 100644 index 0000000..ef6a3b3 --- /dev/null +++ b/app/views/mailer/message_posted.text.plain.ctp @@ -0,0 +1,4 @@ +<%= @message_url %> +<%= @message.author %> + +<%= @message.content %> diff --git a/app/views/mailer/news_added.text.html.ctp b/app/views/mailer/news_added.text.html.ctp new file mode 100644 index 0000000..15bc89f --- /dev/null +++ b/app/views/mailer/news_added.text.html.ctp @@ -0,0 +1,4 @@ +<h1><%= link_to @news.title, @news_url %></h1> +<em><%= @news.author.name %></em> + +<%= textilizable(@news, :description, :only_path => false) %> diff --git a/app/views/mailer/news_added.text.plain.ctp b/app/views/mailer/news_added.text.plain.ctp new file mode 100644 index 0000000..c8ae303 --- /dev/null +++ b/app/views/mailer/news_added.text.plain.ctp @@ -0,0 +1,5 @@ +<%= @news.title %> +<%= @news_url %> +<%= @news.author.name %> + +<%= @news.description %> diff --git a/app/views/mailer/register.text.html.ctp b/app/views/mailer/register.text.html.ctp new file mode 100644 index 0000000..145c3d7 --- /dev/null +++ b/app/views/mailer/register.text.html.ctp @@ -0,0 +1,2 @@ +<p><%= l(:mail_body_register) %><br /> +<%= auto_link(@url) %></p> diff --git a/app/views/mailer/register.text.plain.ctp b/app/views/mailer/register.text.plain.ctp new file mode 100644 index 0000000..102a15e --- /dev/null +++ b/app/views/mailer/register.text.plain.ctp @@ -0,0 +1,2 @@ +<%= l(:mail_body_register) %> +<%= @url %> diff --git a/app/views/mailer/reminder.text.html.ctp b/app/views/mailer/reminder.text.html.ctp new file mode 100644 index 0000000..1e33fbe --- /dev/null +++ b/app/views/mailer/reminder.text.html.ctp @@ -0,0 +1,9 @@ +<p><%= l(:mail_body_reminder, @issues.size, @days) %></p> + +<ul> +<% @issues.each do |issue| -%> + <li><%=h "#{issue.project} - #{issue.tracker} ##{issue.id}: #{issue.subject}" %></li> +<% end -%> +</ul> + +<p><%= link_to l(:label_issue_view_all), @issues_url %></p> diff --git a/app/views/mailer/reminder.text.plain.ctp b/app/views/mailer/reminder.text.plain.ctp new file mode 100644 index 0000000..7e6a2e5 --- /dev/null +++ b/app/views/mailer/reminder.text.plain.ctp @@ -0,0 +1,7 @@ +<%= l(:mail_body_reminder, @issues.size, @days) %>: + +<% @issues.each do |issue| -%> +* <%= "#{issue.project} - #{issue.tracker} ##{issue.id}: #{issue.subject}" %> +<% end -%> + +<%= @issues_url %> diff --git a/app/views/mailer/test.text.html.ctp b/app/views/mailer/test.text.html.ctp new file mode 100644 index 0000000..25ad20c --- /dev/null +++ b/app/views/mailer/test.text.html.ctp @@ -0,0 +1,2 @@ +<p>This is a test email sent by Redmine.<br /> +Redmine URL: <%= auto_link(@url) %></p> diff --git a/app/views/mailer/test.text.plain.ctp b/app/views/mailer/test.text.plain.ctp new file mode 100644 index 0000000..790d6ab --- /dev/null +++ b/app/views/mailer/test.text.plain.ctp @@ -0,0 +1,2 @@ +This is a test email sent by Redmine. +Redmine URL: <%= @url %> diff --git a/app/views/messages/_form.ctp b/app/views/messages/_form.ctp new file mode 100644 index 0000000..540811e --- /dev/null +++ b/app/views/messages/_form.ctp @@ -0,0 +1,21 @@ +<%= error_messages_for 'message' %> +<% replying ||= false %> + +<div class="box"> +<!--[form:message]--> +<p><label><%= l(:field_subject) %></label><br /> +<%= f.text_field :subject, :size => 120 %> + +<% if !replying && User.current.allowed_to?(:edit_messages, @project) %> + <label><%= f.check_box :sticky %> Sticky</label> + <label><%= f.check_box :locked %> Locked</label> +<% end %> +</p> + +<p><%= f.text_area :content, :cols => 80, :rows => 15, :class => 'wiki-edit', :id => 'message_content' %></p> +<%= wikitoolbar_for 'message_content' %> +<!--[eoform:message]--> + +<p><%= l(:label_attachment_plural) %><br /> +<%= render :partial => 'attachments/form' %></p> +</div> diff --git a/app/views/messages/edit.ctp b/app/views/messages/edit.ctp new file mode 100644 index 0000000..56e7086 --- /dev/null +++ b/app/views/messages/edit.ctp @@ -0,0 +1,14 @@ +<h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%=h @message.subject %></h2> + +<% form_for :message, @message, :url => {:action => 'edit'}, :html => {:multipart => true, :id => 'message-form'} do |f| %> + <%= render :partial => 'form', :locals => {:f => f, :replying => !@message.parent.nil?} %> + <%= submit_tag l(:button_save) %> + <%= link_to_remote l(:label_preview), + { :url => { :controller => 'messages', :action => 'preview', :board_id => @board }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('message-form')", + :complete => "Element.scrollTo('preview')" + }, :accesskey => accesskey(:preview) %> +<% end %> +<div id="preview" class="wiki"></div> diff --git a/app/views/messages/new.ctp b/app/views/messages/new.ctp new file mode 100644 index 0000000..050c132 --- /dev/null +++ b/app/views/messages/new.ctp @@ -0,0 +1,15 @@ +<h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%= l(:label_message_new) %></h2> + +<% form_for :message, @message, :url => {:action => 'new'}, :html => {:multipart => true, :id => 'message-form'} do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_create) %> + <%= link_to_remote l(:label_preview), + { :url => { :controller => 'messages', :action => 'preview', :board_id => @board }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('message-form')", + :complete => "Element.scrollTo('preview')" + }, :accesskey => accesskey(:preview) %> +<% end %> + +<div id="preview" class="wiki"></div> diff --git a/app/views/messages/show.ctp b/app/views/messages/show.ctp new file mode 100644 index 0000000..eae349d --- /dev/null +++ b/app/views/messages/show.ctp @@ -0,0 +1,61 @@ +<%= breadcrumb link_to(l(:label_board_plural), {:controller => 'boards', :action => 'index', :project_id => @project}), + link_to(h(@board.name), {:controller => 'boards', :action => 'show', :project_id => @project, :id => @board}) %> + +<div class="contextual"> + <%= watcher_tag(@topic, User.current) %> + <%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'quote', :id => @topic} }, :class => 'icon icon-comment' %> + <%= link_to(l(:button_edit), {:action => 'edit', :id => @topic}, :class => 'icon icon-edit') if @message.editable_by?(User.current) %> + <%= link_to(l(:button_delete), {:action => 'destroy', :id => @topic}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') if @message.destroyable_by?(User.current) %> +</div> + +<h2><%=h @topic.subject %></h2> + +<div class="message"> +<p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p> +<div class="wiki"> +<%= textilizable(@topic.content, :attachments => @topic.attachments) %> +</div> +<%= link_to_attachments @topic, :author => false %> +</div> +<br /> + +<% unless @replies.empty? %> +<h3 class="icon22 icon22-comment"><%= l(:label_reply_plural) %></h3> +<% @replies.each do |message| %> + <a name="<%= "message-#{message.id}" %>"></a> + <div class="contextual"> + <%= link_to_remote_if_authorized image_tag('comment.png'), { :url => {:action => 'quote', :id => message} }, :title => l(:button_quote) %> + <%= link_to(image_tag('edit.png'), {:action => 'edit', :id => message}, :title => l(:button_edit)) if message.editable_by?(User.current) %> + <%= link_to(image_tag('delete.png'), {:action => 'destroy', :id => message}, :method => :post, :confirm => l(:text_are_you_sure), :title => l(:button_delete)) if message.destroyable_by?(User.current) %> + </div> + <div class="message reply"> + <h4><%=h message.subject %> - <%= authoring message.created_on, message.author %></h4> + <div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div> + <%= link_to_attachments message, :author => false %> + </div> +<% end %> +<% end %> + +<% if !@topic.locked? && authorize_for('messages', 'reply') %> +<p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p> +<div id="reply" style="display:none;"> +<% form_for :reply, @reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true, :id => 'message-form'} do |f| %> + <%= render :partial => 'form', :locals => {:f => f, :replying => true} %> + <%= submit_tag l(:button_submit) %> + <%= link_to_remote l(:label_preview), + { :url => { :controller => 'messages', :action => 'preview', :board_id => @board }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('message-form')", + :complete => "Element.scrollTo('preview')" + }, :accesskey => accesskey(:preview) %> +<% end %> +<div id="preview" class="wiki"></div> +</div> +<% end %> + +<% content_for :header_tags do %> + <%= stylesheet_link_tag 'scm' %> +<% end %> + +<% html_title h(@topic.subject) %> diff --git a/app/views/my/_block.ctp b/app/views/my/_block.ctp new file mode 100644 index 0000000..bb5dd1f --- /dev/null +++ b/app/views/my/_block.ctp @@ -0,0 +1,14 @@ +<div id="block_<%= block_name %>" class="mypage-box"> + + <div style="float:right;margin-right:16px;z-index:500;"> + <%= link_to_remote "", { + :url => { :action => "remove_block", :block => block_name }, + :complete => "removeBlock('block_#{block_name}')" }, + :class => "close-icon" + %> + </div> + + <div class="handle"> + <%= render :partial => "my/blocks/#{block_name}", :locals => { :user => user } %> + </div> +</div> \ No newline at end of file diff --git a/app/views/my/_sidebar.ctp b/app/views/my/_sidebar.ctp new file mode 100644 index 0000000..d30eacf --- /dev/null +++ b/app/views/my/_sidebar.ctp @@ -0,0 +1,8 @@ +<h3><%=l(:label_my_account)%></h3> + +<p><%=l(:field_login)%>: <strong><%= @user.login %></strong><br /> +<%=l(:field_created_on)%>: <%= format_time(@user.created_on) %></p> +<% if @user.rss_token %> +<p><%= l(:label_feeds_access_key_created_on, distance_of_time_in_words(Time.now, @user.rss_token.created_on)) %> +(<%= link_to l(:button_reset), {:action => 'reset_rss_key'}, :method => :post %>)</p> +<% end %> diff --git a/app/views/my/account.ctp b/app/views/my/account.ctp new file mode 100644 index 0000000..f4b726f --- /dev/null +++ b/app/views/my/account.ctp @@ -0,0 +1,52 @@ +<div class="contextual"> +<%= link_to(l(:button_change_password), :action => 'password') unless @user.auth_source_id %> +</div> +<h2><%=l(:label_my_account)%></h2> +<%= error_messages_for 'user' %> + +<% form_for :user, @user, :url => { :action => "account" }, + :builder => TabularFormBuilder, + :lang => current_language, + :html => { :id => 'my_account_form' } do |f| %> +<div class="splitcontentleft"> +<h3><%=l(:label_information_plural)%></h3> +<div class="box tabular"> +<p><%= f.text_field :firstname, :required => true %></p> +<p><%= f.text_field :lastname, :required => true %></p> +<p><%= f.text_field :mail, :required => true %></p> +<p><%= f.select :language, lang_options_for_select %></p> +</div> + +<%= submit_tag l(:button_save) %> +</div> + +<div class="splitcontentright"> +<h3><%=l(:field_mail_notification)%></h3> +<div class="box"> +<%= select_tag 'notification_option', options_for_select(@notification_options, @notification_option), + :onchange => 'if ($("notification_option").value == "selected") {Element.show("notified-projects")} else {Element.hide("notified-projects")}' %> +<% content_tag 'div', :id => 'notified-projects', :style => (@notification_option == 'selected' ? '' : 'display:none;') do %> +<p><% User.current.projects.each do |project| %> + <label><%= check_box_tag 'notified_project_ids[]', project.id, @user.notified_projects_ids.include?(project.id) %> <%=h project.name %></label><br /> +<% end %></p> +<p><em><%= l(:text_user_mail_option) %></em></p> +<% end %> +<p><label><%= check_box_tag 'no_self_notified', 1, @user.pref[:no_self_notified] %> <%= l(:label_user_mail_no_self_notified) %></label></p> +</div> + +<h3><%=l(:label_preferences)%></h3> +<div class="box tabular"> +<% fields_for :pref, @user.pref, :builder => TabularFormBuilder, :lang => current_language do |pref_fields| %> +<p><%= pref_fields.check_box :hide_mail %></p> +<p><%= pref_fields.select :time_zone, ActiveSupport::TimeZone.all.collect {|z| [ z.to_s, z.name ]}, :include_blank => true %></p> +<p><%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %></p> +<% end %> +</div> +</div> +<% end %> + +<% content_for :sidebar do %> +<%= render :partial => 'sidebar' %> +<% end %> + +<% html_title(l(:label_my_account)) -%> diff --git a/app/views/my/blocks/_calendar.ctp b/app/views/my/blocks/_calendar.ctp new file mode 100644 index 0000000..bad7293 --- /dev/null +++ b/app/views/my/blocks/_calendar.ctp @@ -0,0 +1,8 @@ +<h3><%= l(:label_calendar) %></h3> + +<% calendar = Redmine::Helpers::Calendar.new(Date.today, current_language, :week) + calendar.events = Issue.find :all, + :conditions => ["#{Issue.table_name}.project_id in (#{@user.projects.collect{|m| m.id}.join(',')}) AND ((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?))", calendar.startdt, calendar.enddt, calendar.startdt, calendar.enddt], + :include => [:project, :tracker, :priority, :assigned_to] unless @user.projects.empty? %> + +<%= render :partial => 'common/calendar', :locals => {:calendar => calendar } %> diff --git a/app/views/my/blocks/_documents.ctp b/app/views/my/blocks/_documents.ctp new file mode 100644 index 0000000..d222e42 --- /dev/null +++ b/app/views/my/blocks/_documents.ctp @@ -0,0 +1,9 @@ +<h3><%=l(:label_document_plural)%></h3> + +<% project_ids = @user.projects.select {|p| @user.allowed_to?(:view_documents, p)}.collect(&:id) %> +<%= render(:partial => 'documents/document', + :collection => Document.find(:all, + :limit => 10, + :order => "#{Document.table_name}.created_on DESC", + :conditions => "#{Document.table_name}.project_id in (#{project_ids.join(',')})", + :include => [:project])) unless project_ids.empty? %> \ No newline at end of file diff --git a/app/views/my/blocks/_issuesassignedtome.ctp b/app/views/my/blocks/_issuesassignedtome.ctp new file mode 100644 index 0000000..99812f6 --- /dev/null +++ b/app/views/my/blocks/_issuesassignedtome.ctp @@ -0,0 +1,17 @@ +<h3><%=l(:label_assigned_to_me_issues)%></h3> +<% assigned_issues = Issue.find(:all, + :conditions => ["assigned_to_id=? AND #{IssueStatus.table_name}.is_closed=? AND #{Project.table_name}.status=#{Project::STATUS_ACTIVE}", user.id, false], + :limit => 10, + :include => [ :status, :project, :tracker, :priority ], + :order => "#{Enumeration.table_name}.position DESC, #{Issue.table_name}.updated_on DESC") %> +<%= render :partial => 'issues/list_simple', :locals => { :issues => assigned_issues } %> +<% if assigned_issues.length > 0 %> +<p class="small"><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => 'me' %></p> +<% end %> + +<% content_for :header_tags do %> +<%= auto_discovery_link_tag(:atom, + {:controller => 'issues', :action => 'index', :set_filter => 1, + :assigned_to_id => 'me', :format => 'atom', :key => User.current.rss_key}, + {:title => l(:label_assigned_to_me_issues)}) %> +<% end %> diff --git a/app/views/my/blocks/_issuesreportedbyme.ctp b/app/views/my/blocks/_issuesreportedbyme.ctp new file mode 100644 index 0000000..317aebb --- /dev/null +++ b/app/views/my/blocks/_issuesreportedbyme.ctp @@ -0,0 +1,17 @@ +<h3><%=l(:label_reported_issues)%></h3> +<% reported_issues = Issue.find(:all, + :conditions => ["author_id=? AND #{Project.table_name}.status=#{Project::STATUS_ACTIVE}", user.id], + :limit => 10, + :include => [ :status, :project, :tracker ], + :order => "#{Issue.table_name}.updated_on DESC") %> +<%= render :partial => 'issues/list_simple', :locals => { :issues => reported_issues } %> +<% if reported_issues.length > 0 %> +<p class="small"><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :set_filter => 1, :author_id => 'me' %></p> +<% end %> + +<% content_for :header_tags do %> +<%= auto_discovery_link_tag(:atom, + {:controller => 'issues', :action => 'index', :set_filter => 1, + :author_id => 'me', :format => 'atom', :key => User.current.rss_key}, + {:title => l(:label_reported_issues)}) %> +<% end %> diff --git a/app/views/my/blocks/_issueswatched.ctp b/app/views/my/blocks/_issueswatched.ctp new file mode 100644 index 0000000..e5c2f23 --- /dev/null +++ b/app/views/my/blocks/_issueswatched.ctp @@ -0,0 +1,10 @@ +<h3><%=l(:label_watched_issues)%></h3> +<% watched_issues = Issue.find(:all, + :include => [:status, :project, :tracker, :watchers], + :limit => 10, + :conditions => ["#{Watcher.table_name}.user_id = ? AND #{Project.table_name}.status=#{Project::STATUS_ACTIVE}", user.id], + :order => "#{Issue.table_name}.updated_on DESC") %> +<%= render :partial => 'issues/list_simple', :locals => { :issues => watched_issues } %> +<% if watched_issues.length > 0 %> +<p><%=lwr(:label_last_updates, watched_issues.length)%></p> +<% end %> diff --git a/app/views/my/blocks/_news.ctp b/app/views/my/blocks/_news.ctp new file mode 100644 index 0000000..d86cced --- /dev/null +++ b/app/views/my/blocks/_news.ctp @@ -0,0 +1,8 @@ +<h3><%=l(:label_news_latest)%></h3> + +<%= render(:partial => 'news/news', + :collection => News.find(:all, + :limit => 10, + :order => "#{News.table_name}.created_on DESC", + :conditions => "#{News.table_name}.project_id in (#{@user.projects.collect{|m| m.id}.join(',')})", + :include => [:project, :author])) unless @user.projects.empty? %> \ No newline at end of file diff --git a/app/views/my/blocks/_timelog.ctp b/app/views/my/blocks/_timelog.ctp new file mode 100644 index 0000000..ca66f7e --- /dev/null +++ b/app/views/my/blocks/_timelog.ctp @@ -0,0 +1,52 @@ +<h3><%=l(:label_spent_time)%> (<%= l(:label_last_n_days, 7) %>)</h3> +<% +entries = TimeEntry.find(:all, + :conditions => ["#{TimeEntry.table_name}.user_id = ? AND #{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", @user.id, Date.today - 6, Date.today], + :include => [:activity, :project, {:issue => [:tracker, :status]}], + :order => "#{TimeEntry.table_name}.spent_on DESC, #{Project.table_name}.name ASC, #{Tracker.table_name}.position ASC, #{Issue.table_name}.id ASC") +entries_by_day = entries.group_by(&:spent_on) +%> + +<div class="total-hours"> +<p><%= l(:label_total) %>: <%= html_hours("%.2f" % entries.sum(&:hours).to_f) %></p> +</div> + +<% if entries.any? %> +<table class="list time-entries"> +<thead> +<th><%= l(:label_activity) %></th> +<th><%= l(:label_project) %></th> +<th><%= l(:field_comments) %></th> +<th><%= l(:field_hours) %></th> +<th></th> +</thead> +<tbody> +<% entries_by_day.keys.sort.reverse.each do |day| %> + <tr class="odd"> + <td><strong><%= day == Date.today ? l(:label_today).titleize : format_date(day) %></strong></td> + <td colspan="2"></td> + <td class="hours"><em><%= html_hours("%.2f" % entries_by_day[day].sum(&:hours).to_f) %></em></td> + <td></td> + </tr> + <% entries_by_day[day].each do |entry| -%> + <tr class="time-entry" style="border-bottom: 1px solid #f5f5f5;"> + <td class="activity"><%=h entry.activity %></td> + <td class="subject"><%=h entry.project %> <%= ' - ' + link_to_issue(entry.issue, :title => h("#{entry.issue.subject} (#{entry.issue.status})")) if entry.issue %></td> + <td class="comments"><%=h entry.comments %></td> + <td class="hours"><%= html_hours("%.2f" % entry.hours) %></td> + <td align="center"> + <% if entry.editable_by?(@user) -%> + <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry}, + :title => l(:button_edit) %> + <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry}, + :confirm => l(:text_are_you_sure), + :method => :post, + :title => l(:button_delete) %> + <% end -%> + </td> + </tr> + <% end -%> +<% end -%> +</tbody> +</table> +<% end %> diff --git a/app/views/my/page.ctp b/app/views/my/page.ctp new file mode 100644 index 0000000..4d4c921 --- /dev/null +++ b/app/views/my/page.ctp @@ -0,0 +1,42 @@ +<div class="contextual"> + <%= link_to l(:label_personalize_page), :action => 'page_layout' %> +</div> + +<h2><%=l(:label_my_page)%></h2> + +<div id="list-top"> + <% @blocks['top'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> + <div class="mypage-box"> + <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %> + </div> + <% end if @blocks['top'] %> +</div> + +<div id="list-left" class="splitcontentleft"> + <% @blocks['left'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> + <div class="mypage-box"> + <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %> + </div> + <% end if @blocks['left'] %> +</div> + +<div id="list-right" class="splitcontentright"> + <% @blocks['right'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> + <div class="mypage-box"> + <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %> + </div> + <% end if @blocks['right'] %> +</div> + +<% content_for :header_tags do %> + <%= javascript_include_tag 'context_menu' %> + <%= stylesheet_link_tag 'context_menu' %> +<% end %> + +<div id="context-menu" style="display: none;"></div> +<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %> + +<% html_title(l(:label_my_page)) -%> diff --git a/app/views/my/page_layout.ctp b/app/views/my/page_layout.ctp new file mode 100644 index 0000000..1e348bf --- /dev/null +++ b/app/views/my/page_layout.ctp @@ -0,0 +1,107 @@ +<script language="JavaScript"> +//<![CDATA[ +function recreateSortables() { + Sortable.destroy('list-top'); + Sortable.destroy('list-left'); + Sortable.destroy('list-right'); + + Sortable.create("list-top", {constraint:false, containment:['list-top','list-left','list-right'], dropOnEmpty:true, handle:'handle', onUpdate:function(){new Ajax.Request('/my/order_blocks?group=top', {asynchronous:true, evalScripts:true, parameters:Sortable.serialize("list-top")})}, only:'mypage-box', tag:'div'}) + Sortable.create("list-left", {constraint:false, containment:['list-top','list-left','list-right'], dropOnEmpty:true, handle:'handle', onUpdate:function(){new Ajax.Request('/my/order_blocks?group=left', {asynchronous:true, evalScripts:true, parameters:Sortable.serialize("list-left")})}, only:'mypage-box', tag:'div'}) + Sortable.create("list-right", {constraint:false, containment:['list-top','list-left','list-right'], dropOnEmpty:true, handle:'handle', onUpdate:function(){new Ajax.Request('/my/order_blocks?group=right', {asynchronous:true, evalScripts:true, parameters:Sortable.serialize("list-right")})}, only:'mypage-box', tag:'div'}) +} + +function updateSelect() { + s = $('block-select') + for (var i = 0; i < s.options.length; i++) { + if ($('block_' + s.options[i].value)) { + s.options[i].disabled = true; + } else { + s.options[i].disabled = false; + } + } + s.options[0].selected = true; +} + +function afterAddBlock() { + recreateSortables(); + updateSelect(); +} + +function removeBlock(block) { + Effect.DropOut(block); + updateSelect(); +} +//]]> +</script> + +<div class="contextual"> +<% form_tag({:action => "add_block"}, :id => "block-form") do %> +<%= select_tag 'block', "<option></option>" + options_for_select(@block_options), :id => "block-select" %> +<%= link_to_remote l(:button_add), + {:url => { :action => "add_block" }, + :with => "Form.serialize('block-form')", + :update => "list-top", + :position => :top, + :complete => "afterAddBlock();" + }, :class => 'icon icon-add' + %> +<% end %> +<%= link_to l(:button_save), {:action => 'page_layout_save'}, :class => 'icon icon-save' %> +<%= link_to l(:button_cancel), {:action => 'page'}, :class => 'icon icon-cancel' %> +</div> + +<h2><%=l(:label_my_page)%></h2> + +<div id="list-top" class="block-receiver"> + <% @blocks['top'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> + <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %> + <% end if @blocks['top'] %> +</div> + +<div id="list-left" class="splitcontentleft block-receiver"> + <% @blocks['left'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> + <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %> + <% end if @blocks['left'] %> +</div> + +<div id="list-right" class="splitcontentright block-receiver"> + <% @blocks['right'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> + <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %> + <% end if @blocks['right'] %> +</div> + +<%= sortable_element 'list-top', + :tag => 'div', + :only => 'mypage-box', + :handle => "handle", + :dropOnEmpty => true, + :containment => ['list-top', 'list-left', 'list-right'], + :constraint => false, + :url => { :action => "order_blocks", :group => "top" } + %> + + +<%= sortable_element 'list-left', + :tag => 'div', + :only => 'mypage-box', + :handle => "handle", + :dropOnEmpty => true, + :containment => ['list-top', 'list-left', 'list-right'], + :constraint => false, + :url => { :action => "order_blocks", :group => "left" } + %> + +<%= sortable_element 'list-right', + :tag => 'div', + :only => 'mypage-box', + :handle => "handle", + :dropOnEmpty => true, + :containment => ['list-top', 'list-left', 'list-right'], + :constraint => false, + :url => { :action => "order_blocks", :group => "right" } + %> + +<%= javascript_tag "updateSelect()" %> diff --git a/app/views/my/password.ctp b/app/views/my/password.ctp new file mode 100644 index 0000000..2e9fd0f --- /dev/null +++ b/app/views/my/password.ctp @@ -0,0 +1,22 @@ +<h2><%=l(:button_change_password)%></h2> + +<%= error_messages_for 'user' %> + +<% form_tag({}, :class => "tabular") do %> +<div class="box"> +<p><label for="password"><%=l(:field_password)%> <span class="required">*</span></label> +<%= password_field_tag 'password', nil, :size => 25 %></p> + +<p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label> +<%= password_field_tag 'new_password', nil, :size => 25 %><br /> +<em><%= l(:text_caracters_minimum, 4) %></em></p> + +<p><label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label> +<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %></p> +</div> +<%= submit_tag l(:button_apply) %> +<% end %> + +<% content_for :sidebar do %> +<%= render :partial => 'sidebar' %> +<% end %> diff --git a/app/views/news/_form.ctp b/app/views/news/_form.ctp new file mode 100644 index 0000000..0cfe7a6 --- /dev/null +++ b/app/views/news/_form.ctp @@ -0,0 +1,8 @@ +<%= error_messages_for 'news' %> +<div class="box tabular"> +<p><%= f.text_field :title, :required => true, :size => 60 %></p> +<p><%= f.text_area :summary, :cols => 60, :rows => 2 %></p> +<p><%= f.text_area :description, :required => true, :cols => 60, :rows => 15, :class => 'wiki-edit' %></p> +</div> + +<%= wikitoolbar_for 'news_description' %> diff --git a/app/views/news/_news.ctp b/app/views/news/_news.ctp new file mode 100644 index 0000000..e26d2c4 --- /dev/null +++ b/app/views/news/_news.ctp @@ -0,0 +1,6 @@ +<p><%= link_to(h(news.project.name), :controller => 'projects', :action => 'show', :id => news.project) + ': ' unless @project %> +<%= link_to h(news.title), :controller => 'news', :action => 'show', :id => news %> +<%= "(#{news.comments_count} #{lwr(:label_comment, news.comments_count).downcase})" if news.comments_count > 0 %> +<br /> +<% unless news.summary.blank? %><span class="summary"><%=h news.summary %></span><br /><% end %> +<span class="author"><%= authoring news.created_on, news.author %></span></p> diff --git a/app/views/news/edit.ctp b/app/views/news/edit.ctp new file mode 100644 index 0000000..4be566e --- /dev/null +++ b/app/views/news/edit.ctp @@ -0,0 +1,14 @@ +<h2><%=l(:label_news)%></h2> + +<% labelled_tabular_form_for :news, @news, :url => { :action => "edit" }, + :html => { :id => 'news-form' } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<%= link_to_remote l(:label_preview), + { :url => { :controller => 'news', :action => 'preview', :project_id => @project }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('news-form')" + }, :accesskey => accesskey(:preview) %> +<% end %> +<div id="preview" class="wiki"></div> diff --git a/app/views/news/index.ctp b/app/views/news/index.ctp new file mode 100644 index 0000000..9cac390 --- /dev/null +++ b/app/views/news/index.ctp @@ -0,0 +1,51 @@ +<div class="contextual"> +<%= link_to_if_authorized(l(:label_news_new), + {:controller => 'news', :action => 'new', :project_id => @project}, + :class => 'icon icon-add', + :onclick => 'Element.show("add-news"); return false;') if @project %> +</div> + +<div id="add-news" style="display:none;"> +<h2><%=l(:label_news_new)%></h2> +<% labelled_tabular_form_for :news, @news, :url => { :controller => 'news', :action => 'new', :project_id => @project }, + :html => { :id => 'news-form' } do |f| %> +<%= render :partial => 'news/form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<%= link_to_remote l(:label_preview), + { :url => { :controller => 'news', :action => 'preview', :project_id => @project }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('news-form')" + }, :accesskey => accesskey(:preview) %> | +<%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-news")' %> +<% end if @project %> +<div id="preview" class="wiki"></div> +</div> + +<h2><%=l(:label_news_plural)%></h2> + +<% if @newss.empty? %> +<p class="nodata"><%= l(:label_no_data) %></p> +<% else %> +<% @newss.each do |news| %> + <h3><%= link_to(h(news.project.name), :controller => 'projects', :action => 'show', :id => news.project) + ': ' unless news.project == @project %> + <%= link_to h(news.title), :controller => 'news', :action => 'show', :id => news %> + <%= "(#{news.comments_count} #{lwr(:label_comment, news.comments_count).downcase})" if news.comments_count > 0 %></h3> + <p class="author"><%= authoring news.created_on, news.author %></p> + <div class="wiki"> + <%= textilizable(news.description) %> + </div> +<% end %> +<% end %> +<p class="pagination"><%= pagination_links_full @news_pages %></p> + +<p class="other-formats"> +<%= l(:label_export_to) %> +<span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span> +</p> + +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :page => nil, :key => User.current.rss_key})) %> +<% end %> + +<% html_title(l(:label_news_plural)) -%> diff --git a/app/views/news/new.ctp b/app/views/news/new.ctp new file mode 100644 index 0000000..a4d29a0 --- /dev/null +++ b/app/views/news/new.ctp @@ -0,0 +1,14 @@ +<h2><%=l(:label_news_new)%></h2> + +<% labelled_tabular_form_for :news, @news, :url => { :controller => 'news', :action => 'new', :project_id => @project }, + :html => { :id => 'news-form' } do |f| %> +<%= render :partial => 'news/form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<%= link_to_remote l(:label_preview), + { :url => { :controller => 'news', :action => 'preview', :project_id => @project }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('news-form')" + }, :accesskey => accesskey(:preview) %> +<% end %> +<div id="preview" class="wiki"></div> diff --git a/app/views/news/show.ctp b/app/views/news/show.ctp new file mode 100644 index 0000000..78be9c2 --- /dev/null +++ b/app/views/news/show.ctp @@ -0,0 +1,61 @@ +<div class="contextual"> +<%= link_to_if_authorized l(:button_edit), + {:controller => 'news', :action => 'edit', :id => @news}, + :class => 'icon icon-edit', + :accesskey => accesskey(:edit), + :onclick => 'Element.show("edit-news"); return false;' %> +<%= link_to_if_authorized l(:button_delete), {:controller => 'news', :action => 'destroy', :id => @news}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> +</div> + +<h2><%=h @news.title %></h2> + +<div id="edit-news" style="display:none;"> +<% labelled_tabular_form_for :news, @news, :url => { :action => "edit", :id => @news }, + :html => { :id => 'news-form' } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<%= link_to_remote l(:label_preview), + { :url => { :controller => 'news', :action => 'preview', :project_id => @project }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('news-form')" + }, :accesskey => accesskey(:preview) %> | +<%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("edit-news")' %> +<% end %> +<div id="preview" class="wiki"></div> +</div> + +<p><em><% unless @news.summary.blank? %><%=h @news.summary %><br /><% end %> +<span class="author"><%= authoring @news.created_on, @news.author %></span></em></p> +<div class="wiki"> +<%= textilizable(@news.description) %> +</div> +<br /> + +<div id="comments" style="margin-bottom:16px;"> +<h3 class="icon22 icon22-comment"><%= l(:label_comment_plural) %></h3> +<% @comments.each do |comment| %> + <% next if comment.new_record? %> + <div class="contextual"> + <%= link_to_if_authorized image_tag('delete.png'), {:controller => 'news', :action => 'destroy_comment', :id => @news, :comment_id => comment}, + :confirm => l(:text_are_you_sure), :method => :post, :title => l(:button_delete) %> + </div> + <h4><%= authoring comment.created_on, comment.author %></h4> + <%= textilizable(comment.comments) %> +<% end if @comments.any? %> +</div> + +<% if authorize_for 'news', 'add_comment' %> +<p><%= toggle_link l(:label_comment_add), "add_comment_form", :focus => "comment_comments" %></p> +<% form_tag({:action => 'add_comment', :id => @news}, :id => "add_comment_form", :style => "display:none;") do %> +<%= text_area 'comment', 'comments', :cols => 80, :rows => 15, :class => 'wiki-edit' %> +<%= wikitoolbar_for 'comment_comments' %> +<p><%= submit_tag l(:button_add) %></p> +<% end %> +<% end %> + +<% html_title @news.title -%> + +<% content_for :header_tags do %> + <%= stylesheet_link_tag 'scm' %> +<% end %> diff --git a/app/views/projects/_edit.ctp b/app/views/projects/_edit.ctp new file mode 100644 index 0000000..b7c2987 --- /dev/null +++ b/app/views/projects/_edit.ctp @@ -0,0 +1,4 @@ +<% labelled_tabular_form_for :project, @project, :url => { :action => "edit", :id => @project } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/projects/_form.ctp b/app/views/projects/_form.ctp new file mode 100644 index 0000000..f0c9fda --- /dev/null +++ b/app/views/projects/_form.ctp @@ -0,0 +1,49 @@ +<%= error_messages_for 'project' %> + +<div class="box"> +<!--[form:project]--> +<p><%= f.text_field :name, :required => true %><br /><em><%= l(:text_caracters_maximum, 30) %></em></p> + +<% if User.current.admin? and !@root_projects.empty? %> + <p><%= f.select :parent_id, (@root_projects.collect {|p| [p.name, p.id]}), { :include_blank => true } %></p> +<% end %> + +<p><%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %></p> +<p><%= f.text_field :identifier, :required => true, :disabled => @project.identifier_frozen? %> +<% unless @project.identifier_frozen? %> +<br /><em><%= l(:text_length_between, 2, 20) %> <%= l(:text_project_identifier_info) %></em> +<% end %></p> +<p><%= f.text_field :homepage, :size => 60 %></p> +<p><%= f.check_box :is_public %></p> +<%= wikitoolbar_for 'project_description' %> + +<% @project.custom_field_values.each do |value| %> + <p><%= custom_field_tag_with_label :project, value %></p> +<% end %> +<%= call_hook(:view_projects_form, :project => @project, :form => f) %> +</div> + +<% unless @trackers.empty? %> +<fieldset class="box"><legend><%=l(:label_tracker_plural)%></legend> +<% @trackers.each do |tracker| %> + <label class="floating"> + <%= check_box_tag 'project[tracker_ids][]', tracker.id, @project.trackers.include?(tracker) %> + <%= tracker %> + </label> +<% end %> +<%= hidden_field_tag 'project[tracker_ids][]', '' %> +</fieldset> +<% end %> + +<% unless @issue_custom_fields.empty? %> +<fieldset class="box"><legend><%=l(:label_custom_field_plural)%></legend> +<% @issue_custom_fields.each do |custom_field| %> + <label class="floating"> + <%= check_box_tag 'project[issue_custom_field_ids][]', custom_field.id, (@project.all_issue_custom_fields.include? custom_field), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %> + <%= custom_field.name %> + </label> +<% end %> +<%= hidden_field_tag 'project[issue_custom_field_ids][]', '' %> +</fieldset> +<% end %> +<!--[eoform:project]--> diff --git a/app/views/projects/activity.ctp b/app/views/projects/activity.ctp new file mode 100644 index 0000000..b0e5366 --- /dev/null +++ b/app/views/projects/activity.ctp @@ -0,0 +1,60 @@ +<h2><%= @author.nil? ? l(:label_activity) : l(:label_user_activity, link_to_user(@author)) %></h2> +<p class="subtitle"><%= "#{l(:label_date_from)} #{format_date(@date_to - @days)} #{l(:label_date_to).downcase} #{format_date(@date_to-1)}" %></p> + +<div id="activity"> +<% @events_by_day.keys.sort.reverse.each do |day| %> +<h3><%= format_activity_day(day) %></h3> +<dl> +<% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%> + <dt class="<%= e.event_type %> <%= User.current.logged? && e.respond_to?(:event_author) && User.current == e.event_author ? 'me' : nil %>"> + <%= avatar(e.event_author, :size => "24") if e.respond_to?(:event_author) %> + <span class="time"><%= format_time(e.event_datetime, false) %></span> + <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %> + <%= link_to format_activity_title(e.event_title), e.event_url %></dt> + <dd><span class="description"><%= format_activity_description(e.event_description) %></span> + <span class="author"><%= e.event_author if e.respond_to?(:event_author) %></span></dd> +<% end -%> +</dl> +<% end -%> +</div> + +<%= content_tag('p', l(:label_no_data), :class => 'nodata') if @events_by_day.empty? %> + +<div style="float:left;"> +<%= link_to_remote(('&#171; ' + l(:label_previous)), + {:update => "content", :url => params.merge(:from => @date_to - @days - 1), :complete => 'window.scrollTo(0,0)'}, + {:href => url_for(params.merge(:from => @date_to - @days - 1)), + :title => "#{l(:label_date_from)} #{format_date(@date_to - 2*@days)} #{l(:label_date_to).downcase} #{format_date(@date_to - @days - 1)}"}) %> +</div> +<div style="float:right;"> +<%= link_to_remote((l(:label_next) + ' &#187;'), + {:update => "content", :url => params.merge(:from => @date_to + @days - 1), :complete => 'window.scrollTo(0,0)'}, + {:href => url_for(params.merge(:from => @date_to + @days - 1)), + :title => "#{l(:label_date_from)} #{format_date(@date_to)} #{l(:label_date_to).downcase} #{format_date(@date_to + @days - 1)}"}) unless @date_to >= Date.today %> +</div> +&nbsp; +<p class="other-formats"> + <%= l(:label_export_to) %> + <%= link_to 'Atom', params.merge(:format => :atom, :from => nil, :key => User.current.rss_key), :class => 'feed' %> +</p> + +<% content_for :header_tags do %> +<%= auto_discovery_link_tag(:atom, params.merge(:format => 'atom', :from => nil, :key => User.current.rss_key)) %> +<% end %> + +<% content_for :sidebar do %> +<% form_tag({}, :method => :get) do %> +<h3><%= l(:label_activity) %></h3> +<p><% @activity.event_types.each do |t| %> +<label><%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label><br /> +<% end %></p> +<% if @project && @project.active_children.any? %> + <p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p> + <%= hidden_field_tag 'with_subprojects', 0 %> +<% end %> +<%= hidden_field_tag('user_id', params[:user_id]) unless params[:user_id].blank? %> +<p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p> +<% end %> +<% end %> + +<% html_title(l(:label_activity), @author) -%> diff --git a/app/views/projects/add.ctp b/app/views/projects/add.ctp new file mode 100644 index 0000000..bc3d7b0 --- /dev/null +++ b/app/views/projects/add.ctp @@ -0,0 +1,16 @@ +<h2><%=l(:label_project_new)%></h2> + +<% labelled_tabular_form_for :project, @project, :url => { :action => "add" } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> + +<fieldset class="box"><legend><%= l(:label_module_plural) %></legend> +<% Redmine::AccessControl.available_project_modules.each do |m| %> + <label class="floating"> + <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %> + <%= (l_has_string?("project_module_#{m}".to_sym) ? l("project_module_#{m}".to_sym) : m.to_s.humanize) %> + </label> +<% end %> +</fieldset> + +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/projects/add_file.ctp b/app/views/projects/add_file.ctp new file mode 100644 index 0000000..ab9c735 --- /dev/null +++ b/app/views/projects/add_file.ctp @@ -0,0 +1,16 @@ +<h2><%=l(:label_attachment_new)%></h2> + +<%= error_messages_for 'attachment' %> +<div class="box"> +<% form_tag({ :action => 'add_file', :id => @project }, :multipart => true, :class => "tabular") do %> + +<% if @versions.any? %> +<p><label for="version_id"><%=l(:field_version)%></label> +<%= select_tag "version_id", content_tag('option', '') + + options_from_collection_for_select(@versions, "id", "name") %></p> +<% end %> + +<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p> +</div> +<%= submit_tag l(:button_add) %> +<% end %> diff --git a/app/views/projects/add_issue_category.ctp b/app/views/projects/add_issue_category.ctp new file mode 100644 index 0000000..08bc6d0 --- /dev/null +++ b/app/views/projects/add_issue_category.ctp @@ -0,0 +1,6 @@ +<h2><%=l(:label_issue_category_new)%></h2> + +<% labelled_tabular_form_for :category, @category, :url => { :action => 'add_issue_category' } do |f| %> +<%= render :partial => 'issue_categories/form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<% end %> diff --git a/app/views/projects/add_version.ctp b/app/views/projects/add_version.ctp new file mode 100644 index 0000000..c038b7d --- /dev/null +++ b/app/views/projects/add_version.ctp @@ -0,0 +1,6 @@ +<h2><%=l(:label_version_new)%></h2> + +<% labelled_tabular_form_for :version, @version, :url => { :action => 'add_version' } do |f| %> +<%= render :partial => 'versions/form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<% end %> \ No newline at end of file diff --git a/app/views/projects/changelog.ctp b/app/views/projects/changelog.ctp new file mode 100644 index 0000000..e4d32a3 --- /dev/null +++ b/app/views/projects/changelog.ctp @@ -0,0 +1,42 @@ +<h2><%=l(:label_change_log)%></h2> + +<% if @versions.empty? %> +<p class="nodata"><%= l(:label_no_data) %></p> +<% end %> + +<% @versions.each do |version| %> + <a name="<%= version.name %>"><h3 class="icon22 icon22-package"><%= version.name %></h3></a> + <% if version.effective_date %> + <p><%= format_date(version.effective_date) %></p> + <% end %> + <p><%=h version.description %></p> + <% issues = version.fixed_issues.find(:all, + :include => [:status, :tracker], + :conditions => ["#{IssueStatus.table_name}.is_closed=? AND #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')})", true], + :order => "#{Tracker.table_name}.position") unless @selected_tracker_ids.empty? + issues ||= [] + %> + <% if !issues.empty? %> + <ul> + <% issues.each do |issue| %> + <li><%= link_to_issue(issue) %>: <%=h issue.subject %></li> + <% end %> + </ul> + <% end %> +<% end %> + +<% content_for :sidebar do %> +<% form_tag do %> +<h3><%= l(:label_change_log) %></h3> +<% @trackers.each do |tracker| %> + <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s) %> + <%= tracker.name %></label><br /> +<% end %> +<p><%= submit_tag l(:button_apply), :class => 'button-small' %></p> +<% end %> + +<h3><%= l(:label_version_plural) %></h3> +<% @versions.each do |version| %> +<%= link_to version.name, :anchor => version.name %><br /> +<% end %> +<% end %> diff --git a/app/views/projects/destroy.ctp b/app/views/projects/destroy.ctp new file mode 100644 index 0000000..a1913c1 --- /dev/null +++ b/app/views/projects/destroy.ctp @@ -0,0 +1,16 @@ +<h2><%=l(:label_confirmation)%></h2> +<div class="warning"> +<p><strong><%=h @project_to_destroy %></strong><br /> +<%=l(:text_project_destroy_confirmation)%> + +<% if @project_to_destroy.children.any? %> +<br /><%= l(:text_subprojects_destroy_warning, content_tag('strong', h(@project_to_destroy.children.sort.collect{|p| p.to_s}.join(', ')))) %> +<% end %> +</p> +<p> + <% form_tag({:controller => 'projects', :action => 'destroy', :id => @project_to_destroy}) do %> + <label><%= check_box_tag 'confirm', 1 %> <%= l(:general_text_Yes) %></label> + <%= submit_tag l(:button_delete) %> + <% end %> +</p> +</div> diff --git a/app/views/projects/index.ctp b/app/views/projects/index.ctp new file mode 100644 index 0000000..4c68717 --- /dev/null +++ b/app/views/projects/index.ctp @@ -0,0 +1,31 @@ +<div class="contextual"> + <%= link_to(l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add') + ' |' if User.current.admin? %> + <%= link_to l(:label_issue_view_all), { :controller => 'issues' } %> | + <%= link_to l(:label_overall_activity), { :controller => 'projects', :action => 'activity' }%> +</div> + +<h2><%=l(:label_project_plural)%></h2> + +<% @project_tree.keys.sort.each do |project| %> +<h3><%= link_to h(project.name), {:action => 'show', :id => project}, :class => (User.current.member_of?(project) ? "icon icon-fav" : "") %></h3> +<%= textilizable(project.short_description, :project => project) %> + +<% if @project_tree[project].any? %> + <p><%= l(:label_subproject_plural) %>: + <%= @project_tree[project].sort.collect {|subproject| + link_to(h(subproject.name), {:action => 'show', :id => subproject}, :class => (User.current.member_of?(subproject) ? "icon icon-fav" : ""))}.join(', ') %></p> +<% end %> +<% end %> + +<% if User.current.logged? %> +<p style="text-align:right;"> +<span class="icon icon-fav"><%= l(:label_my_projects) %></span> +</p> +<% end %> + +<p class="other-formats"> +<%= l(:label_export_to) %> +<span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span> +</p> + +<% html_title(l(:label_project_plural)) -%> diff --git a/app/views/projects/list_files.ctp b/app/views/projects/list_files.ctp new file mode 100644 index 0000000..0871ba2 --- /dev/null +++ b/app/views/projects/list_files.ctp @@ -0,0 +1,42 @@ +<div class="contextual"> +<%= link_to_if_authorized l(:label_attachment_new), {:controller => 'projects', :action => 'add_file', :id => @project}, :class => 'icon icon-add' %> +</div> + +<h2><%=l(:label_attachment_plural)%></h2> + +<% delete_allowed = User.current.allowed_to?(:manage_files, @project) %> + +<table class="list"> + <thead><tr> + <%= sort_header_tag('filename', :caption => l(:field_filename)) %> + <%= sort_header_tag('created_on', :caption => l(:label_date), :default_order => 'desc') %> + <%= sort_header_tag('size', :caption => l(:field_filesize), :default_order => 'desc') %> + <%= sort_header_tag('downloads', :caption => l(:label_downloads_abbr), :default_order => 'desc') %> + <th>MD5</th> + <th></th> + </tr></thead> + <tbody> +<% @containers.each do |container| %> + <% next if container.attachments.empty? -%> + <% if container.is_a?(Version) -%> + <tr><th colspan="6" align="left"><span class="icon icon-package"><b><%=h container %></b></span></th></tr> + <% end -%> + <% container.attachments.each do |file| %> + <tr class="<%= cycle("odd", "even") %>"> + <td><%= link_to_attachment file, :download => true, :title => file.description %></td> + <td align="center"><%= format_time(file.created_on) %></td> + <td align="center"><%= number_to_human_size(file.filesize) %></td> + <td align="center"><%= file.downloads %></td> + <td align="center"><small><%= file.digest %></small></td> + <td align="center"> + <%= link_to(image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => file}, + :confirm => l(:text_are_you_sure), :method => :post) if delete_allowed %> + </td> + </tr> + <% end + reset_cycle %> +<% end %> + </tbody> +</table> + +<% html_title(l(:label_attachment_plural)) -%> diff --git a/app/views/projects/list_members.ctp b/app/views/projects/list_members.ctp new file mode 100644 index 0000000..fcfb4f7 --- /dev/null +++ b/app/views/projects/list_members.ctp @@ -0,0 +1,13 @@ +<h2><%=l(:label_member_plural)%></h2> + +<% if @members.empty? %><p><i><%= l(:label_no_data) %></i></p><% end %> + +<% members = @members.group_by {|m| m.role } %> +<% members.keys.sort{|x,y| x.position <=> y.position}.each do |role| %> +<h3><%= role.name %></h3> +<ul> +<% members[role].each do |m| %> +<li><%= link_to m.name, :controller => 'account', :action => 'show', :id => m.user %> (<%= format_date m.created_on %>)</li> +<% end %> +</ul> +<% end %> diff --git a/app/views/projects/roadmap.ctp b/app/views/projects/roadmap.ctp new file mode 100644 index 0000000..0778d81 --- /dev/null +++ b/app/views/projects/roadmap.ctp @@ -0,0 +1,50 @@ +<h2><%=l(:label_roadmap)%></h2> + +<% if @versions.empty? %> +<p class="nodata"><%= l(:label_no_data) %></p> +<% else %> +<div id="roadmap"> +<% @versions.each do |version| %> + <%= tag 'a', :name => version.name %> + <h3 class="icon22 icon22-package"><%= link_to h(version.name), :controller => 'versions', :action => 'show', :id => version %></h3> + <%= render :partial => 'versions/overview', :locals => {:version => version} %> + <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %> + + <% issues = version.fixed_issues.find(:all, + :include => [:status, :tracker], + :conditions => ["tracker_id in (#{@selected_tracker_ids.join(',')})"], + :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") unless @selected_tracker_ids.empty? + issues ||= [] + %> + <% if issues.size > 0 %> + <fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend> + <ul> + <%- issues.each do |issue| -%> + <li><%= link_to_issue(issue) %>: <%=h issue.subject %></li> + <%- end -%> + </ul> + </fieldset> + <% end %> +<% end %> +</div> +<% end %> + +<% content_for :sidebar do %> +<% form_tag({}, :method => :get) do %> +<h3><%= l(:label_roadmap) %></h3> +<% @trackers.each do |tracker| %> + <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s), :id => nil %> + <%= tracker.name %></label><br /> +<% end %> +<br /> +<label for="completed"><%= check_box_tag "completed", 1, params[:completed] %> <%= l(:label_show_completed_versions) %></label> +<p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p> +<% end %> + +<h3><%= l(:label_version_plural) %></h3> +<% @versions.each do |version| %> +<%= link_to version.name, "##{version.name}" %><br /> +<% end %> +<% end %> + +<% html_title(l(:label_roadmap)) %> diff --git a/app/views/projects/settings.ctp b/app/views/projects/settings.ctp new file mode 100644 index 0000000..c7b0f50 --- /dev/null +++ b/app/views/projects/settings.ctp @@ -0,0 +1,24 @@ +<h2><%=l(:label_settings)%></h2> + +<% tabs = project_settings_tabs %> +<% selected_tab = params[:tab] ? params[:tab].to_s : tabs.first[:name] %> + +<div class="tabs"> +<ul> +<% tabs.each do |tab| -%> + <li><%= link_to l(tab[:label]), { :tab => tab[:name] }, + :id => "tab-#{tab[:name]}", + :class => (tab[:name] != selected_tab ? nil : 'selected'), + :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %></li> +<% end -%> +</ul> +</div> + +<% tabs.each do |tab| -%> +<%= content_tag('div', render(:partial => tab[:partial]), + :id => "tab-content-#{tab[:name]}", + :style => (tab[:name] != selected_tab ? 'display:none' : nil), + :class => 'tab-content') %> +<% end -%> + +<% html_title(l(:label_settings)) -%> diff --git a/app/views/projects/settings/_boards.ctp b/app/views/projects/settings/_boards.ctp new file mode 100644 index 0000000..bf6da3a --- /dev/null +++ b/app/views/projects/settings/_boards.ctp @@ -0,0 +1,28 @@ +<% if @project.boards.any? %> +<table class="list"> + <thead><th><%= l(:label_board) %></th><th><%= l(:field_description) %></th><th style="width:15%"></th><th style="width:15%"></th><th style="width:15%"></th></thead> + <tbody> +<% @project.boards.each do |board| + next if board.new_record? %> + <tr class="<%= cycle 'odd', 'even' %>"> + <td><%=h board.name %></td> + <td><%=h board.description %></td> + <td align="center"> + <% if authorize_for("boards", "edit") %> + <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:controller => 'boards', :action => 'edit', :project_id => @project, :id => board, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> + <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:controller => 'boards', :action => 'edit', :project_id => @project, :id => board, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> - + <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:controller => 'boards', :action => 'edit', :project_id => @project, :id => board, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %> + <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:controller => 'boards', :action => 'edit', :project_id => @project, :id => board, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %> + <% end %> + </td> + <td align="center"><%= link_to_if_authorized l(:button_edit), {:controller => 'boards', :action => 'edit', :project_id => @project, :id => board}, :class => 'icon icon-edit' %></td> + <td align="center"><%= link_to_if_authorized l(:button_delete), {:controller => 'boards', :action => 'destroy', :project_id => @project, :id => board}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %></td> + </tr> +<% end %> + </tbody> +</table> +<% else %> +<p class="nodata"><%= l(:label_no_data) %></p> +<% end %> + +<p><%= link_to_if_authorized l(:label_board_new), {:controller => 'boards', :action => 'new', :project_id => @project} %></p> diff --git a/app/views/projects/settings/_issue_categories.ctp b/app/views/projects/settings/_issue_categories.ctp new file mode 100644 index 0000000..6d9dc0d --- /dev/null +++ b/app/views/projects/settings/_issue_categories.ctp @@ -0,0 +1,26 @@ +<% if @project.issue_categories.any? %> +<table class="list"> + <thead> + <th><%= l(:label_issue_category) %></th> + <th><%= l(:field_assigned_to) %></th> + <th style="width:15%"></th> + <th style="width:15%"></th> + </thead> + <tbody> +<% for category in @project.issue_categories %> + <% unless category.new_record? %> + <tr class="<%= cycle 'odd', 'even' %>"> + <td><%=h(category.name) %></td> + <td><%=h(category.assigned_to.name) if category.assigned_to %></td> + <td align="center"><%= link_to_if_authorized l(:button_edit), { :controller => 'issue_categories', :action => 'edit', :id => category }, :class => 'icon icon-edit' %></td> + <td align="center"><%= link_to_if_authorized l(:button_delete), {:controller => 'issue_categories', :action => 'destroy', :id => category}, :method => :post, :class => 'icon icon-del' %></td> + </tr> + <% end %> +<% end %> + </tbody> +</table> +<% else %> +<p class="nodata"><%= l(:label_no_data) %></p> +<% end %> + +<p><%= link_to_if_authorized l(:label_issue_category_new), :controller => 'projects', :action => 'add_issue_category', :id => @project %></p> diff --git a/app/views/projects/settings/_members.ctp b/app/views/projects/settings/_members.ctp new file mode 100644 index 0000000..20806fe --- /dev/null +++ b/app/views/projects/settings/_members.ctp @@ -0,0 +1,50 @@ +<%= error_messages_for 'member' %> +<% roles = Role.find_all_givable %> +<% users = User.active.find(:all).sort - @project.users %> +<% # members sorted by role position + members = @project.members.find(:all, :include => [:role, :user]).sort %> + +<% if members.any? %> +<table class="list"> + <thead> + <th><%= l(:label_user) %></th> + <th><%= l(:label_role) %></th> + <th style="width:15%"></th> + <%= call_hook(:view_projects_settings_members_table_header, :project => @project) %> + </thead> + <tbody> + <% members.each do |member| %> + <% next if member.new_record? %> + <tr class="<%= cycle 'odd', 'even' %>"> + <td><%= member.name %></td> + <td align="center"> + <% if authorize_for('members', 'edit') %> + <% remote_form_for(:member, member, :url => {:controller => 'members', :action => 'edit', :id => member}, :method => :post) do |f| %> + <%= f.select :role_id, roles.collect{|role| [role.name, role.id]}, {}, :class => "small" %> + <%= submit_tag l(:button_change), :class => "small" %> + <% end %> + <% end %> + </td> + <td align="center"> + <%= link_to_remote l(:button_delete), { :url => {:controller => 'members', :action => 'destroy', :id => member}, + :method => :post + }, :title => l(:button_delete), + :class => 'icon icon-del' %> + </td> + <%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %> + </tr> + </tbody> +<% end; reset_cycle %> +</table> +<% else %> +<p class="nodata"><%= l(:label_no_data) %></p> +<% end %> + +<% if authorize_for('members', 'new') && !users.empty? %> + <% remote_form_for(:member, @member, :url => {:controller => 'members', :action => 'new', :id => @project}, :method => :post) do |f| %> + <p><label for="member_user_id"><%=l(:label_member_new)%></label><br /> + <%= f.select :user_id, users.collect{|user| [user.name, user.id]} %> + <%= l(:label_role) %>: <%= f.select :role_id, roles.collect{|role| [role.name, role.id]}, :selected => nil %> + <%= submit_tag l(:button_add) %></p> + <% end %> +<% end %> diff --git a/app/views/projects/settings/_modules.ctp b/app/views/projects/settings/_modules.ctp new file mode 100644 index 0000000..b4decf7 --- /dev/null +++ b/app/views/projects/settings/_modules.ctp @@ -0,0 +1,17 @@ +<% form_for :project, @project, + :url => { :action => 'modules', :id => @project }, + :html => {:id => 'modules-form'} do |f| %> + +<div class=box> +<strong><%= l(:text_select_project_modules) %></strong> + +<% Redmine::AccessControl.available_project_modules.each do |m| %> +<p><label><%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) -%> + <%= (l_has_string?("project_module_#{m}".to_sym) ? l("project_module_#{m}".to_sym) : m.to_s.humanize) %></label></p> +<% end %> +</div> + +<p><%= check_all_links 'modules-form' %></p> +<p><%= submit_tag l(:button_save) %></p> + +<% end %> diff --git a/app/views/projects/settings/_repository.ctp b/app/views/projects/settings/_repository.ctp new file mode 100644 index 0000000..cdfb7fe --- /dev/null +++ b/app/views/projects/settings/_repository.ctp @@ -0,0 +1,24 @@ +<% remote_form_for :repository, @repository, + :url => { :controller => 'repositories', :action => 'edit', :id => @project }, + :builder => TabularFormBuilder, + :lang => current_language do |f| %> + +<%= error_messages_for 'repository' %> + +<div class="box tabular"> +<p><label><%= l(:label_scm) %></label><%= scm_select_tag(@repository) %></p> +<%= repository_field_tags(f, @repository) if @repository %> +</div> + +<div class="contextual"> +<% if @repository && !@repository.new_record? %> +<%= link_to(l(:label_user_plural), {:controller => 'repositories', :action => 'committers', :id => @project}, :class => 'icon icon-user') %> +<%= link_to(l(:button_delete), {:controller => 'repositories', :action => 'destroy', :id => @project}, + :confirm => l(:text_are_you_sure), + :method => :post, + :class => 'icon icon-del') %> +<% end %> +</div> + +<%= submit_tag((@repository.nil? || @repository.new_record?) ? l(:button_create) : l(:button_save), :disabled => @repository.nil?) %> +<% end %> diff --git a/app/views/projects/settings/_versions.ctp b/app/views/projects/settings/_versions.ctp new file mode 100644 index 0000000..79d92d8 --- /dev/null +++ b/app/views/projects/settings/_versions.ctp @@ -0,0 +1,28 @@ +<% if @project.versions.any? %> +<table class="list"> + <thead> + <th><%= l(:label_version) %></th> + <th><%= l(:field_effective_date) %></th> + <th><%= l(:field_description) %></th> + <th><%= l(:label_wiki_page) unless @project.wiki.nil? %></th> + <th style="width:15%"></th> + <th style="width:15%"></th> + </thead> + <tbody> +<% for version in @project.versions.sort %> + <tr class="<%= cycle 'odd', 'even' %>"> + <td><%= link_to h(version.name), :controller => 'versions', :action => 'show', :id => version %></td> + <td align="center"><%= format_date(version.effective_date) %></td> + <td><%=h version.description %></td> + <td><%= link_to(version.wiki_page_title, :controller => 'wiki', :page => Wiki.titleize(version.wiki_page_title)) unless version.wiki_page_title.blank? || @project.wiki.nil? %></td> + <td align="center"><%= link_to_if_authorized l(:button_edit), { :controller => 'versions', :action => 'edit', :id => version }, :class => 'icon icon-edit' %></td> + <td align="center"><%= link_to_if_authorized l(:button_delete), {:controller => 'versions', :action => 'destroy', :id => version}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %></td> + </tr> +<% end; reset_cycle %> + </tbody> +</table> +<% else %> +<p class="nodata"><%= l(:label_no_data) %></p> +<% end %> + +<p><%= link_to_if_authorized l(:label_version_new), :controller => 'projects', :action => 'add_version', :id => @project %></p> diff --git a/app/views/projects/settings/_wiki.ctp b/app/views/projects/settings/_wiki.ctp new file mode 100644 index 0000000..00da930 --- /dev/null +++ b/app/views/projects/settings/_wiki.ctp @@ -0,0 +1,19 @@ +<% remote_form_for :wiki, @wiki, + :url => { :controller => 'wikis', :action => 'edit', :id => @project }, + :builder => TabularFormBuilder, + :lang => current_language do |f| %> + +<%= error_messages_for 'wiki' %> + +<div class="box tabular"> +<p><%= f.text_field :start_page, :size => 60, :required => true %><br /> +<em><%= l(:text_unallowed_characters) %>: , . / ? ; : |</em></p> +</div> + +<div class="contextual"> +<%= link_to(l(:button_delete), {:controller => 'wikis', :action => 'destroy', :id => @project}, + :class => 'icon icon-del') if @wiki && !@wiki.new_record? %> +</div> + +<%= submit_tag((@wiki.nil? || @wiki.new_record?) ? l(:button_create) : l(:button_save)) %> +<% end %> diff --git a/app/views/projects/show.ctp b/app/views/projects/show.ctp new file mode 100644 index 0000000..fa65713 --- /dev/null +++ b/app/views/projects/show.ctp @@ -0,0 +1,80 @@ +<h2><%=l(:label_overview)%></h2> + +<div class="splitcontentleft"> + <%= textilizable @project.description %> + <ul> + <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= link_to(h(@project.homepage), @project.homepage) %></li><% end %> + <% if @subprojects.any? %> + <li><%=l(:label_subproject_plural)%>: <%= @subprojects.collect{|p| link_to(h(p.name), :action => 'show', :id => p)}.join(", ") %></li> + <% end %> + <% if @project.parent %> + <li><%=l(:field_parent)%>: <%= link_to h(@project.parent.name), :controller => 'projects', :action => 'show', :id => @project.parent %></li> + <% end %> + <% @project.custom_values.each do |custom_value| %> + <% if !custom_value.value.empty? %> + <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li> + <% end %> + <% end %> + </ul> + + <% if User.current.allowed_to?(:view_issues, @project) %> + <div class="box"> + <h3 class="icon22 icon22-tracker"><%=l(:label_issue_tracking)%></h3> + <ul> + <% for tracker in @trackers %> + <li><%= link_to tracker.name, :controller => 'issues', :action => 'index', :project_id => @project, + :set_filter => 1, + "tracker_id" => tracker.id %>: + <%= @open_issues_by_tracker[tracker] || 0 %> <%= lwr(:label_open_issues, @open_issues_by_tracker[tracker] || 0) %> + <%= l(:label_on) %> <%= @total_issues_by_tracker[tracker] || 0 %></li> + <% end %> + </ul> + <p><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %></p> + </div> + <% end %> +</div> + +<div class="splitcontentright"> + <% if @members_by_role.any? %> + <div class="box"> + <h3 class="icon22 icon22-users"><%=l(:label_member_plural)%></h3> + <p><% @members_by_role.keys.sort.each do |role| %> + <%= role.name %>: + <%= @members_by_role[role].collect(&:user).sort.collect{|u| link_to_user u}.join(", ") %> + <br /> + <% end %></p> + </div> + <% end %> + + <% if @news.any? && authorize_for('news', 'index') %> + <div class="box"> + <h3><%=l(:label_news_latest)%></h3> + <%= render :partial => 'news/news', :collection => @news %> + <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p> + </div> + <% end %> +</div> + +<% content_for :sidebar do %> + <% planning_links = [] + planning_links << link_to_if_authorized(l(:label_calendar), :controller => 'issues', :action => 'calendar', :project_id => @project) + planning_links << link_to_if_authorized(l(:label_gantt), :controller => 'issues', :action => 'gantt', :project_id => @project) + planning_links.compact! + unless planning_links.empty? %> + <h3><%= l(:label_planning) %></h3> + <p><%= planning_links.join(' | ') %></p> + <% end %> + + <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %> + <h3><%= l(:label_spent_time) %></h3> + <p><span class="icon icon-time"><%= lwr(:label_f_hour, @total_hours) %></span></p> + <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> | + <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p> + <% end %> +<% end %> + +<% content_for :header_tags do %> +<%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %> +<% end %> + +<% html_title(l(:label_overview)) -%> diff --git a/app/views/queries/_columns.ctp b/app/views/queries/_columns.ctp new file mode 100644 index 0000000..1a481ad --- /dev/null +++ b/app/views/queries/_columns.ctp @@ -0,0 +1,27 @@ +<% content_tag 'fieldset', :id => 'columns', :style => (query.has_default_columns? ? 'display:none;' : nil) do %> +<legend><%= l(:field_column_names) %></legend> + +<%= hidden_field_tag 'query[column_names][]', '', :id => nil %> +<table> + <tr> + <td><%= select_tag 'available_columns', + options_for_select((query.available_columns - query.columns).collect {|column| [column.caption, column.name]}), + :multiple => true, :size => 10, :style => "width:150px" %> + </td> + <td align="center" valign="middle"> + <input type="button" value="--&gt;" + onclick="moveOptions(this.form.available_columns, this.form.selected_columns);" /><br /> + <input type="button" value="&lt;--" + onclick="moveOptions(this.form.selected_columns, this.form.available_columns);" /> + </td> + <td><%= select_tag 'query[column_names][]', + options_for_select(@query.columns.collect {|column| [column.caption, column.name]}), + :id => 'selected_columns', :multiple => true, :size => 10, :style => "width:150px" %> + </td> + </tr> +</table> +<% end %> + +<% content_for :header_tags do %> +<%= javascript_include_tag 'select_list_move' %> +<% end %> diff --git a/app/views/queries/_filters.ctp b/app/views/queries/_filters.ctp new file mode 100644 index 0000000..c9d6123 --- /dev/null +++ b/app/views/queries/_filters.ctp @@ -0,0 +1,101 @@ +<script type="text/javascript"> +//<![CDATA[ +function add_filter() { + select = $('add_filter_select'); + field = select.value + Element.show('tr_' + field); + check_box = $('cb_' + field); + check_box.checked = true; + toggle_filter(field); + select.selectedIndex = 0; + + for (i=0; i<select.options.length; i++) { + if (select.options[i].value == field) { + select.options[i].disabled = true; + } + } +} + +function toggle_filter(field) { + check_box = $('cb_' + field); + + if (check_box.checked) { + Element.show("operators_" + field); + toggle_operator(field); + } else { + Element.hide("operators_" + field); + Element.hide("div_values_" + field); + } +} + +function toggle_operator(field) { + operator = $("operators_" + field); + switch (operator.value) { + case "!*": + case "*": + case "t": + case "w": + case "o": + case "c": + Element.hide("div_values_" + field); + break; + default: + Element.show("div_values_" + field); + break; + } +} + +function toggle_multi_select(field) { + select = $('values_' + field); + if (select.multiple == true) { + select.multiple = false; + } else { + select.multiple = true; + } +} +//]]> +</script> + +<table width="100%"> +<tr> +<td> +<table> +<% query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.each do |filter| %> + <% field = filter[0] + options = filter[1] %> + <tr <%= 'style="display:none;"' unless query.has_filter?(field) %> id="tr_<%= field %>" class="filter"> + <td style="width:200px;"> + <%= check_box_tag 'fields[]', field, query.has_filter?(field), :onclick => "toggle_filter('#{field}');", :id => "cb_#{field}" %> + <label for="cb_<%= field %>"><%= filter[1][:name] || l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) %></label> + </td> + <td style="width:150px;"> + <%= select_tag "operators[#{field}]", options_for_select(operators_for_select(options[:type]), query.operator_for(field)), :id => "operators_#{field}", :onchange => "toggle_operator('#{field}');", :class => "select-small", :style => "vertical-align: top;" %> + </td> + <td> + <div id="div_values_<%= field %>" style="display:none;"> + <% case options[:type] + when :list, :list_optional, :list_status, :list_subprojects %> + <select <%= "multiple=true" if query.values_for(field) and query.values_for(field).length > 1 %> name="values[<%= field %>][]" id="values_<%= field %>" class="select-small" style="vertical-align: top;"> + <%= options_for_select options[:values], query.values_for(field) %> + </select> + <%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select('#{field}');", :style => "vertical-align: bottom;" %> + <% when :date, :date_past %> + <%= text_field_tag "values[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 3, :class => "select-small" %> <%= l(:label_day_plural) %> + <% when :string, :text %> + <%= text_field_tag "values[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 30, :class => "select-small" %> + <% when :integer %> + <%= text_field_tag "values[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 3, :class => "select-small" %> + <% end %> + </div> + <script type="text/javascript">toggle_filter('<%= field %>');</script> + </td> + </tr> +<% end %> +</table> +</td> +<td class="add-filter"> +<%= l(:label_filter_add) %>: +<%= select_tag 'add_filter_select', options_for_select([["",""]] + query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.collect{|field| [ field[1][:name] || l(("field_"+field[0].to_s.gsub(/\_id$/, "")).to_sym), field[0]] unless query.has_filter?(field[0])}.compact), :onchange => "add_filter();", :class => "select-small" %> +</td> +</tr> +</table> diff --git a/app/views/queries/_form.ctp b/app/views/queries/_form.ctp new file mode 100644 index 0000000..8da2640 --- /dev/null +++ b/app/views/queries/_form.ctp @@ -0,0 +1,29 @@ +<%= error_messages_for 'query' %> +<%= hidden_field_tag 'confirm', 1 %> + +<div class="box"> +<div class="tabular"> +<p><label for="query_name"><%=l(:field_name)%></label> +<%= text_field 'query', 'name', :size => 80 %></p> + +<% if User.current.admin? || (@project && current_role.allowed_to?(:manage_public_queries)) %> +<p><label for="query_is_public"><%=l(:field_is_public)%></label> +<%= check_box 'query', 'is_public', + :onchange => (User.current.admin? ? nil : 'if (this.checked) {$("query_is_for_all").checked = false; $("query_is_for_all").disabled = true;} else {$("query_is_for_all").disabled = false;}') %></p> +<% end %> + +<p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label> +<%= check_box_tag 'query_is_for_all', 1, @query.project.nil?, + :disabled => (!@query.new_record? && (@query.project.nil? || (@query.is_public? && !User.current.admin?))) %></p> + +<p><label for="query_default_columns"><%=l(:label_default_columns)%></label> +<%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns', + :onclick => 'if (this.checked) {Element.hide("columns")} else {Element.show("columns")}' %></p> +</div> + +<fieldset><legend><%= l(:label_filter_plural) %></legend> +<%= render :partial => 'queries/filters', :locals => {:query => query}%> +</fieldset> + +<%= render :partial => 'queries/columns', :locals => {:query => query}%> +</div> diff --git a/app/views/queries/edit.ctp b/app/views/queries/edit.ctp new file mode 100644 index 0000000..1c99ac0 --- /dev/null +++ b/app/views/queries/edit.ctp @@ -0,0 +1,6 @@ +<h2><%= l(:label_query) %></h2> + +<% form_tag({:action => 'edit', :id => @query}, :onsubmit => 'selectAllOptions("selected_columns");') do %> + <%= render :partial => 'form', :locals => {:query => @query} %> + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/queries/index.ctp b/app/views/queries/index.ctp new file mode 100644 index 0000000..1c608b8 --- /dev/null +++ b/app/views/queries/index.ctp @@ -0,0 +1,27 @@ +<div class="contextual"> +<%= link_to_if_authorized l(:label_query_new), {:controller => 'queries', :action => 'new', :project_id => @project}, :class => 'icon icon-add' %> +</div> + +<h2><%= l(:label_query_plural) %></h2> + +<% if @queries.empty? %> + <p><i><%=l(:label_no_data)%></i></p> +<% else %> + <table class="list"> + <% @queries.each do |query| %> + <tr class="<%= cycle('odd', 'even') %>"> + <td> + <%= link_to query.name, :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %> + </td> + <td align="right"> + <small> + <% if query.editable_by?(User.current) %> + <%= link_to l(:button_edit), {:controller => 'queries', :action => 'edit', :id => query}, :class => 'icon icon-edit' %> + <%= link_to l(:button_delete), {:controller => 'queries', :action => 'destroy', :id => query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> + </small> + <% end %> + </td> + </tr> + <% end %> + </table> +<% end %> diff --git a/app/views/queries/new.ctp b/app/views/queries/new.ctp new file mode 100644 index 0000000..631fd6c --- /dev/null +++ b/app/views/queries/new.ctp @@ -0,0 +1,6 @@ +<h2><%= l(:label_query_new) %></h2> + +<% form_tag({:action => 'new', :project_id => @query.project}, :onsubmit => 'selectAllOptions("selected_columns");') do %> + <%= render :partial => 'form', :locals => {:query => @query} %> + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/reports/_details.ctp b/app/views/reports/_details.ctp new file mode 100644 index 0000000..c3ad2be --- /dev/null +++ b/app/views/reports/_details.ctp @@ -0,0 +1,48 @@ +<% if @statuses.empty? or rows.empty? %> + <p><i><%=l(:label_no_data)%></i></p> +<% else %> +<% col_width = 70 / (@statuses.length+3) %> +<table class="list"> +<thead><tr> +<th style="width:25%"></th> +<% for status in @statuses %> +<th style="width:<%= col_width %>%"><%= status.name %></th> +<% end %> +<th align="center" style="width:<%= col_width %>%"><strong><%=l(:label_open_issues_plural)%></strong></th> +<th align="center" style="width:<%= col_width %>%"><strong><%=l(:label_closed_issues_plural)%></strong></th> +<th align="center" style="width:<%= col_width %>%"><strong><%=l(:label_total)%></strong></th> +</tr></thead> +<tbody> +<% for row in rows %> +<tr class="<%= cycle("odd", "even") %>"> + <td><%= link_to row.name, :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)), + :set_filter => 1, + "#{field_name}" => row.id %></td> + <% for status in @statuses %> + <td align="center"><%= aggregate_link data, { field_name => row.id, "status_id" => status.id }, + :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)), + :set_filter => 1, + "status_id" => status.id, + "#{field_name}" => row.id %></td> + <% end %> + <td align="center"><%= aggregate_link data, { field_name => row.id, "closed" => 0 }, + :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)), + :set_filter => 1, + "#{field_name}" => row.id, + "status_id" => "o" %></td> + <td align="center"><%= aggregate_link data, { field_name => row.id, "closed" => 1 }, + :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)), + :set_filter => 1, + "#{field_name}" => row.id, + "status_id" => "c" %></td> + <td align="center"><%= aggregate_link data, { field_name => row.id }, + :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)), + :set_filter => 1, + "#{field_name}" => row.id, + "status_id" => "*" %></td> +</tr> +<% end %> +</tbody> +</table> +<% end + reset_cycle %> \ No newline at end of file diff --git a/app/views/reports/_simple.ctp b/app/views/reports/_simple.ctp new file mode 100644 index 0000000..7f799f3 --- /dev/null +++ b/app/views/reports/_simple.ctp @@ -0,0 +1,37 @@ +<% if @statuses.empty? or rows.empty? %> + <p><i><%=l(:label_no_data)%></i></p> +<% else %> +<table class="list"> +<thead><tr> +<th style="width:25%"></th> +<th align="center" style="width:25%"><%=l(:label_open_issues_plural)%></th> +<th align="center" style="width:25%"><%=l(:label_closed_issues_plural)%></th> +<th align="center" style="width:25%"><%=l(:label_total)%></th> +</tr></thead> +<tbody> +<% for row in rows %> +<tr class="<%= cycle("odd", "even") %>"> + <td><%= link_to row.name, :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)), + :set_filter => 1, + "#{field_name}" => row.id %></td> + <td align="center"><%= aggregate_link data, { field_name => row.id, "closed" => 0 }, + :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)), + :set_filter => 1, + "#{field_name}" => row.id, + "status_id" => "o" %></td> + <td align="center"><%= aggregate_link data, { field_name => row.id, "closed" => 1 }, + :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)), + :set_filter => 1, + "#{field_name}" => row.id, + "status_id" => "c" %></td> + <td align="center"><%= aggregate_link data, { field_name => row.id }, + :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)), + :set_filter => 1, + "#{field_name}" => row.id, + "status_id" => "*" %></td> +</tr> +<% end %> +</tbody> +</table> +<% end + reset_cycle %> \ No newline at end of file diff --git a/app/views/reports/issue_report.ctp b/app/views/reports/issue_report.ctp new file mode 100644 index 0000000..1ed16ea --- /dev/null +++ b/app/views/reports/issue_report.ctp @@ -0,0 +1,31 @@ +<h2><%=l(:label_report_plural)%></h2> + +<div class="splitcontentleft"> +<h3><%=l(:field_tracker)%>&nbsp;&nbsp;<%= link_to image_tag('zoom_in.png'), :detail => 'tracker' %></h3> +<%= render :partial => 'simple', :locals => { :data => @issues_by_tracker, :field_name => "tracker_id", :rows => @trackers } %> +<br /> +<h3><%=l(:field_priority)%>&nbsp;&nbsp;<%= link_to image_tag('zoom_in.png'), :detail => 'priority' %></h3> +<%= render :partial => 'simple', :locals => { :data => @issues_by_priority, :field_name => "priority_id", :rows => @priorities } %> +<br /> +<h3><%=l(:field_assigned_to)%>&nbsp;&nbsp;<%= link_to image_tag('zoom_in.png'), :detail => 'assigned_to' %></h3> +<%= render :partial => 'simple', :locals => { :data => @issues_by_assigned_to, :field_name => "assigned_to_id", :rows => @assignees } %> +<br /> +<h3><%=l(:field_author)%>&nbsp;&nbsp;<%= link_to image_tag('zoom_in.png'), :detail => 'author' %></h3> +<%= render :partial => 'simple', :locals => { :data => @issues_by_author, :field_name => "author_id", :rows => @authors } %> +<br /> +</div> + +<div class="splitcontentright"> +<h3><%=l(:field_version)%>&nbsp;&nbsp;<%= link_to image_tag('zoom_in.png'), :detail => 'version' %></h3> +<%= render :partial => 'simple', :locals => { :data => @issues_by_version, :field_name => "fixed_version_id", :rows => @versions } %> +<br /> +<% if @project.children.any? %> +<h3><%=l(:field_subproject)%>&nbsp;&nbsp;<%= link_to image_tag('zoom_in.png'), :detail => 'subproject' %></h3> +<%= render :partial => 'simple', :locals => { :data => @issues_by_subproject, :field_name => "project_id", :rows => @subprojects } %> +<br /> +<% end %> +<h3><%=l(:field_category)%>&nbsp;&nbsp;<%= link_to image_tag('zoom_in.png'), :detail => 'category' %></h3> +<%= render :partial => 'simple', :locals => { :data => @issues_by_category, :field_name => "category_id", :rows => @categories } %> +<br /> +</div> + diff --git a/app/views/reports/issue_report_details.ctp b/app/views/reports/issue_report_details.ctp new file mode 100644 index 0000000..6b4b422 --- /dev/null +++ b/app/views/reports/issue_report_details.ctp @@ -0,0 +1,7 @@ +<h2><%=l(:label_report_plural)%></h2> + +<h3><%=@report_title%></h3> +<%= render :partial => 'details', :locals => { :data => @data, :field_name => @field, :rows => @rows } %> +<br /> +<%= link_to l(:button_back), :action => 'issue_report' %> + diff --git a/app/views/repositories/_dir_list.ctp b/app/views/repositories/_dir_list.ctp new file mode 100644 index 0000000..5590652 --- /dev/null +++ b/app/views/repositories/_dir_list.ctp @@ -0,0 +1,15 @@ +<table class="list entries" id="browser"> +<thead> +<tr id="root"> +<th><%= l(:field_name) %></th> +<th><%= l(:field_filesize) %></th> +<th><%= l(:label_revision) %></th> +<th><%= l(:label_age) %></th> +<th><%= l(:field_author) %></th> +<th><%= l(:field_comments) %></th> +</tr> +</thead> +<tbody> +<%= render :partial => 'dir_list_content' %> +</tbody> +</table> diff --git a/app/views/repositories/_dir_list_content.ctp b/app/views/repositories/_dir_list_content.ctp new file mode 100644 index 0000000..12ee44d --- /dev/null +++ b/app/views/repositories/_dir_list_content.ctp @@ -0,0 +1,24 @@ +<% @entries.each do |entry| %> +<% tr_id = Digest::MD5.hexdigest(entry.path) + depth = params[:depth].to_i %> +<tr id="<%= tr_id %>" class="<%= params[:parent_id] %> entry <%= entry.kind %>"> +<td style="padding-left: <%=18 * depth%>px;" class="filename"> +<% if entry.is_dir? %> +<span class="expander" onclick="<%= remote_function :url => {:action => 'browse', :id => @project, :path => to_path_param(entry.path), :rev => @rev, :depth => (depth + 1), :parent_id => tr_id}, + :update => { :success => tr_id }, + :position => :after, + :success => "scmEntryLoaded('#{tr_id}')", + :condition => "scmEntryClick('#{tr_id}')"%>">&nbsp</span> +<% end %> +<%= link_to h(entry.name), + {:action => (entry.is_dir? ? 'browse' : 'changes'), :id => @project, :path => to_path_param(entry.path), :rev => @rev}, + :class => (entry.is_dir? ? 'icon icon-folder' : 'icon icon-file')%> +</td> +<td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td> +<% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %> +<td class="revision"><%= link_to(format_revision(entry.lastrev.name), :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %></td> +<td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td> +<td class="author"><%= changeset.nil? ? h(entry.lastrev.author.to_s.split('<').first) : changeset.author if entry.lastrev %></td> +<td class="comments"><%=h truncate(changeset.comments, 50) unless changeset.nil? %></td> +</tr> +<% end %> diff --git a/app/views/repositories/_link_to_functions.ctp b/app/views/repositories/_link_to_functions.ctp new file mode 100644 index 0000000..7737bf4 --- /dev/null +++ b/app/views/repositories/_link_to_functions.ctp @@ -0,0 +1,10 @@ +<p> +<% if @repository.supports_cat? %> + <%= link_to_if action_name != 'entry', l(:button_view), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev } %> | +<% end %> +<% if @repository.supports_annotate? %> + <%= link_to_if action_name != 'annotate', l(:button_annotate), {:action => 'annotate', :id => @project, :path => to_path_param(@path), :rev => @rev } %> | +<% end %> +<%= link_to(l(:button_download), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev, :format => 'raw' }) if @repository.supports_cat? %> +<%= "(#{number_to_human_size(@entry.size)})" if @entry.size %> +</p> diff --git a/app/views/repositories/_navigation.ctp b/app/views/repositories/_navigation.ctp new file mode 100644 index 0000000..25a15f4 --- /dev/null +++ b/app/views/repositories/_navigation.ctp @@ -0,0 +1,21 @@ +<%= link_to 'root', :action => 'browse', :id => @project, :path => '', :rev => @rev %> +<% +dirs = path.split('/') +if 'file' == kind + filename = dirs.pop +end +link_path = '' +dirs.each do |dir| + next if dir.blank? + link_path << '/' unless link_path.empty? + link_path << "#{dir}" + %> + / <%= link_to h(dir), :action => 'browse', :id => @project, :path => to_path_param(link_path), :rev => @rev %> +<% end %> +<% if filename %> + / <%= link_to h(filename), :action => 'changes', :id => @project, :path => to_path_param("#{link_path}/#{filename}"), :rev => @rev %> +<% end %> + +<%= "@ #{revision}" if revision %> + +<% html_title(with_leading_slash(path)) -%> diff --git a/app/views/repositories/_revisions.ctp b/app/views/repositories/_revisions.ctp new file mode 100644 index 0000000..fb0131c --- /dev/null +++ b/app/views/repositories/_revisions.ctp @@ -0,0 +1,28 @@ +<% form_tag({:controller => 'repositories', :action => 'diff', :id => @project, :path => to_path_param(path)}, :method => :get) do %> +<table class="list changesets"> +<thead><tr> +<th>#</th> +<th></th> +<th></th> +<th><%= l(:label_date) %></th> +<th><%= l(:field_author) %></th> +<th><%= l(:field_comments) %></th> +</tr></thead> +<tbody> +<% show_diff = entry && entry.is_file? && revisions.size > 1 %> +<% line_num = 1 %> +<% revisions.each do |changeset| %> +<tr class="changeset <%= cycle 'odd', 'even' %>"> +<td class="id"><%= link_to format_revision(changeset.revision), :action => 'revision', :id => project, :rev => changeset.revision %></td> +<td class="checkbox"><%= radio_button_tag('rev', changeset.revision, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %></td> +<td class="checkbox"><%= radio_button_tag('rev_to', changeset.revision, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td> +<td class="committed_on"><%= format_time(changeset.committed_on) %></td> +<td class="author"><%=h changeset.author %></td> +<td class="comments"><%= textilizable(truncate_at_line_break(changeset.comments)) %></td> +</tr> +<% line_num += 1 %> +<% end %> +</tbody> +</table> +<%= submit_tag(l(:label_view_diff), :name => nil) if show_diff %> +<% end %> diff --git a/app/views/repositories/annotate.ctp b/app/views/repositories/annotate.ctp new file mode 100644 index 0000000..44b5a81 --- /dev/null +++ b/app/views/repositories/annotate.ctp @@ -0,0 +1,30 @@ +<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2> + +<p><%= render :partial => 'link_to_functions' %></p> + +<% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %> + +<div class="autoscroll"> +<table class="filecontent annotate CodeRay"> + <tbody> + <% line_num = 1 %> + <% syntax_highlight(@path, to_utf8(@annotate.content)).each_line do |line| %> + <% revision = @annotate.revisions[line_num-1] %> + <tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>"> + <th class="line-num"><%= line_num %></th> + <td class="revision"> + <%= (revision.identifier ? link_to(format_revision(revision.identifier), :action => 'revision', :id => @project, :rev => revision.identifier) : format_revision(revision.revision)) if revision %></td> + <td class="author"><%= h(revision.author.to_s.split('<').first) if revision %></td> + <td class="line-code"><pre><%= line %></pre></td> + </tr> + <% line_num += 1 %> + <% end %> + </tbody> +</table> +</div> + +<% html_title(l(:button_annotate)) -%> + +<% content_for :header_tags do %> +<%= stylesheet_link_tag 'scm' %> +<% end %> diff --git a/app/views/repositories/browse.ctp b/app/views/repositories/browse.ctp new file mode 100644 index 0000000..4029a77 --- /dev/null +++ b/app/views/repositories/browse.ctp @@ -0,0 +1,14 @@ +<div class="contextual"> +<% form_tag do %> +<%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %> +<% end %> +</div> + +<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'dir', :revision => @rev } %></h2> + +<%= render :partial => 'dir_list' %> +<%= render_properties(@properties) %> + +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<% end %> diff --git a/app/views/repositories/changes.ctp b/app/views/repositories/changes.ctp new file mode 100644 index 0000000..aa359ef --- /dev/null +++ b/app/views/repositories/changes.ctp @@ -0,0 +1,10 @@ +<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => (@entry ? @entry.kind : nil), :revision => @rev } %></h2> + +<p><%= render :partial => 'link_to_functions' %></p> + +<%= render_properties(@properties) %> + +<%= render(:partial => 'revisions', + :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }) unless @changesets.empty? %> + +<% html_title(l(:label_change_plural)) -%> diff --git a/app/views/repositories/committers.ctp b/app/views/repositories/committers.ctp new file mode 100644 index 0000000..597a414 --- /dev/null +++ b/app/views/repositories/committers.ctp @@ -0,0 +1,34 @@ +<h2><%= l(:label_repository) %></h2> + +<%= simple_format(l(:text_repository_usernames_mapping)) %> + +<% if @committers.empty? %> +<p class="nodata"><%= l(:label_no_data) %></p> +<% else %> + +<% form_tag({}) do %> +<table class="list"> +<thead> + <tr> + <th><%= l(:field_login) %></th> + <th><%= l(:label_user) %></th> + </tr> +</thead> +<tbody> +<% i = 0 -%> +<% @committers.each do |committer, user_id| -%> + <tr class="<%= cycle 'odd', 'even' %>"> + <td><%=h committer %></td> + <td> + <%= hidden_field_tag "committers[#{i}][]", committer %> + <%= select_tag "committers[#{i}][]", content_tag('option', "-- #{l :actionview_instancetag_blank_option} --", :value => '') + options_from_collection_for_select(@users, 'id', 'name', user_id.to_i) %> + </td> + </tr> + <% i += 1 -%> +<% end -%> +</tbody> +</table> +<p><%= submit_tag(l(:button_update)) %></p> +<% end %> + +<% end %> \ No newline at end of file diff --git a/app/views/repositories/diff.ctp b/app/views/repositories/diff.ctp new file mode 100644 index 0000000..52a5d60 --- /dev/null +++ b/app/views/repositories/diff.ctp @@ -0,0 +1,27 @@ +<h2><%= l(:label_revision) %> <%= format_revision(@rev) %> <%= @path.gsub(/^.*\//, '') %></h2> + +<!-- Choose view type --> +<% form_tag({ :controller => 'repositories', :action => 'diff'}, :method => 'get') do %> + <% params.each do |k, p| %> + <% if k != "type" %> + <%= hidden_field_tag(k,p) %> + <% end %> + <% end %> + <p><label><%= l(:label_view_diff) %></label> + <%= select_tag 'type', options_for_select([[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type), :onchange => "if (this.value != '') {this.form.submit()}" %></p> +<% end %> + +<% cache(@cache_key) do -%> +<%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %> +<% end -%> + +<p class="other-formats"> +<%= l(:label_export_to) %> +<span><%= link_to 'Unified diff', params.merge(:format => 'diff') %></span> +</p> + +<% html_title(with_leading_slash(@path), 'Diff') -%> + +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<% end %> diff --git a/app/views/repositories/entry.ctp b/app/views/repositories/entry.ctp new file mode 100644 index 0000000..12ba9f4 --- /dev/null +++ b/app/views/repositories/entry.ctp @@ -0,0 +1,9 @@ +<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2> + +<p><%= render :partial => 'link_to_functions' %></p> + +<%= render :partial => 'common/file', :locals => {:filename => @path, :content => @content} %> + +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<% end %> diff --git a/app/views/repositories/revision.ctp b/app/views/repositories/revision.ctp new file mode 100644 index 0000000..c5bac72 --- /dev/null +++ b/app/views/repositories/revision.ctp @@ -0,0 +1,57 @@ +<div class="contextual"> + &#171; + <% unless @changeset.previous.nil? -%> + <%= link_to l(:label_previous), :controller => 'repositories', :action => 'revision', :id => @project, :rev => @changeset.previous.revision %> + <% else -%> + <%= l(:label_previous) %> + <% end -%> +| + <% unless @changeset.next.nil? -%> + <%= link_to l(:label_next), :controller => 'repositories', :action => 'revision', :id => @project, :rev => @changeset.next.revision %> + <% else -%> + <%= l(:label_next) %> + <% end -%> + &#187;&nbsp; + + <% form_tag({:controller => 'repositories', :action => 'revision', :id => @project, :rev => nil}, :method => :get) do %> + <%= text_field_tag 'rev', @rev, :size => 5 %> + <%= submit_tag 'OK', :name => nil %> + <% end %> +</div> + +<h2><%= l(:label_revision) %> <%= format_revision(@changeset.revision) %></h2> + +<p><% if @changeset.scmid %>ID: <%= @changeset.scmid %><br /><% end %> +<span class="author"><%= authoring(@changeset.committed_on, @changeset.author) %></span></p> + +<%= textilizable @changeset.comments %> + +<% if @changeset.issues.any? %> +<h3><%= l(:label_related_issues) %></h3> +<ul> +<% @changeset.issues.each do |issue| %> + <li><%= link_to_issue issue %>: <%=h issue.subject %></li> +<% end %> +</ul> +<% end %> + +<h3><%= l(:label_attachment_plural) %></h3> +<ul id="changes-legend"> +<li class="change change-A"><%= l(:label_added) %></li> +<li class="change change-M"><%= l(:label_modified) %></li> +<li class="change change-C"><%= l(:label_copied) %></li> +<li class="change change-R"><%= l(:label_renamed) %></li> +<li class="change change-D"><%= l(:label_deleted) %></li> +</ul> + +<p><%= link_to(l(:label_view_diff), :action => 'diff', :id => @project, :path => "", :rev => @changeset.revision) if @changeset.changes.any? %></p> + +<div class="changeset-changes"> +<%= render_changeset_changes %> +</div> + +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<% end %> + +<% html_title("#{l(:label_revision)} #{@changeset.revision}") -%> diff --git a/app/views/repositories/revisions.ctp b/app/views/repositories/revisions.ctp new file mode 100644 index 0000000..8da7d58 --- /dev/null +++ b/app/views/repositories/revisions.ctp @@ -0,0 +1,24 @@ +<div class="contextual"> +<% form_tag({:action => 'revision', :id => @project}) do %> +<%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %> +<%= submit_tag 'OK' %> +<% end %> +</div> + +<h2><%= l(:label_revision_plural) %></h2> + +<%= render :partial => 'revisions', :locals => {:project => @project, :path => '', :revisions => @changesets, :entry => nil }%> + +<p class="pagination"><%= pagination_links_full @changeset_pages,@changeset_count %></p> + +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :page => nil, :key => User.current.rss_key})) %> +<% end %> + +<p class="other-formats"> +<%= l(:label_export_to) %> +<span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span> +</p> + +<% html_title(l(:label_revision_plural)) -%> diff --git a/app/views/repositories/show.ctp b/app/views/repositories/show.ctp new file mode 100644 index 0000000..280ae3b --- /dev/null +++ b/app/views/repositories/show.ctp @@ -0,0 +1,35 @@ +<div class="contextual"> +<%= call_hook(:view_repositories_show_contextual, { :repository => @repository, :project => @project }) %> +<%= link_to l(:label_statistics), {:action => 'stats', :id => @project}, :class => 'icon icon-stats' %> + +<% if !@entries.nil? && authorize_for('repositories', 'browse') -%> +<% form_tag(:action => 'browse', :id => @project) do -%> +| <%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %> +<% end -%> +<% end -%> +</div> + +<h2><%= l(:label_repository) %> (<%= @repository.scm_name %>)</h2> + +<% if !@entries.nil? && authorize_for('repositories', 'browse') %> +<%= render :partial => 'dir_list' %> +<% end %> + +<% if !@changesets.empty? && authorize_for('repositories', 'revisions') %> +<h3><%= l(:label_latest_revision_plural) %></h3> +<%= render :partial => 'revisions', :locals => {:project => @project, :path => '', :revisions => @changesets, :entry => nil }%> +<p><%= link_to l(:label_view_revisions), :action => 'revisions', :id => @project %></p> +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :action => 'revisions', :id => @project, :page => nil, :key => User.current.rss_key})) %> +<% end %> +<p class="other-formats"> +<%= l(:label_export_to) %> +<span><%= link_to 'Atom', {:action => 'revisions', :id => @project, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span> +</p> +<% end %> + +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<% end %> + +<% html_title(l(:label_repository)) -%> diff --git a/app/views/repositories/stats.ctp b/app/views/repositories/stats.ctp new file mode 100644 index 0000000..1e61757 --- /dev/null +++ b/app/views/repositories/stats.ctp @@ -0,0 +1,12 @@ +<h2><%= l(:label_statistics) %></h2> + +<p> +<%= tag("embed", :width => 800, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_month")) %> +</p> +<p> +<%= tag("embed", :width => 800, :height => 400, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_author")) %> +</p> + +<p><%= link_to l(:button_back), :action => 'show', :id => @project %></p> + +<% html_title(l(:label_repository), l(:label_statistics)) -%> diff --git a/app/views/roles/_form.ctp b/app/views/roles/_form.ctp new file mode 100644 index 0000000..a167907 --- /dev/null +++ b/app/views/roles/_form.ctp @@ -0,0 +1,29 @@ +<%= error_messages_for 'role' %> + +<% unless @role.builtin? %> +<div class="box"> +<p><%= f.text_field :name, :required => true %></p> +<p><%= f.check_box :assignable %></p> +<% if @role.new_record? && @roles.any? %> +<p><label><%= l(:label_copy_workflow_from) %></label> +<%= select_tag(:copy_workflow_from, content_tag("option") + options_from_collection_for_select(@roles, :id, :name)) %></p> +<% end %> +</div> +<% end %> + +<h3><%= l(:label_permissions) %></h3> +<div class="box" id="permissions"> +<% perms_by_module = @permissions.group_by {|p| p.project_module.to_s} %> +<% perms_by_module.keys.sort.each do |mod| %> + <fieldset><legend><%= mod.blank? ? l(:label_project) : l_or_humanize(mod, :prefix => 'project_module_') %></legend> + <% perms_by_module[mod].each do |permission| %> + <label class="floating"> + <%= check_box_tag 'role[permissions][]', permission.name, (@role.permissions.include? permission.name) %> + <%= l_or_humanize(permission.name, :prefix => 'permission_') %> + </label> + <% end %> + </fieldset> +<% end %> +<br /><%= check_all_links 'permissions' %> +<%= hidden_field_tag 'role[permissions][]', '' %> +</div> diff --git a/app/views/roles/edit.ctp b/app/views/roles/edit.ctp new file mode 100644 index 0000000..e53a0f5 --- /dev/null +++ b/app/views/roles/edit.ctp @@ -0,0 +1,6 @@ +<h2><%=l(:label_role)%>: <%= @role.name %></h2> + +<% labelled_tabular_form_for :role, @role, :url => { :action => 'edit' }, :html => {:id => 'role_form'} do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/roles/list.ctp b/app/views/roles/list.ctp new file mode 100644 index 0000000..93b8213 --- /dev/null +++ b/app/views/roles/list.ctp @@ -0,0 +1,37 @@ +<div class="contextual"> +<%= link_to l(:label_role_new), {:action => 'new'}, :class => 'icon icon-add' %> +</div> + +<h2><%=l(:label_role_plural)%></h2> + +<table class="list"> + <thead><tr> + <th><%=l(:label_role)%></th> + <th><%=l(:button_sort)%></th> + <th></th> + </tr></thead> + <tbody> +<% for role in @roles %> + <tr class="<%= cycle("odd", "even") %>"> + <td><%= content_tag(role.builtin? ? 'em' : 'span', link_to(role.name, :action => 'edit', :id => role)) %></td> + <td align="center" style="width:15%;"> + <% unless role.builtin? %> + <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => role, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> + <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => role, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> - + <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => role, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %> + <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => role, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %> + <% end %> + </td> + <td align="center" style="width:10%;"> + <%= button_to(l(:button_delete), { :action => 'destroy', :id => role }, :confirm => l(:text_are_you_sure), :class => "button-small", :disabled => role.builtin? ) %> + </td> + </tr> +<% end %> + </tbody> +</table> + +<p class="pagination"><%= pagination_links_full @role_pages %></p> + +<p><%= link_to l(:label_permissions_report), :action => 'report' %></p> + +<% html_title(l(:label_role_plural)) -%> diff --git a/app/views/roles/new.ctp b/app/views/roles/new.ctp new file mode 100644 index 0000000..8f03aef --- /dev/null +++ b/app/views/roles/new.ctp @@ -0,0 +1,6 @@ +<h2><%=l(:label_role_new)%></h2> + +<% labelled_tabular_form_for :role, @role, :url => { :action => 'new' }, :html => {:id => 'role_form'} do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<% end %> \ No newline at end of file diff --git a/app/views/roles/report.ctp b/app/views/roles/report.ctp new file mode 100644 index 0000000..f5ebc88 --- /dev/null +++ b/app/views/roles/report.ctp @@ -0,0 +1,45 @@ +<h2><%=l(:label_permissions_report)%></h2> + +<% form_tag({:action => 'report'}, :id => 'permissions_form') do %> +<%= hidden_field_tag 'permissions[0]', '', :id => nil %> +<table class="list"> +<thead> + <tr> + <th><%=l(:label_permissions)%></th> + <% @roles.each do |role| %> + <th> + <%= content_tag(role.builtin? ? 'em' : 'span', h(role.name)) %> + <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.role-#{role.id}')", + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %> + </th> + <% end %> + </tr> +</thead> +<tbody> +<% perms_by_module = @permissions.group_by {|p| p.project_module.to_s} %> +<% perms_by_module.keys.sort.each do |mod| %> + <% unless mod.blank? %> + <tr><%= content_tag('th', l_or_humanize(mod, :prefix => 'project_module_'), :colspan => (@roles.size + 1), :align => 'left') %></tr> + <% end %> + <% perms_by_module[mod].each do |permission| %> + <tr class="<%= cycle('odd', 'even') %> permission-<%= permission.name %>"> + <td> + <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('.permission-#{permission.name} input')", + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %> + <%= l_or_humanize(permission.name, :prefix => 'permission_') %> + </td> + <% @roles.each do |role| %> + <td align="center"> + <% if role.setable_permissions.include? permission %> + <%= check_box_tag "permissions[#{role.id}][]", permission.name, (role.permissions.include? permission.name), :id => nil, :class => "role-#{role.id}" %> + <% end %> + </td> + <% end %> + </tr> + <% end %> +<% end %> +</tbody> +</table> +<p><%= check_all_links 'permissions_form' %></p> +<p><%= submit_tag l(:button_save) %></p> +<% end %> diff --git a/app/views/scaffolds/empty b/app/views/scaffolds/empty new file mode 100755 index 0000000..e69de29 diff --git a/app/views/search/index.ctp b/app/views/search/index.ctp new file mode 100644 index 0000000..cb5b70a --- /dev/null +++ b/app/views/search/index.ctp @@ -0,0 +1,51 @@ +<h2><%= l(:label_search) %></h2> + +<div class="box"> +<% form_tag({}, :method => :get) do %> +<p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %> +<%= javascript_tag "Field.focus('search-input')" %> +<%= project_select_tag %> +<label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label> +<label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label> +</p> +<p> +<% @object_types.each do |t| %> +<label><%= check_box_tag t, 1, @scope.include?(t) %> <%= type_label(t) %></label> +<% end %> +</p> + +<p><%= submit_tag l(:button_submit), :name => 'submit' %></p> +<% end %> +</div> + +<% if @results %> + <div id="search-results-counts"> + <%= render_results_by_type(@results_by_type) unless @scope.size == 1 %> + </div> + + <h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3> + <dl id="search-results"> + <% @results.each do |e| %> + <dt class="<%= e.event_type %>"><%= content_tag('span', h(e.project), :class => 'project') unless @project == e.project %> <%= link_to highlight_tokens(truncate(e.event_title, 255), @tokens), e.event_url %></dt> + <dd><span class="description"><%= highlight_tokens(e.event_description, @tokens) %></span> + <span class="author"><%= format_time(e.event_datetime) %></span></dd> + <% end %> + </dl> +<% end %> + +<p><center> +<% if @pagination_previous_date %> +<%= link_to_remote ('&#171; ' + l(:label_previous)), + {:update => :content, + :url => params.merge(:previous => 1, :offset => @pagination_previous_date.strftime("%Y%m%d%H%M%S")) + }, :href => url_for(params.merge(:previous => 1, :offset => @pagination_previous_date.strftime("%Y%m%d%H%M%S"))) %>&nbsp; +<% end %> +<% if @pagination_next_date %> +<%= link_to_remote (l(:label_next) + ' &#187;'), + {:update => :content, + :url => params.merge(:previous => nil, :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S")) + }, :href => url_for(params.merge(:previous => nil, :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S"))) %> +<% end %> +</center></p> + +<% html_title(l(:label_search)) -%> diff --git a/app/views/settings/_authentication.ctp b/app/views/settings/_authentication.ctp new file mode 100644 index 0000000..6bf20cb --- /dev/null +++ b/app/views/settings/_authentication.ctp @@ -0,0 +1,27 @@ +<% form_tag({:action => 'edit', :tab => 'authentication'}) do %> + +<div class="box tabular settings"> +<p><label><%= l(:setting_login_required) %></label> +<%= check_box_tag 'settings[login_required]', 1, Setting.login_required? %><%= hidden_field_tag 'settings[login_required]', 0 %></p> + +<p><label><%= l(:setting_autologin) %></label> +<%= select_tag 'settings[autologin]', options_for_select( [[l(:label_disabled), "0"]] + [1, 7, 30, 365].collect{|days| [lwr(:actionview_datehelper_time_in_words_day, days), days.to_s]}, Setting.autologin) %></p> + +<p><label><%= l(:setting_self_registration) %></label> +<%= select_tag 'settings[self_registration]', + options_for_select( [[l(:label_disabled), "0"], + [l(:label_registration_activation_by_email), "1"], + [l(:label_registration_manual_activation), "2"], + [l(:label_registration_automatic_activation), "3"] + ], Setting.self_registration ) %></p> + +<p><label><%= l(:label_password_lost) %></label> +<%= check_box_tag 'settings[lost_password]', 1, Setting.lost_password? %><%= hidden_field_tag 'settings[lost_password]', 0 %></p> +</div> + +<div style="float:right;"> + <%= link_to l(:label_ldap_authentication), :controller => 'auth_sources', :action => 'list' %> +</div> + +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/settings/_general.ctp b/app/views/settings/_general.ctp new file mode 100644 index 0000000..9889702 --- /dev/null +++ b/app/views/settings/_general.ctp @@ -0,0 +1,59 @@ +<% form_tag({:action => 'edit'}) do %> + +<div class="box tabular settings"> +<p><label><%= l(:setting_app_title) %></label> +<%= text_field_tag 'settings[app_title]', Setting.app_title, :size => 30 %></p> + +<p><label><%= l(:setting_welcome_text) %></label> +<%= text_area_tag 'settings[welcome_text]', Setting.welcome_text, :cols => 60, :rows => 5, :class => 'wiki-edit' %></p> +<%= wikitoolbar_for 'settings[welcome_text]' %> + +<p><label><%= l(:label_theme) %></label> +<%= select_tag 'settings[ui_theme]', options_for_select( ([[l(:label_default), '']] + Redmine::Themes.themes.collect {|t| [t.name, t.id]}), Setting.ui_theme) %></p> + +<p><label><%= l(:setting_default_language) %></label> +<%= select_tag 'settings[default_language]', options_for_select( lang_options_for_select(false), Setting.default_language) %></p> + +<p><label><%= l(:setting_date_format) %></label> +<%= select_tag 'settings[date_format]', options_for_select( [[l(:label_language_based), '']] + Setting::DATE_FORMATS.collect {|f| [Date.today.strftime(f), f]}, Setting.date_format) %></p> + +<p><label><%= l(:setting_time_format) %></label> +<%= select_tag 'settings[time_format]', options_for_select( [[l(:label_language_based), '']] + Setting::TIME_FORMATS.collect {|f| [Time.now.strftime(f), f]}, Setting.time_format) %></p> + +<p><label><%= l(:setting_user_format) %></label> +<%= select_tag 'settings[user_format]', options_for_select( @options[:user_format], Setting.user_format.to_s ) %></p> + +<p><label><%= l(:setting_attachment_max_size) %></label> +<%= text_field_tag 'settings[attachment_max_size]', Setting.attachment_max_size, :size => 6 %> KB</p> + +<p><label><%= l(:setting_per_page_options) %></label> +<%= text_field_tag 'settings[per_page_options]', Setting.per_page_options_array.join(', '), :size => 20 %><br /><em><%= l(:text_comma_separated) %></em></p> + +<p><label><%= l(:setting_activity_days_default) %></label> +<%= text_field_tag 'settings[activity_days_default]', Setting.activity_days_default, :size => 6 %> <%= l(:label_day_plural) %></p> + +<p><label><%= l(:setting_host_name) %></label> +<%= text_field_tag 'settings[host_name]', Setting.host_name, :size => 60 %><br /> +<em><%= l(:label_example) %>: <%= @guessed_host_and_path %></em></p> + +<p><label><%= l(:setting_protocol) %></label> +<%= select_tag 'settings[protocol]', options_for_select(['http', 'https'], Setting.protocol) %></p> + +<p><label><%= l(:setting_text_formatting) %></label> +<%= select_tag 'settings[text_formatting]', options_for_select([[l(:label_none), "0"], *Redmine::WikiFormatting.format_names.collect{|name| [name, name]} ], Setting.text_formatting.to_sym) %></p> + +<p><label><%= l(:setting_wiki_compression) %></label> +<%= select_tag 'settings[wiki_compression]', options_for_select( [[l(:label_none), 0], ["gzip", "gzip"]], Setting.wiki_compression) %></p> + +<p><label><%= l(:setting_feeds_limit) %></label> +<%= text_field_tag 'settings[feeds_limit]', Setting.feeds_limit, :size => 6 %></p> + +<p><label><%= l(:setting_diff_max_lines_displayed) %></label> +<%= text_field_tag 'settings[diff_max_lines_displayed]', Setting.diff_max_lines_displayed, :size => 6 %></p> + +<p><label><%= l(:setting_gravatar_enabled) %></label> +<%= check_box_tag 'settings[gravatar_enabled]', 1, Setting.gravatar_enabled? %><%= hidden_field_tag 'settings[gravatar_enabled]', 0 %></p> +</div> + +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/settings/_issues.ctp b/app/views/settings/_issues.ctp new file mode 100644 index 0000000..bbd7356 --- /dev/null +++ b/app/views/settings/_issues.ctp @@ -0,0 +1,23 @@ +<% form_tag({:action => 'edit', :tab => 'issues'}) do %> + +<div class="box tabular settings"> +<p><label><%= l(:setting_cross_project_issue_relations) %></label> +<%= check_box_tag 'settings[cross_project_issue_relations]', 1, Setting.cross_project_issue_relations? %><%= hidden_field_tag 'settings[cross_project_issue_relations]', 0 %></p> + +<p><label><%= l(:setting_display_subprojects_issues) %></label> +<%= check_box_tag 'settings[display_subprojects_issues]', 1, Setting.display_subprojects_issues? %><%= hidden_field_tag 'settings[display_subprojects_issues]', 0 %></p> + +<p><label><%= l(:setting_issues_export_limit) %></label> +<%= text_field_tag 'settings[issues_export_limit]', Setting.issues_export_limit, :size => 6 %></p> +</div> + +<fieldset class="box"><legend><%= l(:setting_issue_list_default_columns) %></legend> +<%= hidden_field_tag 'settings[issue_list_default_columns][]', '' %> +<p><% Query.new.available_columns.each do |column| %> + <label><%= check_box_tag 'settings[issue_list_default_columns][]', column.name, Setting.issue_list_default_columns.include?(column.name.to_s) %> + <%= column.caption %></label> +<% end %></p> +</fieldset> + +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/settings/_mail_handler.ctp b/app/views/settings/_mail_handler.ctp new file mode 100644 index 0000000..830b1ba --- /dev/null +++ b/app/views/settings/_mail_handler.ctp @@ -0,0 +1,18 @@ +<% form_tag({:action => 'edit', :tab => 'mail_handler'}) do %> + +<div class="box tabular settings"> +<p><label><%= l(:setting_mail_handler_api_enabled) %></label> +<%= check_box_tag 'settings[mail_handler_api_enabled]', 1, Setting.mail_handler_api_enabled?, + :onclick => "if (this.checked) { Form.Element.enable('settings_mail_handler_api_key'); } else { Form.Element.disable('settings_mail_handler_api_key'); }" %> +<%= hidden_field_tag 'settings[mail_handler_api_enabled]', 0 %></p> + +<p><label><%= l(:setting_mail_handler_api_key) %></label> +<%= text_field_tag 'settings[mail_handler_api_key]', Setting.mail_handler_api_key, + :size => 30, + :id => 'settings_mail_handler_api_key', + :disabled => !Setting.mail_handler_api_enabled? %> +<%= link_to_function l(:label_generate_key), "if ($('settings_mail_handler_api_key').disabled == false) { $('settings_mail_handler_api_key').value = randomKey(20) }" %></p> +</div> + +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/settings/_notifications.ctp b/app/views/settings/_notifications.ctp new file mode 100644 index 0000000..bc01411 --- /dev/null +++ b/app/views/settings/_notifications.ctp @@ -0,0 +1,40 @@ +<% if @deliveries %> +<% form_tag({:action => 'edit', :tab => 'notifications'}) do %> + +<div class="box tabular settings"> +<p><label><%= l(:setting_mail_from) %></label> +<%= text_field_tag 'settings[mail_from]', Setting.mail_from, :size => 60 %></p> + +<p><label><%= l(:setting_bcc_recipients) %></label> +<%= check_box_tag 'settings[bcc_recipients]', 1, Setting.bcc_recipients? %> +<%= hidden_field_tag 'settings[bcc_recipients]', 0 %></p> + +<p><label><%= l(:setting_plain_text_mail) %></label> +<%= check_box_tag 'settings[plain_text_mail]', 1, Setting.plain_text_mail? %> +<%= hidden_field_tag 'settings[plain_text_mail]', 0 %></p> +</div> + +<fieldset class="box" id="notified_events"><legend><%=l(:text_select_mail_notifications)%></legend> +<% @notifiables.each do |notifiable| %> + <label><%= check_box_tag 'settings[notified_events][]', notifiable, Setting.notified_events.include?(notifiable) %> + <%= l_or_humanize(notifiable, :prefix => 'label_') %></label><br /> +<% end %> +<%= hidden_field_tag 'settings[notified_events][]', '' %> +<p><%= check_all_links('notified_events') %></p> +</fieldset> + +<fieldset class="box"><legend><%= l(:setting_emails_footer) %></legend> +<%= text_area_tag 'settings[emails_footer]', Setting.emails_footer, :class => 'wiki-edit', :rows => 5 %> +</fieldset> + +<div style="float:right;"> +<%= link_to l(:label_send_test_email), :controller => 'admin', :action => 'test_email' %> +</div> + +<%= submit_tag l(:button_save) %> +<% end %> +<% else %> +<div class="nodata"> +<%= simple_format(l(:text_email_delivery_not_configured)) %> +</div> +<% end %> diff --git a/app/views/settings/_projects.ctp b/app/views/settings/_projects.ctp new file mode 100644 index 0000000..1cd4f6e --- /dev/null +++ b/app/views/settings/_projects.ctp @@ -0,0 +1,12 @@ +<% form_tag({:action => 'edit', :tab => 'projects'}) do %> + +<div class="box tabular settings"> +<p><label><%= l(:setting_default_projects_public) %></label> +<%= check_box_tag 'settings[default_projects_public]', 1, Setting.default_projects_public? %><%= hidden_field_tag 'settings[default_projects_public]', 0 %></p> + +<p><label><%= l(:setting_sequential_project_identifiers) %></label> +<%= check_box_tag 'settings[sequential_project_identifiers]', 1, Setting.sequential_project_identifiers? %><%= hidden_field_tag 'settings[sequential_project_identifiers]', 0 %></p> +</div> + +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/settings/_repositories.ctp b/app/views/settings/_repositories.ctp new file mode 100644 index 0000000..a8c9244 --- /dev/null +++ b/app/views/settings/_repositories.ctp @@ -0,0 +1,36 @@ +<% form_tag({:action => 'edit', :tab => 'repositories'}) do %> + +<div class="box tabular settings"> +<p><label><%= l(:setting_autofetch_changesets) %></label> +<%= check_box_tag 'settings[autofetch_changesets]', 1, Setting.autofetch_changesets? %><%= hidden_field_tag 'settings[autofetch_changesets]', 0 %></p> + +<p><label><%= l(:setting_sys_api_enabled) %></label> +<%= check_box_tag 'settings[sys_api_enabled]', 1, Setting.sys_api_enabled? %><%= hidden_field_tag 'settings[sys_api_enabled]', 0 %></p> + +<p><label><%= l(:setting_enabled_scm) %></label> +<% REDMINE_SUPPORTED_SCM.each do |scm| -%> +<%= check_box_tag 'settings[enabled_scm][]', scm, Setting.enabled_scm.include?(scm) %> <%= scm %> +<% end -%> +<%= hidden_field_tag 'settings[enabled_scm][]', '' %> +</p> + +<p><label><%= l(:setting_repositories_encodings) %></label> +<%= text_field_tag 'settings[repositories_encodings]', Setting.repositories_encodings, :size => 60 %><br /><em><%= l(:text_comma_separated) %></em></p> + +<p><label><%= l(:setting_commit_logs_encoding) %></label> +<%= select_tag 'settings[commit_logs_encoding]', options_for_select(Setting::ENCODINGS, Setting.commit_logs_encoding) %></p> +</div> + +<fieldset class="box tabular settings"><legend><%= l(:text_issues_ref_in_commit_messages) %></legend> +<p><label><%= l(:setting_commit_ref_keywords) %></label> +<%= text_field_tag 'settings[commit_ref_keywords]', Setting.commit_ref_keywords, :size => 30 %><br /><em><%= l(:text_comma_separated) %></em></p> + +<p><label><%= l(:setting_commit_fix_keywords) %></label> +<%= text_field_tag 'settings[commit_fix_keywords]', Setting.commit_fix_keywords, :size => 30 %> +&nbsp;<%= l(:label_applied_status) %>: <%= select_tag 'settings[commit_fix_status_id]', options_for_select( [["", 0]] + IssueStatus.find(:all).collect{|status| [status.name, status.id.to_s]}, Setting.commit_fix_status_id) %> +&nbsp;<%= l(:field_done_ratio) %>: <%= select_tag 'settings[commit_fix_done_ratio]', options_for_select( [[l(:label_no_change_option), '']] + ((0..10).to_a.collect {|r| ["#{r*10} %", "#{r*10}"] }), Setting.commit_fix_done_ratio) %> +<br /><em><%= l(:text_comma_separated) %></em></p> +</fieldset> + +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/settings/edit.ctp b/app/views/settings/edit.ctp new file mode 100644 index 0000000..c99a139 --- /dev/null +++ b/app/views/settings/edit.ctp @@ -0,0 +1,23 @@ +<h2><%= l(:label_settings) %></h2> + +<% selected_tab = params[:tab] ? params[:tab].to_s : administration_settings_tabs.first[:name] %> + +<div class="tabs"> +<ul> +<% administration_settings_tabs.each do |tab| -%> + <li><%= link_to l(tab[:label]), { :tab => tab[:name] }, + :id => "tab-#{tab[:name]}", + :class => (tab[:name] != selected_tab ? nil : 'selected'), + :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %></li> +<% end -%> +</ul> +</div> + +<% administration_settings_tabs.each do |tab| -%> +<%= content_tag('div', render(:partial => tab[:partial]), + :id => "tab-content-#{tab[:name]}", + :style => (tab[:name] != selected_tab ? 'display:none' : nil), + :class => 'tab-content') %> +<% end -%> + +<% html_title(l(:label_settings), l(:label_administration)) -%> diff --git a/app/views/settings/plugin.ctp b/app/views/settings/plugin.ctp new file mode 100644 index 0000000..6191348 --- /dev/null +++ b/app/views/settings/plugin.ctp @@ -0,0 +1,10 @@ +<h2><%= l(:label_settings) %>: <%=h @plugin.name %></h2> + +<div id="settings"> +<% form_tag({:action => 'plugin'}) do %> +<div class="box tabular"> +<%= render :partial => @partial, :locals => {:settings => @settings}%> +</div> +<%= submit_tag l(:button_apply) %> +<% end %> +</div> diff --git a/app/views/timelog/_date_range.ctp b/app/views/timelog/_date_range.ctp new file mode 100644 index 0000000..ed84b16 --- /dev/null +++ b/app/views/timelog/_date_range.ctp @@ -0,0 +1,28 @@ +<fieldset id="filters"><legend><%= l(:label_date_range) %></legend> +<p> +<%= radio_button_tag 'period_type', '1', !@free_period %> +<%= select_tag 'period', options_for_period_select(params[:period]), + :onchange => 'this.form.onsubmit();', + :onfocus => '$("period_type_1").checked = true;' %> +</p> +<p> +<%= radio_button_tag 'period_type', '2', @free_period %> +<span onclick="$('period_type_2').checked = true;"> +<%= l(:label_date_from) %> +<%= text_field_tag 'from', @from, :size => 10 %> <%= calendar_for('from') %> +<%= l(:label_date_to) %> +<%= text_field_tag 'to', @to, :size => 10 %> <%= calendar_for('to') %> +</span> +<%= submit_tag l(:button_apply), :name => nil %> +</p> +</fieldset> + +<div class="tabs"> +<% url_params = @free_period ? { :from => @from, :to => @to } : { :period => params[:period] } %> +<ul> + <li><%= link_to(l(:label_details), url_params.merge({:controller => 'timelog', :action => 'details', :project_id => @project }), + :class => (@controller.action_name == 'details' ? 'selected' : nil)) %></li> + <li><%= link_to(l(:label_report), url_params.merge({:controller => 'timelog', :action => 'report', :project_id => @project}), + :class => (@controller.action_name == 'report' ? 'selected' : nil)) %></li> +</ul> +</div> diff --git a/app/views/timelog/_list.ctp b/app/views/timelog/_list.ctp new file mode 100644 index 0000000..1144d42 --- /dev/null +++ b/app/views/timelog/_list.ctp @@ -0,0 +1,41 @@ +<table class="list time-entries"> +<thead> +<tr> +<%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %> +<%= sort_header_tag('user', :caption => l(:label_member)) %> +<%= sort_header_tag('activity', :caption => l(:label_activity)) %> +<%= sort_header_tag('project', :caption => l(:label_project)) %> +<%= sort_header_tag('issue', :caption => l(:label_issue), :default_order => 'desc') %> +<th><%= l(:field_comments) %></th> +<%= sort_header_tag('hours', :caption => l(:field_hours)) %> +<th></th> +</tr> +</thead> +<tbody> +<% entries.each do |entry| -%> +<tr class="time-entry <%= cycle("odd", "even") %>"> +<td class="spent_on"><%= format_date(entry.spent_on) %></td> +<td class="user"><%=h entry.user %></td> +<td class="activity"><%=h entry.activity %></td> +<td class="project"><%=h entry.project %></td> +<td class="subject"> +<% if entry.issue -%> +<%= link_to_issue entry.issue %>: <%= h(truncate(entry.issue.subject, 50)) -%> +<% end -%> +</td> +<td class="comments"><%=h entry.comments %></td> +<td class="hours"><%= html_hours("%.2f" % entry.hours) %></td> +<td align="center"> +<% if entry.editable_by?(User.current) -%> + <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry, :project_id => nil}, + :title => l(:button_edit) %> + <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry, :project_id => nil}, + :confirm => l(:text_are_you_sure), + :method => :post, + :title => l(:button_delete) %> +<% end -%> +</td> +</tr> +<% end -%> +</tbody> +</table> diff --git a/app/views/timelog/_report_criteria.ctp b/app/views/timelog/_report_criteria.ctp new file mode 100644 index 0000000..c9a1cfb --- /dev/null +++ b/app/views/timelog/_report_criteria.ctp @@ -0,0 +1,19 @@ +<% @hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value| %> +<% hours_for_value = select_hours(hours, criterias[level], value) -%> +<% next if hours_for_value.empty? -%> +<tr class="<%= cycle('odd', 'even') %> <%= 'last-level' unless criterias.length > level+1 %>"> +<%= '<td></td>' * level %> +<td><%= h(format_criteria_value(criterias[level], value)) %></td> +<%= '<td></td>' * (criterias.length - level - 1) -%> + <% total = 0 -%> + <% @periods.each do |period| -%> + <% sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)); total += sum -%> + <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td> + <% end -%> + <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td> +</tr> +<% if criterias.length > level+1 -%> + <%= render(:partial => 'report_criteria', :locals => {:criterias => criterias, :hours => hours_for_value, :level => (level + 1)}) %> +<% end -%> + +<% end %> diff --git a/app/views/timelog/details.ctp b/app/views/timelog/details.ctp new file mode 100644 index 0000000..db62fae --- /dev/null +++ b/app/views/timelog/details.ctp @@ -0,0 +1,34 @@ +<div class="contextual"> +<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %> +</div> + +<%= render_timelog_breadcrumb %> + +<h2><%= l(:label_spent_time) %></h2> + +<% form_remote_tag( :url => {}, :method => :get, :update => 'content' ) do %> +<%= hidden_field_tag 'project_id', params[:project_id] %> +<%= hidden_field_tag 'issue_id', params[:issue_id] if @issue %> +<%= render :partial => 'date_range' %> +<% end %> + +<div class="total-hours"> +<p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p> +</div> + +<% unless @entries.empty? %> +<%= render :partial => 'list', :locals => { :entries => @entries }%> +<p class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></p> + +<p class="other-formats"> +<%= l(:label_export_to) %> +<span><%= link_to 'Atom', {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span> +<span><%= link_to 'CSV', params.merge(:format => 'csv'), :class => 'csv' %></span> +</p> +<% end %> + +<% html_title l(:label_spent_time), l(:label_details) %> + +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %> +<% end %> diff --git a/app/views/timelog/edit.ctp b/app/views/timelog/edit.ctp new file mode 100644 index 0000000..c403d8f --- /dev/null +++ b/app/views/timelog/edit.ctp @@ -0,0 +1,21 @@ +<h2><%= l(:label_spent_time) %></h2> + +<% labelled_tabular_form_for :time_entry, @time_entry, :url => {:action => 'edit', :project_id => @time_entry.project} do |f| %> +<%= error_messages_for 'time_entry' %> +<%= back_url_hidden_field_tag %> + +<div class="box"> +<p><%= f.text_field :issue_id, :size => 6 %> <em><%= h("#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}") if @time_entry.issue %></em></p> +<p><%= f.text_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p> +<p><%= f.text_field :hours, :size => 6, :required => true %></p> +<p><%= f.text_field :comments, :size => 100 %></p> +<p><%= f.select :activity_id, activity_collection_for_select_options, :required => true %></p> +<% @time_entry.custom_field_values.each do |value| %> + <p><%= custom_field_tag_with_label :time_entry, value %></p> +<% end %> +<%= call_hook(:view_timelog_edit_form_bottom, { :time_entry => @time_entry, :form => f }) %> +</div> + +<%= submit_tag l(:button_save) %> + +<% end %> diff --git a/app/views/timelog/report.ctp b/app/views/timelog/report.ctp new file mode 100644 index 0000000..eea0d0f --- /dev/null +++ b/app/views/timelog/report.ctp @@ -0,0 +1,74 @@ +<div class="contextual"> +<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %> +</div> + +<%= render_timelog_breadcrumb %> + +<h2><%= l(:label_spent_time) %></h2> + +<% form_remote_tag(:url => {}, :update => 'content') do %> + <% @criterias.each do |criteria| %> + <%= hidden_field_tag 'criterias[]', criteria, :id => nil %> + <% end %> + <%= hidden_field_tag 'project_id', params[:project_id] %> + <%= render :partial => 'date_range' %> + + <p><%= l(:label_details) %>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'], + [l(:label_month), 'month'], + [l(:label_week), 'week'], + [l(:label_day_plural).titleize, 'day']], @columns), + :onchange => "this.form.onsubmit();" %> + + <%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}), + :onchange => "this.form.onsubmit();", + :style => 'width: 200px', + :id => nil, + :disabled => (@criterias.length >= 3)) %> + <%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @columns}, + :update => 'content' + }, :class => 'icon icon-reload' %></p> +<% end %> + +<% unless @criterias.empty? %> +<div class="total-hours"> +<p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p> +</div> + +<% unless @hours.empty? %> +<table class="list" id="time-report"> +<thead> +<tr> +<% @criterias.each do |criteria| %> + <th><%= l(@available_criterias[criteria][:label]) %></th> +<% end %> +<% columns_width = (40 / (@periods.length+1)).to_i %> +<% @periods.each do |period| %> + <th class="period" width="<%= columns_width %>%"><%= period %></th> +<% end %> + <th class="total" width="<%= columns_width %>%"><%= l(:label_total) %></th> +</tr> +</thead> +<tbody> +<%= render :partial => 'report_criteria', :locals => {:criterias => @criterias, :hours => @hours, :level => 0} %> + <tr class="total"> + <td><%= l(:label_total) %></td> + <%= '<td></td>' * (@criterias.size - 1) %> + <% total = 0 -%> + <% @periods.each do |period| -%> + <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)); total += sum -%> + <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td> + <% end -%> + <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td> + </tr> +</tbody> +</table> + +<p class="other-formats"> +<%= l(:label_export_to) %> +<span><%= link_to 'CSV', params.merge({:format => 'csv'}), :class => 'csv' %></span> +</p> +<% end %> +<% end %> + +<% html_title l(:label_spent_time), l(:label_report) %> + diff --git a/app/views/trackers/_form.ctp b/app/views/trackers/_form.ctp new file mode 100644 index 0000000..856b70b --- /dev/null +++ b/app/views/trackers/_form.ctp @@ -0,0 +1,12 @@ +<%= error_messages_for 'tracker' %> +<div class="box"> +<!--[form:tracker]--> +<p><%= f.text_field :name, :required => true %></p> +<p><%= f.check_box :is_in_chlog %></p> +<p><%= f.check_box :is_in_roadmap %></p> +<% if @tracker.new_record? && @trackers.any? %> +<p><label><%= l(:label_copy_workflow_from) %></label> +<%= select_tag(:copy_workflow_from, content_tag("option") + options_from_collection_for_select(@trackers, :id, :name)) %></p> +<% end %> +<!--[eoform:tracker]--> +</div> diff --git a/app/views/trackers/edit.ctp b/app/views/trackers/edit.ctp new file mode 100644 index 0000000..d841109 --- /dev/null +++ b/app/views/trackers/edit.ctp @@ -0,0 +1,6 @@ +<h2><%=l(:label_tracker)%></h2> + +<% labelled_tabular_form_for :tracker, @tracker, :url => { :action => 'edit' } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> \ No newline at end of file diff --git a/app/views/trackers/list.ctp b/app/views/trackers/list.ctp new file mode 100644 index 0000000..2531f33 --- /dev/null +++ b/app/views/trackers/list.ctp @@ -0,0 +1,35 @@ +<div class="contextual"> +<%= link_to l(:label_tracker_new), {:action => 'new'}, :class => 'icon icon-add' %> +</div> + +<h2><%=l(:label_tracker_plural)%></h2> + +<table class="list"> + <thead><tr> + <th><%=l(:label_tracker)%></th> + <th></th> + <th><%=l(:button_sort)%></th> + <th></th> + </tr></thead> + <tbody> +<% for tracker in @trackers %> + <tr class="<%= cycle("odd", "even") %>"> + <td><%= link_to tracker.name, :action => 'edit', :id => tracker %></td> + <td align="center"><% unless tracker.workflows.count > 0 %><span class="icon icon-warning"><%= l(:text_tracker_no_workflow) %> (<%= link_to l(:button_edit), {:controller => 'workflows', :action => 'edit', :tracker_id => tracker} %>)</span><% end %></td> + <td align="center" style="width:15%;"> + <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => tracker, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> + <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => tracker, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> - + <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => tracker, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %> + <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => tracker, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %> + </td> + <td align="center" style="width:10%;"> + <%= button_to l(:button_delete), { :action => 'destroy', :id => tracker }, :confirm => l(:text_are_you_sure), :class => "button-small" %> + </td> + </tr> +<% end %> + </tbody> +</table> + +<p class="pagination"><%= pagination_links_full @tracker_pages %></p> + +<% html_title(l(:label_tracker_plural)) -%> diff --git a/app/views/trackers/new.ctp b/app/views/trackers/new.ctp new file mode 100644 index 0000000..b318a5d --- /dev/null +++ b/app/views/trackers/new.ctp @@ -0,0 +1,6 @@ +<h2><%=l(:label_tracker_new)%></h2> + +<% labelled_tabular_form_for :tracker, @tracker, :url => { :action => 'new' } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<% end %> \ No newline at end of file diff --git a/app/views/users/_form.ctp b/app/views/users/_form.ctp new file mode 100644 index 0000000..799ebde --- /dev/null +++ b/app/views/users/_form.ctp @@ -0,0 +1,31 @@ +<%= error_messages_for 'user' %> + +<!--[form:user]--> +<div class="box"> +<p><%= f.text_field :login, :required => true, :size => 25 %></p> +<p><%= f.text_field :firstname, :required => true %></p> +<p><%= f.text_field :lastname, :required => true %></p> +<p><%= f.text_field :mail, :required => true %></p> +<p><%= f.select :language, lang_options_for_select %></p> + +<% @user.custom_field_values.each do |value| %> + <p><%= custom_field_tag_with_label :user, value %></p> +<% end %> + +<p><%= f.check_box :admin, :disabled => (@user == User.current) %></p> +</div> + +<div class="box"> +<h3><%=l(:label_authentication)%></h3> +<% unless @auth_sources.empty? %> +<p><%= f.select :auth_source_id, ([[l(:label_internal), ""]] + @auth_sources.collect { |a| [a.name, a.id] }), {}, :onchange => "if (this.value=='') {Element.show('password_fields');} else {Element.hide('password_fields');}" %></p> +<% end %> +<div id="password_fields" style="<%= 'display:none;' if @user.auth_source %>"> +<p><label for="password"><%=l(:field_password)%><span class="required"> *</span></label> +<%= password_field_tag 'password', nil, :size => 25 %><br /> +<em><%= l(:text_caracters_minimum, 4) %></em></p> +<p><label for="password_confirmation"><%=l(:field_password_confirmation)%><span class="required"> *</span></label> +<%= password_field_tag 'password_confirmation', nil, :size => 25 %></p> +</div> +</div> +<!--[eoform:user]--> diff --git a/app/views/users/_general.ctp b/app/views/users/_general.ctp new file mode 100644 index 0000000..80615ff --- /dev/null +++ b/app/views/users/_general.ctp @@ -0,0 +1,4 @@ +<% labelled_tabular_form_for :user, @user, :url => { :action => "edit" } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/users/_memberships.ctp b/app/views/users/_memberships.ctp new file mode 100644 index 0000000..94b4915 --- /dev/null +++ b/app/views/users/_memberships.ctp @@ -0,0 +1,40 @@ +<% if @memberships.any? %> +<table class="list memberships"> + <thead> + <th><%= l(:label_project) %></th> + <th><%= l(:label_role) %></th> + <th style="width:15%"></th> + </thead> + <tbody> + <% @memberships.each do |membership| %> + <% next if membership.new_record? %> + <tr class="<%= cycle 'odd', 'even' %>"> + <td><%=h membership.project %></td> + <td align="center"> + <% form_tag({ :action => 'edit_membership', :id => @user, :membership_id => membership }) do %> + <%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name", membership.role_id) %> + <%= submit_tag l(:button_change), :class => "small" %> + <% end %> + </td> + <td align="center"> + <%= link_to l(:button_delete), {:action => 'destroy_membership', :id => @user, :membership_id => membership }, :method => :post, :class => 'icon icon-del' %> + </td> + </tr> + </tbody> +<% end; reset_cycle %> +</table> +<% else %> +<p class="nodata"><%= l(:label_no_data) %></p> +<% end %> + +<% if @projects.any? %> +<p> +<label><%=l(:label_project_new)%></label><br/> +<% form_tag({ :action => 'edit_membership', :id => @user }) do %> +<%= select_tag 'membership[project_id]', projects_options_for_select(@projects) %> +<%= l(:label_role) %>: +<%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name") %> +<%= submit_tag l(:button_add) %> +<% end %> +</p> +<% end %> diff --git a/app/views/users/add.ctp b/app/views/users/add.ctp new file mode 100644 index 0000000..636bdcb --- /dev/null +++ b/app/views/users/add.ctp @@ -0,0 +1,7 @@ +<h2><%=l(:label_user_new)%></h2> + +<% labelled_tabular_form_for :user, @user, :url => { :action => "add" } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<%= check_box_tag 'send_information', 1, true %> <%= l(:label_send_information) %> +<% end %> diff --git a/app/views/users/edit.ctp b/app/views/users/edit.ctp new file mode 100644 index 0000000..4714bce --- /dev/null +++ b/app/views/users/edit.ctp @@ -0,0 +1,27 @@ +<div class="contextual"> +<%= change_status_link(@user) %> +</div> + +<h2><%=l(:label_user)%>: <%=h @user.login %></h2> + +<% selected_tab = params[:tab] ? params[:tab].to_s : user_settings_tabs.first[:name] %> + +<div class="tabs"> +<ul> +<% user_settings_tabs.each do |tab| -%> + <li><%= link_to l(tab[:label]), { :tab => tab[:name] }, + :id => "tab-#{tab[:name]}", + :class => (tab[:name] != selected_tab ? nil : 'selected'), + :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %></li> +<% end -%> +</ul> +</div> + +<% user_settings_tabs.each do |tab| -%> +<%= content_tag('div', render(:partial => tab[:partial]), + :id => "tab-content-#{tab[:name]}", + :style => (tab[:name] != selected_tab ? 'display:none' : nil), + :class => 'tab-content') %> +<% end -%> + +<% html_title(l(:label_user), @user.login, l(:label_administration)) -%> diff --git a/app/views/users/list.ctp b/app/views/users/list.ctp new file mode 100644 index 0000000..758f176 --- /dev/null +++ b/app/views/users/list.ctp @@ -0,0 +1,47 @@ +<div class="contextual"> +<%= link_to l(:label_user_new), {:action => 'add'}, :class => 'icon icon-add' %> +</div> + +<h2><%=l(:label_user_plural)%></h2> + +<% form_tag({}, :method => :get) do %> +<fieldset><legend><%= l(:label_filter_plural) %></legend> +<label><%= l(:field_status) %>:</label> +<%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %> +<label><%= l(:label_user) %>:</label> +<%= text_field_tag 'name', params[:name], :size => 30 %> +<%= submit_tag l(:button_apply), :class => "small", :name => nil %> +</fieldset> +<% end %> +&nbsp; + +<table class="list"> + <thead><tr> + <%= sort_header_tag('login', :caption => l(:field_login)) %> + <%= sort_header_tag('firstname', :caption => l(:field_firstname)) %> + <%= sort_header_tag('lastname', :caption => l(:field_lastname)) %> + <%= sort_header_tag('mail', :caption => l(:field_mail)) %> + <%= sort_header_tag('admin', :caption => l(:field_admin), :default_order => 'desc') %> + <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %> + <%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on), :default_order => 'desc') %> + <th></th> + </tr></thead> + <tbody> +<% for user in @users -%> + <tr class="user <%= cycle("odd", "even") %> <%= %w(anon active registered locked)[user.status] %>"> + <td class="username"><%= avatar(user, :size => "14") %><%= link_to h(user.login), :action => 'edit', :id => user %></td> + <td class="firstname"><%= h(user.firstname) %></td> + <td class="lastname"><%= h(user.lastname) %></td> + <td class="email"><%= mail_to(h(user.mail)) %></td> + <td align="center"><%= image_tag('true.png') if user.admin? %></td> + <td class="created_on" align="center"><%= format_time(user.created_on) %></td> + <td class="last_login_on" align="center"><%= format_time(user.last_login_on) unless user.last_login_on.nil? %></td> + <td><small><%= change_status_link(user) %></small></td> + </tr> +<% end -%> + </tbody> +</table> + +<p class="pagination"><%= pagination_links_full @user_pages, @user_count %></p> + +<% html_title(l(:label_user_plural)) -%> diff --git a/app/views/versions/_form.ctp b/app/views/versions/_form.ctp new file mode 100644 index 0000000..adc83b5 --- /dev/null +++ b/app/views/versions/_form.ctp @@ -0,0 +1,8 @@ +<%= error_messages_for 'version' %> + +<div class="box"> +<p><%= f.text_field :name, :size => 60, :required => true %></p> +<p><%= f.text_field :description, :size => 60 %></p> +<p><%= f.text_field :wiki_page_title, :label => :label_wiki_page, :size => 60, :disabled => @project.wiki.nil? %></p> +<p><%= f.text_field :effective_date, :size => 10 %><%= calendar_for('version_effective_date') %></p> +</div> diff --git a/app/views/versions/_issue_counts.ctp b/app/views/versions/_issue_counts.ctp new file mode 100644 index 0000000..4bab5c6 --- /dev/null +++ b/app/views/versions/_issue_counts.ctp @@ -0,0 +1,35 @@ +<form id="status_by_form"> +<fieldset> +<legend> +<%= l(:label_issues_by, + select_tag('status_by', + status_by_options_for_select(criteria), + :id => 'status_by_select', + :onchange => remote_function(:url => { :action => :status_by, :id => version }, + :with => "Form.serialize('status_by_form')"))) %> +</legend> +<% if counts.empty? %> + <p><em><%= l(:label_no_data) %></em></p> +<% else %> + <table> + <% counts.each do |count| %> + <tr> + <td width="130px" align="right" > + <%= link_to count[:group], {:controller => 'issues', + :action => 'index', + :project_id => version.project, + :set_filter => 1, + :fixed_version_id => version, + "#{criteria}_id" => count[:group]} %> + </td> + <td width="240px"> + <%= progress_bar((count[:closed].to_f / count[:total])*100, + :legend => "#{count[:closed]}/#{count[:total]}", + :width => "#{(count[:total].to_f / max * 200).floor}px;") %> + </td> + </tr> + <% end %> + </table> +<% end %> +</fieldset> +</form> diff --git a/app/views/versions/_overview.ctp b/app/views/versions/_overview.ctp new file mode 100644 index 0000000..377e917 --- /dev/null +++ b/app/views/versions/_overview.ctp @@ -0,0 +1,22 @@ +<% if version.completed? %> + <p><%= format_date(version.effective_date) %></p> +<% elsif version.effective_date %> + <p><strong><%= due_date_distance_in_words(version.effective_date) %></strong> (<%= format_date(version.effective_date) %>)</p> +<% end %> + +<p><%=h version.description %></p> + +<% if version.fixed_issues.count > 0 %> + <%= progress_bar([version.closed_pourcent, version.completed_pourcent], :width => '40em', :legend => ('%0.0f%' % version.completed_pourcent)) %> + <p class="progress-info"> + <%= link_to(version.closed_issues_count, :controller => 'issues', :action => 'index', :project_id => version.project, :status_id => 'c', :fixed_version_id => version, :set_filter => 1) %> + <%= lwr(:label_closed_issues, version.closed_issues_count) %> + (<%= '%0.0f' % (version.closed_issues_count.to_f / version.fixed_issues.count * 100) %>%) + &#160; + <%= link_to(version.open_issues_count, :controller => 'issues', :action => 'index', :project_id => version.project, :status_id => 'o', :fixed_version_id => version, :set_filter => 1) %> + <%= lwr(:label_open_issues, version.open_issues_count)%> + (<%= '%0.0f' % (version.open_issues_count.to_f / version.fixed_issues.count * 100) %>%) + </p> +<% else %> + <p><em><%= l(:label_roadmap_no_issues) %></em></p> +<% end %> diff --git a/app/views/versions/edit.ctp b/app/views/versions/edit.ctp new file mode 100644 index 0000000..1556ebb --- /dev/null +++ b/app/views/versions/edit.ctp @@ -0,0 +1,7 @@ +<h2><%=l(:label_version)%></h2> + +<% labelled_tabular_form_for :version, @version, :url => { :action => 'edit' } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> + diff --git a/app/views/versions/show.ctp b/app/views/versions/show.ctp new file mode 100644 index 0000000..7e52645 --- /dev/null +++ b/app/views/versions/show.ctp @@ -0,0 +1,50 @@ +<div class="contextual"> +<%= link_to_if_authorized l(:button_edit), {:controller => 'versions', :action => 'edit', :id => @version}, :class => 'icon icon-edit' %> +</div> + +<h2><%= h(@version.name) %></h2> + +<div id="version-summary"> +<% if @version.estimated_hours > 0 || User.current.allowed_to?(:view_time_entries, @project) %> +<fieldset><legend><%= l(:label_time_tracking) %></legend> +<table> +<tr> + <td width="130px" align="right"><%= l(:field_estimated_hours) %></td> + <td width="240px" class="total-hours"width="130px" align="right"><%= html_hours(lwr(:label_f_hour, @version.estimated_hours)) %></td> +</tr> +<% if User.current.allowed_to?(:view_time_entries, @project) %> +<tr> + <td width="130px" align="right"><%= l(:label_spent_time) %></td> + <td width="240px" class="total-hours"><%= html_hours(lwr(:label_f_hour, @version.spent_hours)) %></td> +</tr> +<% end %> +</table> +</fieldset> +<% end %> + +<div id="status_by"> +<%= render_issue_status_by(@version, params[:status_by]) if @version.fixed_issues.count > 0 %> +</div> +</div> + +<div id="roadmap"> +<%= render :partial => 'versions/overview', :locals => {:version => @version} %> +<%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %> + +<% issues = @version.fixed_issues.find(:all, + :include => [:status, :tracker], + :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") %> +<% if issues.size > 0 %> +<fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend> +<ul> +<% issues.each do |issue| -%> + <li><%= link_to_issue(issue) %>: <%=h issue.subject %></li> +<% end -%> +</ul> +</fieldset> +<% end %> +</div> + +<%= call_hook :view_versions_show_bottom, :version => @version %> + +<% html_title @version.name %> diff --git a/app/views/watchers/_watchers.ctp b/app/views/watchers/_watchers.ctp new file mode 100644 index 0000000..14bb5fc --- /dev/null +++ b/app/views/watchers/_watchers.ctp @@ -0,0 +1,25 @@ +<div class="contextual"> +<%= link_to_remote l(:button_add), + :url => {:controller => 'watchers', + :action => 'new', + :object_type => watched.class.name.underscore, + :object_id => watched} if User.current.allowed_to?(:add_issue_watchers, @project) %> +</div> + +<p><strong><%= l(:label_issue_watchers) %></strong></p> +<%= watchers_list(watched) %> + +<% unless @watcher.nil? %> +<% remote_form_for(:watcher, @watcher, + :url => {:controller => 'watchers', + :action => 'new', + :object_type => watched.class.name.underscore, + :object_id => watched}, + :method => :post, + :html => {:id => 'new-watcher-form'}) do |f| %> +<p><%= f.select :user_id, (watched.addable_watcher_users.collect {|m| [m.name, m.id]}), :prompt => true %> + +<%= submit_tag l(:button_add) %> +<%= toggle_link l(:button_cancel), 'new-watcher-form'%></p> +<% end %> +<% end %> diff --git a/app/views/welcome/index.ctp b/app/views/welcome/index.ctp new file mode 100644 index 0000000..855248c --- /dev/null +++ b/app/views/welcome/index.ctp @@ -0,0 +1,35 @@ +<h2><%= l(:label_home) %></h2> + +<div class="splitcontentleft"> + <%= textilizable Setting.welcome_text %> + <% if @news.any? %> + <div class="box"> + <h3><%=l(:label_news_latest)%></h3> + <%= render :partial => 'news/news', :collection => @news %> + <%= link_to l(:label_news_view_all), :controller => 'news' %> + </div> + <% end %> +</div> + +<div class="splitcontentright"> + <% if @projects.any? %> + <div class="box"> + <h3 class="icon22 icon22-projects"><%=l(:label_project_latest)%></h3> + <ul> + <% for project in @projects %> + <li> + <%= link_to h(project.name), :controller => 'projects', :action => 'show', :id => project %> (<%= format_time(project.created_on) %>) + <%= textilizable project.short_description, :project => project %> + </li> + <% end %> + </ul> + </div> + <% end %> +</div> + +<% content_for :header_tags do %> +<%= auto_discovery_link_tag(:atom, {:controller => 'news', :action => 'index', :key => User.current.rss_key, :format => 'atom'}, + :title => "#{Setting.app_title}: #{l(:label_news_latest)}") %> +<%= auto_discovery_link_tag(:atom, {:controller => 'projects', :action => 'activity', :key => User.current.rss_key, :format => 'atom'}, + :title => "#{Setting.app_title}: #{l(:label_activity)}") %> +<% end %> diff --git a/app/views/wiki/_content.ctp b/app/views/wiki/_content.ctp new file mode 100644 index 0000000..421d26c --- /dev/null +++ b/app/views/wiki/_content.ctp @@ -0,0 +1,3 @@ +<div class="wiki"> + <%= textilizable content, :text, :attachments => content.page.attachments %> +</div> diff --git a/app/views/wiki/_sidebar.ctp b/app/views/wiki/_sidebar.ctp new file mode 100644 index 0000000..20c0871 --- /dev/null +++ b/app/views/wiki/_sidebar.ctp @@ -0,0 +1,5 @@ +<h3><%= l(:label_wiki) %></h3> + +<%= link_to l(:field_start_page), {:action => 'index', :page => nil} %><br /> +<%= link_to l(:label_index_by_title), {:action => 'special', :page => 'Page_index'} %><br /> +<%= link_to l(:label_index_by_date), {:action => 'special', :page => 'Date_index'} %><br /> diff --git a/app/views/wiki/annotate.ctp b/app/views/wiki/annotate.ctp new file mode 100644 index 0000000..c274516 --- /dev/null +++ b/app/views/wiki/annotate.ctp @@ -0,0 +1,32 @@ +<div class="contextual"> +<%= link_to(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit') %> +<%= link_to(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %> +</div> + +<h2><%= @page.pretty_title %></h2> + +<p> +<%= l(:label_version) %> <%= link_to @annotate.content.version, :action => 'index', :page => @page.title, :version => @annotate.content.version %> +<em>(<%= @annotate.content.author ? @annotate.content.author.name : "anonyme" %>, <%= format_time(@annotate.content.updated_on) %>)</em> +</p> + +<% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %> + +<table class="filecontent annotate CodeRay "> +<tbody> +<% line_num = 1 %> +<% @annotate.lines.each do |line| -%> +<tr class="bloc-<%= colors[line[0]] %>"> + <th class="line-num"><%= line_num %></th> + <td class="revision"><%= link_to line[0], :controller => 'wiki', :action => 'index', :id => @project, :page => @page.title, :version => line[0] %></td> + <td class="author"><%= h(line[1]) %></td> + <td class="line-code"><pre><%=h line[2] %></pre></td> +</tr> +<% line_num += 1 %> +<% end -%> +</tbody> +</table> + +<% content_for :header_tags do %> +<%= stylesheet_link_tag 'scm' %> +<% end %> diff --git a/app/views/wiki/diff.ctp b/app/views/wiki/diff.ctp new file mode 100644 index 0000000..512d410 --- /dev/null +++ b/app/views/wiki/diff.ctp @@ -0,0 +1,18 @@ +<div class="contextual"> +<%= link_to(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit') %> +<%= link_to(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %> +</div> + +<h2><%= @page.pretty_title %></h2> + +<p> +<%= l(:label_version) %> <%= link_to @diff.content_from.version, :action => 'index', :page => @page.title, :version => @diff.content_from.version %> +<em>(<%= @diff.content_from.author ? @diff.content_from.author.name : "anonyme" %>, <%= format_time(@diff.content_from.updated_on) %>)</em> +&#8594; +<%= l(:label_version) %> <%= link_to @diff.content_to.version, :action => 'index', :page => @page.title, :version => @diff.content_to.version %>/<%= @page.content.version %> +<em>(<%= @diff.content_to.author ? @diff.content_to.author.name : "anonyme" %>, <%= format_time(@diff.content_to.updated_on) %>)</em> +</p> + +<hr /> + +<%= html_diff(@diff) %> diff --git a/app/views/wiki/edit.ctp b/app/views/wiki/edit.ctp new file mode 100644 index 0000000..19f3bd5 --- /dev/null +++ b/app/views/wiki/edit.ctp @@ -0,0 +1,26 @@ +<h2><%= @page.pretty_title %></h2> + +<% form_for :content, @content, :url => {:action => 'edit', :page => @page.title}, :html => {:id => 'wiki_form'} do |f| %> +<%= f.hidden_field :version %> +<%= error_messages_for 'content' %> + +<p><%= f.text_area :text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit) %></p> +<p><label><%= l(:field_comments) %></label><br /><%= f.text_field :comments, :size => 120 %></p> +<p><%= submit_tag l(:button_save) %> + <%= link_to_remote l(:label_preview), + { :url => { :controller => 'wiki', :action => 'preview', :id => @project, :page => @page.title }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('wiki_form')", + :complete => "Element.scrollTo('preview')" + }, :accesskey => accesskey(:preview) %></p> +<%= wikitoolbar_for 'content_text' %> +<% end %> + +<div id="preview" class="wiki"></div> + +<% content_for :header_tags do %> + <%= stylesheet_link_tag 'scm' %> +<% end %> + +<% html_title @page.pretty_title %> diff --git a/app/views/wiki/export.ctp b/app/views/wiki/export.ctp new file mode 100644 index 0000000..7f861fa --- /dev/null +++ b/app/views/wiki/export.ctp @@ -0,0 +1,18 @@ +<!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><%=h @page.pretty_title %></title> +<meta http-equiv="content-type" content="text/html; charset=utf-8" /> +<style> +body { font:80% Verdana,Tahoma,Arial,sans-serif; } +h1, h2, h3, h4 { font-family: "Trebuchet MS",Georgia,"Times New Roman",serif; } +ul.toc { padding: 4px; margin-left: 0; } +ul.toc li { list-style-type:none; } +ul.toc li.heading2 { margin-left: 1em; } +ul.toc li.heading3 { margin-left: 2em; } +</style> +</head> +<body> +<%= textilizable @content, :text, :wiki_links => :local %> +</body> +</html> diff --git a/app/views/wiki/export_multiple.ctp b/app/views/wiki/export_multiple.ctp new file mode 100644 index 0000000..a4e4c5e --- /dev/null +++ b/app/views/wiki/export_multiple.ctp @@ -0,0 +1,27 @@ +<!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><%=h @wiki.project.name %></title> +<meta http-equiv="content-type" content="text/html; charset=utf-8" /> +<style> +body { font:80% Verdana,Tahoma,Arial,sans-serif; } +h1, h2, h3, h4 { font-family: "Trebuchet MS",Georgia,"Times New Roman",serif; } +</style> +</head> +<body> + +<strong><%= l(:label_index_by_title) %></strong> +<ul> +<% @pages.each do |page| %> + <li><a href="#<%= page.title %>"><%= page.pretty_title %></a></li> +<% end %> +</ul> + +<% @pages.each do |page| %> +<hr /> +<a name="<%= page.title %>" /> +<%= textilizable page.content ,:text, :wiki_links => :anchor %> +<% end %> + +</body> +</html> diff --git a/app/views/wiki/history.ctp b/app/views/wiki/history.ctp new file mode 100644 index 0000000..7ce78a0 --- /dev/null +++ b/app/views/wiki/history.ctp @@ -0,0 +1,35 @@ +<h2><%= @page.pretty_title %></h2> + +<h3><%= l(:label_history) %></h3> + +<% form_tag({:action => "diff"}, :method => :get) do %> +<table class="list"> +<thead><tr> + <th>#</th> + <th></th> + <th></th> + <th><%= l(:field_updated_on) %></th> + <th><%= l(:field_author) %></th> + <th><%= l(:field_comments) %></th> + <th></th> +</tr></thead> +<tbody> +<% show_diff = @versions.size > 1 %> +<% line_num = 1 %> +<% @versions.each do |ver| %> +<tr class="<%= cycle("odd", "even") %>"> + <td class="id"><%= link_to ver.version, :action => 'index', :page => @page.title, :version => ver.version %></td> + <td class="checkbox"><%= radio_button_tag('version', ver.version, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < @versions.size) %></td> + <td class="checkbox"><%= radio_button_tag('version_from', ver.version, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true || $('version_from').value > #{ver.version}) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td> + <td align="center"><%= format_time(ver.updated_on) %></td> + <td><em><%= ver.author ? ver.author.name : "anonyme" %></em></td> + <td><%=h ver.comments %></td> + <td align="center"><%= link_to l(:button_annotate), :action => 'annotate', :page => @page.title, :version => ver.version %></td> +</tr> +<% line_num += 1 %> +<% end %> +</tbody> +</table> +<%= submit_tag l(:label_view_diff), :class => 'small' if show_diff %> +<span class="pagination"><%= pagination_links_full @version_pages, @version_count, :page_param => :p %></span> +<% end %> diff --git a/app/views/wiki/rename.ctp b/app/views/wiki/rename.ctp new file mode 100644 index 0000000..260f9af --- /dev/null +++ b/app/views/wiki/rename.ctp @@ -0,0 +1,12 @@ +<h2><%= l(:button_rename) %>: <%= @original_title %></h2> + +<%= error_messages_for 'page' %> + +<% labelled_tabular_form_for :wiki_page, @page, :url => { :action => 'rename' } do |f| %> +<div class="box"> +<p><%= f.text_field :title, :required => true, :size => 100 %></p> +<p><%= f.check_box :redirect_existing_links %></p> +<p><%= f.text_field :parent_title, :size => 100 %></p> +</div> +<%= submit_tag l(:button_rename) %> +<% end %> diff --git a/app/views/wiki/show.ctp b/app/views/wiki/show.ctp new file mode 100644 index 0000000..1cb67df --- /dev/null +++ b/app/views/wiki/show.ctp @@ -0,0 +1,59 @@ +<div class="contextual"> +<% if @editable %> +<%= link_to_if_authorized(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if @content.version == @page.content.version %> +<%= link_to_if_authorized(l(:button_lock), {:action => 'protect', :page => @page.title, :protected => 1}, :method => :post, :class => 'icon icon-lock') if !@page.protected? %> +<%= link_to_if_authorized(l(:button_unlock), {:action => 'protect', :page => @page.title, :protected => 0}, :method => :post, :class => 'icon icon-unlock') if @page.protected? %> +<%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :page => @page.title}, :class => 'icon icon-move') if @content.version == @page.content.version %> +<%= link_to_if_authorized(l(:button_delete), {:action => 'destroy', :page => @page.title}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') %> +<%= link_to_if_authorized(l(:button_rollback), {:action => 'edit', :page => @page.title, :version => @content.version }, :class => 'icon icon-cancel') if @content.version < @page.content.version %> +<% end %> +<%= link_to_if_authorized(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %> +</div> + +<%= breadcrumb(@page.ancestors.reverse.collect {|parent| link_to h(parent.pretty_title), {:page => parent.title}}) %> + +<% if @content.version != @page.content.version %> + <p> + <%= link_to(('&#171; ' + l(:label_previous)), :action => 'index', :page => @page.title, :version => (@content.version - 1)) + " - " if @content.version > 1 %> + <%= "#{l(:label_version)} #{@content.version}/#{@page.content.version}" %> + <%= '(' + link_to('diff', :controller => 'wiki', :action => 'diff', :page => @page.title, :version => @content.version) + ')' if @content.version > 1 %> - + <%= link_to((l(:label_next) + ' &#187;'), :action => 'index', :page => @page.title, :version => (@content.version + 1)) + " - " if @content.version < @page.content.version %> + <%= link_to(l(:label_current_version), :action => 'index', :page => @page.title) %> + <br /> + <em><%= @content.author ? @content.author.name : "anonyme" %>, <%= format_time(@content.updated_on) %> </em><br /> + <%=h @content.comments %> + </p> + <hr /> +<% end %> + +<%= render(:partial => "wiki/content", :locals => {:content => @content}) %> + +<%= link_to_attachments @page %> + +<% if @editable && authorize_for('wiki', 'add_attachment') %> +<p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;", + :id => 'attach_files_link' %></p> +<% form_tag({ :controller => 'wiki', :action => 'add_attachment', :page => @page.title }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %> + <div class="box"> + <p><%= render :partial => 'attachments/form' %></p> + </div> +<%= submit_tag l(:button_add) %> +<%= link_to l(:button_cancel), {}, :onclick => "Element.hide('add_attachment_form'); Element.show('attach_files_link'); return false;" %> +<% end %> +<% end %> + +<p class="other-formats"> +<%= l(:label_export_to) %> +<span><%= link_to 'HTML', {:page => @page.title, :export => 'html', :version => @content.version}, :class => 'html' %></span> +<span><%= link_to 'TXT', {:page => @page.title, :export => 'txt', :version => @content.version}, :class => 'text' %></span> +</p> + +<% content_for :header_tags do %> + <%= stylesheet_link_tag 'scm' %> +<% end %> + +<% content_for :sidebar do %> + <%= render :partial => 'sidebar' %> +<% end %> + +<% html_title @page.pretty_title %> diff --git a/app/views/wiki/special_date_index.ctp b/app/views/wiki/special_date_index.ctp new file mode 100644 index 0000000..6717ebc --- /dev/null +++ b/app/views/wiki/special_date_index.ctp @@ -0,0 +1,30 @@ +<h2><%= l(:label_index_by_date) %></h2> + +<% if @pages.empty? %> +<p class="nodata"><%= l(:label_no_data) %></p> +<% end %> + +<% @pages_by_date.keys.sort.reverse.each do |date| %> +<h3><%= format_date(date) %></h3> +<ul> +<% @pages_by_date[date].each do |page| %> + <li><%= link_to page.pretty_title, :action => 'index', :page => page.title %></li> +<% end %> +</ul> +<% end %> + +<% content_for :sidebar do %> + <%= render :partial => 'sidebar' %> +<% end %> + +<% unless @pages.empty? %> +<p class="other-formats"> +<%= l(:label_export_to) %> +<span><%= link_to 'Atom', {:controller => 'projects', :action => 'activity', :id => @project, :show_wiki_pages => 1, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span> +<span><%= link_to 'HTML', {:action => 'special', :page => 'export'}, :class => 'html' %></span> +</p> +<% end %> + +<% content_for :header_tags do %> +<%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :id => @project, :show_wiki_pages => 1, :format => 'atom', :key => User.current.rss_key) %> +<% end %> diff --git a/app/views/wiki/special_page_index.ctp b/app/views/wiki/special_page_index.ctp new file mode 100644 index 0000000..72b395e --- /dev/null +++ b/app/views/wiki/special_page_index.ctp @@ -0,0 +1,23 @@ +<h2><%= l(:label_index_by_title) %></h2> + +<% if @pages.empty? %> +<p class="nodata"><%= l(:label_no_data) %></p> +<% end %> + +<%= render_page_hierarchy(@pages_by_parent_id) %> + +<% content_for :sidebar do %> + <%= render :partial => 'sidebar' %> +<% end %> + +<% unless @pages.empty? %> +<p class="other-formats"> +<%= l(:label_export_to) %> +<span><%= link_to 'Atom', {:controller => 'projects', :action => 'activity', :id => @project, :show_wiki_pages => 1, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span> +<span><%= link_to 'HTML', {:action => 'special', :page => 'export'} %></span> +</p> +<% end %> + +<% content_for :header_tags do %> +<%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :id => @project, :show_wiki_pages => 1, :format => 'atom', :key => User.current.rss_key) %> +<% end %> diff --git a/app/views/wikis/destroy.ctp b/app/views/wikis/destroy.ctp new file mode 100644 index 0000000..b5b1de1 --- /dev/null +++ b/app/views/wikis/destroy.ctp @@ -0,0 +1,10 @@ +<h2><%=l(:label_confirmation)%></h2> + +<div class="box"><center> +<p><strong><%= @project.name %></strong><br /><%=l(:text_wiki_destroy_confirmation)%></p> + +<% form_tag({:controller => 'wikis', :action => 'destroy', :id => @project}) do %> +<%= hidden_field_tag "confirm", 1 %> +<%= submit_tag l(:button_delete) %> +<% end %> +</center></div> diff --git a/app/views/workflows/edit.ctp b/app/views/workflows/edit.ctp new file mode 100644 index 0000000..1aef8eb --- /dev/null +++ b/app/views/workflows/edit.ctp @@ -0,0 +1,66 @@ +<div class="contextual"> +<%= link_to l(:field_summary), :action => 'index' %> +</div> + +<h2><%=l(:label_workflow)%></h2> + +<p><%=l(:text_workflow_edit)%>:</p> + +<% form_tag({}, :method => 'get') do %> +<p><label for="role_id"><%=l(:label_role)%>:</label> +<select name="role_id"> + <%= options_from_collection_for_select @roles, "id", "name", (@role.id unless @role.nil?) %> +</select> + +<label for="tracker_id"><%=l(:label_tracker)%>:</label> +<select name="tracker_id"> + <%= options_from_collection_for_select @trackers, "id", "name", (@tracker.id unless @tracker.nil?) %> +</select> +<%= submit_tag l(:button_edit), :name => nil %> +</p> +<% end %> + + + +<% unless @tracker.nil? or @role.nil? or @statuses.empty? %> +<% form_tag({}, :id => 'workflow_form' ) do %> +<%= hidden_field_tag 'tracker_id', @tracker.id %> +<%= hidden_field_tag 'role_id', @role.id %> +<table class="list"> +<thead> + <tr> + <th align="left"><%=l(:label_current_status)%></th> + <th align="center" colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th> + </tr> + <tr> + <td></td> + <% for new_status in @statuses %> + <td width="<%= 75 / @statuses.size %>%" align="center"><%= new_status.name %></td> + <% end %> + </tr> +</thead> +<tbody> + <% for old_status in @statuses %> + <tr class="<%= cycle("odd", "even") %>"> + <td><%= old_status.name %></td> + <% new_status_ids_allowed = old_status.find_new_statuses_allowed_to(@role, @tracker).collect(&:id) -%> + <% for new_status in @statuses -%> + <td align="center"> + <input type="checkbox" + name="issue_status[<%= old_status.id %>][]" + value="<%= new_status.id %>" + <%= 'checked="checked"' if new_status_ids_allowed.include? new_status.id %> /> + </td> + <% end -%> + </tr> + <% end %> +</tbody> +</table> +<p><%= check_all_links 'workflow_form' %></p> + +<%= submit_tag l(:button_save) %> +<% end %> + +<% end %> + +<% html_title(l(:label_workflow)) -%> diff --git a/app/views/workflows/index.ctp b/app/views/workflows/index.ctp new file mode 100644 index 0000000..2fd080d --- /dev/null +++ b/app/views/workflows/index.ctp @@ -0,0 +1,31 @@ +<h2><%=l(:label_workflow)%></h2> + +<% if @workflow_counts.empty? %> +<p class="nodata"><%= l(:label_no_data) %></p> +<% else %> +<table class="list"> +<thead> + <tr> + <th></th> + <% @workflow_counts.first.last.each do |role, count| %> + <th> + <%= content_tag(role.builtin? ? 'em' : 'span', h(role.name)) %> + </th> + + <% end %> + </tr> +</thead> +<tbody> +<% @workflow_counts.each do |tracker, roles| -%> +<tr class="<%= cycle('odd', 'even') %>"> + <td><%= h tracker %></td> + <% roles.each do |role, count| -%> + <td align="center"> + <%= link_to((count > 1 ? count : image_tag('false.png')), {:action => 'edit', :role_id => role, :tracker_id => tracker}, :title => l(:button_edit)) %> + </td> + <% end -%> +</tr> +<% end -%> +</tbody> +</table> +<% end %> diff --git a/app/webroot/.htaccess b/app/webroot/.htaccess new file mode 100755 index 0000000..f9d8b93 --- /dev/null +++ b/app/webroot/.htaccess @@ -0,0 +1,6 @@ +<IfModule mod_rewrite.c> + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^(.*)$ index.php?url=$1 [QSA,L] +</IfModule> \ No newline at end of file diff --git a/app/webroot/css.php b/app/webroot/css.php new file mode 100755 index 0000000..eb9be9f --- /dev/null +++ b/app/webroot/css.php @@ -0,0 +1,102 @@ +<?php +/* SVN FILE: $Id: css.php 7945 2008-12-19 02:16:01Z gwoo $ */ +/** + * Short description for file. + * + * Long description for file + * + * PHP versions 4 and 5 + * + * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org) + * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project + * @package cake + * @subpackage cake.app.webroot + * @since CakePHP(tm) v 0.2.9 + * @version $Revision: 7945 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2008-12-18 18:16:01 -0800 (Thu, 18 Dec 2008) $ + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ +if (!defined('CAKE_CORE_INCLUDE_PATH')) { + header('HTTP/1.1 404 Not Found'); + exit('File Not Found'); +} +/** + * Enter description here... + */ +if (!class_exists('File')) { + uses('file'); +} +/** + * Enter description here... + * + * @param unknown_type $path + * @param unknown_type $name + * @return unknown + */ + function make_clean_css($path, $name) { + App::import('Vendor', 'csspp' . DS . 'csspp'); + $data = file_get_contents($path); + $csspp = new csspp(); + $output = $csspp->compress($data); + $ratio = 100 - (round(strlen($output) / strlen($data), 3) * 100); + $output = " /* file: $name, ratio: $ratio% */ " . $output; + return $output; + } +/** + * Enter description here... + * + * @param unknown_type $path + * @param unknown_type $content + * @return unknown + */ + function write_css_cache($path, $content) { + if (!is_dir(dirname($path))) { + mkdir(dirname($path)); + } + $cache = new File($path); + return $cache->write($content); + } + + if (preg_match('|\.\.|', $url) || !preg_match('|^ccss/(.+)$|i', $url, $regs)) { + die('Wrong file name.'); + } + + $filename = 'css/' . $regs[1]; + $filepath = CSS . $regs[1]; + $cachepath = CACHE . 'css' . DS . str_replace(array('/','\\'), '-', $regs[1]); + + if (!file_exists($filepath)) { + die('Wrong file name.'); + } + + if (file_exists($cachepath)) { + $templateModified = filemtime($filepath); + $cacheModified = filemtime($cachepath); + + if ($templateModified > $cacheModified) { + $output = make_clean_css($filepath, $filename); + write_css_cache($cachepath, $output); + } else { + $output = file_get_contents($cachepath); + } + } else { + $output = make_clean_css($filepath, $filename); + write_css_cache($cachepath, $output); + $templateModified = time(); + } + + header("Date: " . date("D, j M Y G:i:s ", $templateModified) . 'GMT'); + header("Content-Type: text/css"); + header("Expires: " . gmdate("D, j M Y H:i:s", time() + DAY) . " GMT"); + header("Cache-Control: max-age=86400, must-revalidate"); // HTTP/1.1 + header("Pragma: cache"); // HTTP/1.0 + print $output; +?> \ No newline at end of file diff --git a/app/webroot/css/application.css b/app/webroot/css/application.css new file mode 100644 index 0000000..824b4cb --- /dev/null +++ b/app/webroot/css/application.css @@ -0,0 +1,682 @@ +body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; } + +h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;} +h1 {margin:0; padding:0; font-size: 24px;} +h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;} +h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;} +h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;} + +/***** Layout *****/ +#wrapper {background: white;} + +#top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;} +#top-menu ul {margin: 0; padding: 0;} +#top-menu li { + float:left; + list-style-type:none; + margin: 0px 0px 0px 0px; + padding: 0px 0px 0px 0px; + white-space:nowrap; +} +#top-menu a {color: #fff; padding-right: 8px; font-weight: bold;} +#top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; } + +#account {float:right;} + +#header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;} +#header a {color:#f8f8f8;} +#quick-search {float:right;} + +#main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;} +#main-menu ul {margin: 0; padding: 0;} +#main-menu li { + float:left; + list-style-type:none; + margin: 0px 2px 0px 0px; + padding: 0px 0px 0px 0px; + white-space:nowrap; +} +#main-menu li a { + display: block; + color: #fff; + text-decoration: none; + font-weight: bold; + margin: 0; + padding: 4px 10px 4px 10px; +} +#main-menu li a:hover {background:#759FCF; color:#fff;} +#main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;} + +#main {background-color:#EEEEEE;} + +#sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;} +* html #sidebar{ width: 17%; } +#sidebar h3{ font-size: 14px; margin-top:14px; color: #666; } +#sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; } +* html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; } + +#content { width: 80%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; } +* html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;} +html>body #content { min-height: 600px; } +* html body #content { height: 600px; } /* IE */ + +#main.nosidebar #sidebar{ display: none; } +#main.nosidebar #content{ width: auto; border-right: 0; } + +#footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;} + +#login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; } +#login-form table td {padding: 6px;} +#login-form label {font-weight: bold;} + +.clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; } + +/***** Links *****/ +a, a:link, a:visited{ color: #2A5685; text-decoration: none; } +a:hover, a:active{ color: #c61a1a; text-decoration: underline;} +a img{ border: 0; } + +a.issue.closed { text-decoration: line-through; } + +/***** Tables *****/ +table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; } +table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; } +table.list td { vertical-align: top; } +table.list td.id { width: 2%; text-align: center;} +table.list td.checkbox { width: 15px; padding: 0px;} + +tr.issue { text-align: center; white-space: nowrap; } +tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; } +tr.issue td.subject { text-align: left; } +tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;} + +tr.entry { border: 1px solid #f8f8f8; } +tr.entry td { white-space: nowrap; } +tr.entry td.filename { width: 30%; } +tr.entry td.size { text-align: right; font-size: 90%; } +tr.entry td.revision, tr.entry td.author { text-align: center; } +tr.entry td.age { text-align: right; } + +tr.entry span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;} +tr.entry.open span.expander {background-image: url(../images/bullet_toggle_minus.png);} +tr.entry.file td.filename a { margin-left: 16px; } + +tr.changeset td.author { text-align: center; width: 15%; } +tr.changeset td.committed_on { text-align: center; width: 15%; } + +tr.message { height: 2.6em; } +tr.message td.last_message { font-size: 80%; } +tr.message.locked td.subject a { background-image: url(../images/locked.png); } +tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; } + +tr.user td { width:13%; } +tr.user td.email { width:18%; } +tr.user td { white-space: nowrap; } +tr.user.locked, tr.user.registered { color: #aaa; } +tr.user.locked a, tr.user.registered a { color: #aaa; } + +tr.time-entry { text-align: center; white-space: nowrap; } +tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; } +td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; } +td.hours .hours-dec { font-size: 0.9em; } + +table.plugins td { vertical-align: middle; } +table.plugins td.configure { text-align: right; padding-right: 1em; } +table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; } +table.plugins span.description { display: block; font-size: 0.9em; } +table.plugins span.url { display: block; font-size: 0.9em; } + +table.list tbody tr:hover { background-color:#ffffdd; } +table td {padding:2px;} +table p {margin:0;} +.odd {background-color:#f6f7f8;} +.even {background-color: #fff;} + +.highlight { background-color: #FCFD8D;} +.highlight.token-1 { background-color: #faa;} +.highlight.token-2 { background-color: #afa;} +.highlight.token-3 { background-color: #aaf;} + +.box{ +padding:6px; +margin-bottom: 10px; +background-color:#f6f6f6; +color:#505050; +line-height:1.5em; +border: 1px solid #e4e4e4; +} + +div.square { + border: 1px solid #999; + float: left; + margin: .3em .4em 0 .4em; + overflow: hidden; + width: .6em; height: .6em; +} +.contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;} +.contextual input {font-size:0.9em;} + +.splitcontentleft{float:left; width:49%;} +.splitcontentright{float:right; width:49%;} +form {display: inline;} +input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;} +fieldset {border: 1px solid #e4e4e4; margin:0;} +legend {color: #484848;} +hr { width: 100%; height: 1px; background: #ccc; border: 0;} +blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;} +blockquote blockquote { margin-left: 0;} +textarea.wiki-edit { width: 99%; } +li p {margin-top: 0;} +div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;} +p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;} +p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; } +p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; } + +fieldset#filters, fieldset#date-range { padding: 0.7em; margin-bottom: 8px; } +fieldset#filters p { margin: 1.2em 0 0.8em 2px; } +fieldset#filters table { border-collapse: collapse; } +fieldset#filters table td { padding: 0; vertical-align: middle; } +fieldset#filters tr.filter { height: 2em; } +fieldset#filters td.add-filter { text-align: right; vertical-align: top; } +.buttons { font-size: 0.9em; } + +div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;} +div#issue-changesets .changeset { padding: 4px;} +div#issue-changesets .changeset { border-bottom: 1px solid #ddd; } +div#issue-changesets p { margin-top: 0; margin-bottom: 1em;} + +div#activity dl, #search-results { margin-left: 2em; } +div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; } +div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } +div#activity dt.me .time { border-bottom: 1px solid #999; } +div#activity dt .time { color: #777; font-size: 80%; } +div#activity dd .description, #search-results dd .description { font-style: italic; } +div#activity span.project:after, #search-results span.project:after { content: " -"; } +div#activity dd span.description, #search-results dd span.description { display:block; } + +#search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; } +div#search-results-counts {float:right;} +div#search-results-counts ul { margin-top: 0.5em; } +div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; } + +dt.issue { background-image: url(../images/ticket.png); } +dt.issue-edit { background-image: url(../images/ticket_edit.png); } +dt.issue-closed { background-image: url(../images/ticket_checked.png); } +dt.issue-note { background-image: url(../images/ticket_note.png); } +dt.changeset { background-image: url(../images/changeset.png); } +dt.news { background-image: url(../images/news.png); } +dt.message { background-image: url(../images/message.png); } +dt.reply { background-image: url(../images/comments.png); } +dt.wiki-page { background-image: url(../images/wiki_edit.png); } +dt.attachment { background-image: url(../images/attachment.png); } +dt.document { background-image: url(../images/document.png); } +dt.project { background-image: url(../images/projects.png); } + +div#roadmap fieldset.related-issues { margin-bottom: 1em; } +div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; } +div#roadmap .wiki h1:first-child { display: none; } +div#roadmap .wiki h1 { font-size: 120%; } +div#roadmap .wiki h2 { font-size: 110%; } + +div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; } +div#version-summary fieldset { margin-bottom: 1em; } +div#version-summary .total-hours { text-align: right; } + +table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; } +table#time-report tbody tr { font-style: italic; color: #777; } +table#time-report tbody tr.last-level { font-style: normal; color: #555; } +table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; } +table#time-report .hours-dec { font-size: 0.9em; } + +ul.properties {padding:0; font-size: 0.9em; color: #777;} +ul.properties li {list-style-type:none;} +ul.properties li span {font-style:italic;} + +.total-hours { font-size: 110%; font-weight: bold; } +.total-hours span.hours-int { font-size: 120%; } + +.autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;} +#user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; } + +.pagination {font-size: 90%} +p.pagination {margin-top:8px;} + +/***** Tabular forms ******/ +.tabular p{ +margin: 0; +padding: 5px 0 8px 0; +padding-left: 180px; /*width of left column containing the label elements*/ +height: 1%; +clear:left; +} + +html>body .tabular p {overflow:hidden;} + +.tabular label{ +font-weight: bold; +float: left; +text-align: right; +margin-left: -180px; /*width of left column*/ +width: 175px; /*width of labels. Should be smaller than left column to create some right +margin*/ +} + +.tabular label.floating{ +font-weight: normal; +margin-left: 0px; +text-align: left; +width: 270px; +} + +input#time_entry_comments { width: 90%;} + +#preview fieldset {margin-top: 1em; background: url(../images/draft.png)} + +.tabular.settings p{ padding-left: 300px; } +.tabular.settings label{ margin-left: -300px; width: 295px; } + +.required {color: #bb0000;} +.summary {font-style: italic;} + +#attachments_fields input[type=text] {margin-left: 8px; } + +div.attachments { margin-top: 12px; } +div.attachments p { margin:4px 0 2px 0; } +div.attachments img { vertical-align: middle; } +div.attachments span.author { font-size: 0.9em; color: #888; } + +p.other-formats { text-align: right; font-size:0.9em; color: #666; } +.other-formats span + span:before { content: "| "; } + +a.feed { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; } + +/***** Flash & error messages ****/ +#errorExplanation, div.flash, .nodata, .warning { + padding: 4px 4px 4px 30px; + margin-bottom: 12px; + font-size: 1.1em; + border: 2px solid; +} + +div.flash {margin-top: 8px;} + +div.flash.error, #errorExplanation { + background: url(../images/false.png) 8px 5px no-repeat; + background-color: #ffe3e3; + border-color: #dd0000; + color: #550000; +} + +div.flash.notice { + background: url(../images/true.png) 8px 5px no-repeat; + background-color: #dfffdf; + border-color: #9fcf9f; + color: #005f00; +} + +div.flash.warning { + background: url(../images/warning.png) 8px 5px no-repeat; + background-color: #FFEBC1; + border-color: #FDBF3B; + color: #A6750C; + text-align: left; +} + +.nodata, .warning { + text-align: center; + background-color: #FFEBC1; + border-color: #FDBF3B; + color: #A6750C; +} + +#errorExplanation ul { font-size: 0.9em;} + +/***** Ajax indicator ******/ +#ajax-indicator { +position: absolute; /* fixed not supported by IE */ +background-color:#eee; +border: 1px solid #bbb; +top:35%; +left:40%; +width:20%; +font-weight:bold; +text-align:center; +padding:0.6em; +z-index:100; +filter:alpha(opacity=50); +opacity: 0.5; +} + +html>body #ajax-indicator { position: fixed; } + +#ajax-indicator span { +background-position: 0% 40%; +background-repeat: no-repeat; +background-image: url(../images/loading.gif); +padding-left: 26px; +vertical-align: bottom; +} + +/***** Calendar *****/ +table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;} +table.cal thead th {width: 14%;} +table.cal tbody tr {height: 100px;} +table.cal th { background-color:#EEEEEE; padding: 4px; } +table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;} +table.cal td p.day-num {font-size: 1.1em; text-align:right;} +table.cal td.odd p.day-num {color: #bbb;} +table.cal td.today {background:#ffffdd;} +table.cal td.today p.day-num {font-weight: bold;} + +/***** Tooltips ******/ +.tooltip{position:relative;z-index:24;} +.tooltip:hover{z-index:25;color:#000;} +.tooltip span.tip{display: none; text-align:left;} + +div.tooltip:hover span.tip{ +display:block; +position:absolute; +top:12px; left:24px; width:270px; +border:1px solid #555; +background-color:#fff; +padding: 4px; +font-size: 0.8em; +color:#505050; +} + +/***** Progress bar *****/ +table.progress { + border: 1px solid #D7D7D7; + border-collapse: collapse; + border-spacing: 0pt; + empty-cells: show; + text-align: center; + float:left; + margin: 1px 6px 1px 0px; +} + +table.progress td { height: 0.9em; } +table.progress td.closed { background: #BAE0BA none repeat scroll 0%; } +table.progress td.done { background: #DEF0DE none repeat scroll 0%; } +table.progress td.open { background: #FFF none repeat scroll 0%; } +p.pourcent {font-size: 80%;} +p.progress-info {clear: left; font-style: italic; font-size: 80%;} + +/***** Tabs *****/ +#content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;} +#content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;} +#content .tabs>ul { bottom:-1px; } /* others */ +#content .tabs ul li { +float:left; +list-style-type:none; +white-space:nowrap; +margin-right:8px; +background:#fff; +} +#content .tabs ul li a{ +display:block; +font-size: 0.9em; +text-decoration:none; +line-height:1.3em; +padding:4px 6px 4px 6px; +border: 1px solid #ccc; +border-bottom: 1px solid #bbbbbb; +background-color: #eeeeee; +color:#777; +font-weight:bold; +} + +#content .tabs ul li a:hover { +background-color: #ffffdd; +text-decoration:none; +} + +#content .tabs ul li a.selected { +background-color: #fff; +border: 1px solid #bbbbbb; +border-bottom: 1px solid #fff; +} + +#content .tabs ul li a.selected:hover { +background-color: #fff; +} + +/***** Diff *****/ +.diff_out { background: #fcc; } +.diff_in { background: #cfc; } + +/***** Wiki *****/ +div.wiki table { + border: 1px solid #505050; + border-collapse: collapse; + margin-bottom: 1em; +} + +div.wiki table, div.wiki td, div.wiki th { + border: 1px solid #bbb; + padding: 4px; +} + +div.wiki .external { + background-position: 0% 60%; + background-repeat: no-repeat; + padding-left: 12px; + background-image: url(../images/external.png); +} + +div.wiki a.new { + color: #b73535; +} + +div.wiki pre { + margin: 1em 1em 1em 1.6em; + padding: 2px; + background-color: #fafafa; + border: 1px solid #dadada; + width:95%; + overflow-x: auto; +} + +div.wiki ul.toc { + background-color: #ffffdd; + border: 1px solid #e4e4e4; + padding: 4px; + line-height: 1.2em; + margin-bottom: 12px; + margin-right: 12px; + margin-left: 0; + display: table +} +* html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */ + +div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; } +div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; } +div.wiki ul.toc li { list-style-type:none;} +div.wiki ul.toc li.heading2 { margin-left: 6px; } +div.wiki ul.toc li.heading3 { margin-left: 12px; font-size: 0.8em; } + +div.wiki ul.toc a { + font-size: 0.9em; + font-weight: normal; + text-decoration: none; + color: #606060; +} +div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;} + +a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; } +a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; } +h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; } + +/***** My page layout *****/ +.block-receiver { +border:1px dashed #c0c0c0; +margin-bottom: 20px; +padding: 15px 0 15px 0; +} + +.mypage-box { +margin:0 0 20px 0; +color:#505050; +line-height:1.5em; +} + +.handle { +cursor: move; +} + +a.close-icon { +display:block; +margin-top:3px; +overflow:hidden; +width:12px; +height:12px; +background-repeat: no-repeat; +cursor:pointer; +background-image:url('../images/close.png'); +} + +a.close-icon:hover { +background-image:url('../images/close_hl.png'); +} + +/***** Gantt chart *****/ +.gantt_hdr { + position:absolute; + top:0; + height:16px; + border-top: 1px solid #c0c0c0; + border-bottom: 1px solid #c0c0c0; + border-right: 1px solid #c0c0c0; + text-align: center; + overflow: hidden; +} + +.task { + position: absolute; + height:8px; + font-size:0.8em; + color:#888; + padding:0; + margin:0; + line-height:0.8em; +} + +.task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; } +.task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; } +.task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; } +.milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; } + +/***** Icons *****/ +.icon { +background-position: 0% 40%; +background-repeat: no-repeat; +padding-left: 20px; +padding-top: 2px; +padding-bottom: 3px; +} + +.icon22 { +background-position: 0% 40%; +background-repeat: no-repeat; +padding-left: 26px; +line-height: 22px; +vertical-align: middle; +} + +.icon-add { background-image: url(../images/add.png); } +.icon-edit { background-image: url(../images/edit.png); } +.icon-copy { background-image: url(../images/copy.png); } +.icon-del { background-image: url(../images/delete.png); } +.icon-move { background-image: url(../images/move.png); } +.icon-save { background-image: url(../images/save.png); } +.icon-cancel { background-image: url(../images/cancel.png); } +.icon-file { background-image: url(../images/file.png); } +.icon-folder { background-image: url(../images/folder.png); } +.open .icon-folder { background-image: url(../images/folder_open.png); } +.icon-package { background-image: url(../images/package.png); } +.icon-home { background-image: url(../images/home.png); } +.icon-user { background-image: url(../images/user.png); } +.icon-mypage { background-image: url(../images/user_page.png); } +.icon-admin { background-image: url(../images/admin.png); } +.icon-projects { background-image: url(../images/projects.png); } +.icon-help { background-image: url(../images/help.png); } +.icon-attachment { background-image: url(../images/attachment.png); } +.icon-index { background-image: url(../images/index.png); } +.icon-history { background-image: url(../images/history.png); } +.icon-time { background-image: url(../images/time.png); } +.icon-stats { background-image: url(../images/stats.png); } +.icon-warning { background-image: url(../images/warning.png); } +.icon-fav { background-image: url(../images/fav.png); } +.icon-fav-off { background-image: url(../images/fav_off.png); } +.icon-reload { background-image: url(../images/reload.png); } +.icon-lock { background-image: url(../images/locked.png); } +.icon-unlock { background-image: url(../images/unlock.png); } +.icon-checked { background-image: url(../images/true.png); } +.icon-details { background-image: url(../images/zoom_in.png); } +.icon-report { background-image: url(../images/report.png); } +.icon-comment { background-image: url(../images/comment.png); } + +.icon22-projects { background-image: url(../images/22x22/projects.png); } +.icon22-users { background-image: url(../images/22x22/users.png); } +.icon22-tracker { background-image: url(../images/22x22/tracker.png); } +.icon22-role { background-image: url(../images/22x22/role.png); } +.icon22-workflow { background-image: url(../images/22x22/workflow.png); } +.icon22-options { background-image: url(../images/22x22/options.png); } +.icon22-notifications { background-image: url(../images/22x22/notifications.png); } +.icon22-authent { background-image: url(../images/22x22/authent.png); } +.icon22-info { background-image: url(../images/22x22/info.png); } +.icon22-comment { background-image: url(../images/22x22/comment.png); } +.icon22-package { background-image: url(../images/22x22/package.png); } +.icon22-settings { background-image: url(../images/22x22/settings.png); } +.icon22-plugin { background-image: url(../images/22x22/plugin.png); } + +img.gravatar { + padding: 2px; + border: solid 1px #d5d5d5; + background: #fff; +} + +div.issue img.gravatar { + float: right; + margin: 0 0 0 1em; + padding: 5px; +} + +div.issue table img.gravatar { + height: 14px; + width: 14px; + padding: 2px; + float: left; + margin: 0 0.5em 0 0; +} + +#history img.gravatar { + padding: 3px; + margin: 0 1.5em 1em 0; + float: left; +} + +td.username img.gravatar { + float: left; + margin: 0 1em 0 0; +} + +#activity dt img.gravatar { + float: left; + margin: 0 1em 1em 0; +} + +#activity dt, +.journal { + clear: left; +} + +h2 img { vertical-align:middle; } + + +/***** Media print specific styles *****/ +@media print { + #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; } + #main { background: #fff; } + #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;} +} diff --git a/app/webroot/css/cake.generic.css b/app/webroot/css/cake.generic.css new file mode 100755 index 0000000..db12fbd --- /dev/null +++ b/app/webroot/css/cake.generic.css @@ -0,0 +1,479 @@ +/* SVN FILE: $Id: cake.generic.css 7945 2008-12-19 02:16:01Z gwoo $ */ +/** + * + * PHP versions 4 and 5 + * + * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org) + * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project + * @package cake + * @subpackage cake.app.webroot.css + * @since CakePHP(tm) + * @version $Revision: 7945 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2008-12-18 18:16:01 -0800 (Thu, 18 Dec 2008) $ + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ + +* { + margin:0; + padding:0; +} + +/* General Style Info */ +body { + background: #003d4c; + color: #fff; + font-family:'lucida grande',verdana,helvetica,arial,sans-serif; + font-size:90%; + margin: 0; +} +a { + background:#fff; + color: #003d4c; + text-decoration: underline; + font-weight: bold; +} +a:hover { + background:#fff; + color: #003d4c; + text-decoration:none; +} +a img { + border:none; +} +h1, h2, h3, h4 { + font-weight: normal; +} +h1 { + background:#fff; + color: #003d4c; + font-size: 100%; + margin: 0.1em 0; +} +h2 { + background:#fff; + color: #e32; + font-family:'Gill Sans','lucida grande',helvetica, arial, sans-serif; + font-size: 190%; + margin: 0.3em 0; + padding-top: 0.8em; +} +h3 { + color: #993; + font-family:'Gill Sans','lucida grande',helvetica, arial, sans-serif; + font-size: 165%; + padding-top: 1.5em; +} +h4 { + color: #993; + font-weight: normal; + padding-top: 0.5em; +} +ul, li { + margin: 0 12px; +} + +/* Layout */ +#container { + text-align: left; +} + +#header{ + padding: 10px 20px; +} +#header h1 { + background: #003d4c url('../img/cake.icon.gif') no-repeat left; + color: #fff; + padding: 0px 30px; +} +#header h1 a { + color: #fff; + background: #003d4c; + font-weight: normal; + text-decoration: none; +} +#header h1 a:hover { + color: #fff; + background: #003d4c; + text-decoration: underline; +} +#content{ + background: #fff; + clear: both; + color: #333; + padding: 10px 20px 40px 20px; + overflow: auto; +} +#footer { + clear: both; + padding: 6px 10px; + text-align: right; +} + +/* Tables */ +table { + background: #fff; + border:1px solid #ccc; + border-right:0; + clear: both; + color: #333; + margin-bottom: 10px; + width: 100%; +} +th { + background: #f2f2f2; + border:1px solid #bbb; + border-top: 1px solid #fff; + border-left: 1px solid #fff; + text-align: center; +} +th a { + background:#f2f2f2; + display: block; + padding: 2px 4px; + text-decoration: none; +} +th a:hover { + background: #ccc; + color: #333; + text-decoration: none; +} +table tr td { + background: #fff; + border-right: 1px solid #ccc; + padding: 4px; + text-align: center; + vertical-align: top; +} +table tr.altrow td { + background: #f4f4f4; +} +td.actions { + text-align: center; + white-space: nowrap; +} +td.actions a { + margin: 0px 6px; +} +.cake-sql-log table { + background: #f4f4f4; +} +.cake-sql-log td { + padding: 4px 8px; + text-align: left; +} + +/* Paging */ +div.paging { + background:#fff; + color: #ccc; + margin-bottom: 2em; +} +div.paging div.disabled { + color: #ddd; + display: inline; +} +div.paging span { +} +div.paging span.current { + color: #000; +} +div.paging span a { +} + +/* Scaffold View */ +dl { + line-height: 2em; + margin: 0em 0em; + width: 60%; +} +dl.altrow { + background: #f4f4f4; +} +dt { + font-weight: bold; + padding-left: 4px; + vertical-align: top; +} +dd { + margin-left: 10em; + margin-top: -2em; + vertical-align: top; +} + +/* Forms */ +form { + clear: both; + margin-right: 20px; + padding: 0; + width: 80%; +} +fieldset { + border: 1px solid #ccc; + margin-top: 30px; + padding: 16px 20px; +} +fieldset legend { + background:#fff; + color: #e32; + font-size: 160%; + font-weight: bold; +} +fieldset fieldset { + margin-top: 0px; + margin-bottom: 20px; + padding: 16px 10px; +} +fieldset fieldset legend { + font-size: 120%; + font-weight: normal; +} +fieldset fieldset div { + clear: left; + margin: 0 20px; +} +form div { + clear: both; + margin-bottom: 1em; + padding: .5em; + vertical-align: text-top; +} +form div.input { + color: #444; +} +form div.required { + color: #333; + font-weight: bold; +} +form div.submit { + border: 0; + clear: both; + margin-top: 10px; + margin-left: 140px; +} +label { + display: block; + font-size: 110%; + padding-right: 20px; +} +input, textarea { + clear: both; + font-size: 140%; + font-family: "frutiger linotype", "lucida grande", "verdana", sans-serif; + padding: 2px; + width: 100%; +} +select { + clear: both; + font-size: 120%; + vertical-align: text-bottom; +} +select[multiple=multiple] { + width: 100%; +} +option { + font-size: 120%; + padding: 0 3px; +} +input[type=checkbox] { + clear: left; + float: left; + margin: 0px 6px 7px 2px; + width: auto; +} +input[type=radio] { + float:left; + width:auto; + margin: 0 3px 7px 0; +} +div.radio label { + margin: 0 0 6px 20px; +} +input[type=submit] { + display: inline; + font-size: 110%; + padding: 2px 5px; + width: auto; + vertical-align: bottom; +} + +/* Notices and Errors */ +div.message { + clear: both; + color: #900; + font-size: 140%; + font-weight: bold; + margin: 1em 0; +} +div.error-message { + clear: both; + color: #900; + font-weight: bold; +} +p.error { + background-color: #e32; + color: #fff; + font-family: Courier, monospace; + font-size: 120%; + line-height: 140%; + padding: 0.8em; + margin: 1em 0; +} +p.error em { + color: #000; + font-weight: normal; + line-height: 140%; +} +.notice { + background: #ffcc00; + color: #000; + display: block; + font-family: Courier, monospace; + font-size: 120%; + line-height: 140%; + padding: 0.8em; + margin: 1em 0; +} +.success { + background: green; + color: #fff; +} + +/* Actions */ +div.actions ul { + margin: 0px 0; + padding: 0; +} +div.actions li { + display: inline; + list-style-type: none; + line-height: 2em; + margin: 0 2em 0 0; + white-space: nowrap; +} +div.actions ul li a { + background:#fff; + color: #003d4c; + text-decoration: none; +} +div.actions ul li a:hover { + color: #333; + text-decoration: underline; +} + +/* Related */ +div.related { + clear: both; + display: block; +} + +/* Debugging */ +pre { + color: #000; + background: #f0f0f0; + padding: 1em; +} +pre.cake-debug { + background: #ffcc00; + font-size: 120%; + line-height: 140%; + margin-top: 1em; + overflow: auto; + position: relative; +} +div.cake-stack-trace { + background: #fff; + border: 4px dotted #ffcc00; + color: #333; + margin: 0px; + padding: 6px; + font-size: 120%; + line-height: 140%; + overflow: auto; + position: relative; +} +div.cake-code-dump pre { + position: relative; + overflow: auto; +} +div.cake-stack-trace pre, div.cake-code-dump pre { + color: #000; + background-color: #F0F0F0; + margin: 0px; + padding: 1em; + overflow: auto; +} +div.cake-code-dump pre, div.cake-code-dump pre code { + clear: both; + font-size: 12px; + line-height: 15px; + margin: 4px 2px; + padding: 4px; + overflow: auto; +} +div.cake-code-dump span.code-highlight { + background-color: #ff0; + padding: 4px; +} +div.code-coverage-results div.code-line { + padding-left:5px; + display:block; + margin-left:10px; +} +div.code-coverage-results div.uncovered span.content { + background:#ecc; +} +div.code-coverage-results div.covered span.content { + background:#cec; +} +div.code-coverage-results div.ignored span.content { + color:#aaa; +} +div.code-coverage-results span.line-num { + color:#666; + display:block; + float:left; + width:20px; + text-align:right; + margin-right:5px; +} +div.code-coverage-results span.line-num strong { + color:#666; +} +div.code-coverage-results div.start { + border:1px solid #aaa; + border-width:1px 1px 0px 1px; + margin-top:30px; + padding-top:5px; +} +div.code-coverage-results div.end { + border:1px solid #aaa; + border-width:0px 1px 1px 1px; + margin-bottom:30px; + padding-bottom:5px; +} +div.code-coverage-results div.realstart { + margin-top:0px; +} +div.code-coverage-results p.note { + color:#bbb; + padding:5px; + margin:5px 0 10px; + font-size:10px; +} +div.code-coverage-results span.result-bad { + color: #a00; +} +div.code-coverage-results span.result-ok { + color: #fa0; +} +div.code-coverage-results span.result-good { + color: #0a0; +} \ No newline at end of file diff --git a/app/webroot/css/calendar.css b/app/webroot/css/calendar.css new file mode 100644 index 0000000..c8d2dd6 --- /dev/null +++ b/app/webroot/css/calendar.css @@ -0,0 +1,237 @@ +/* The main calendar widget. DIV containing a table. */ + +img.calendar-trigger { + cursor: pointer; + vertical-align: middle; + margin-left: 4px; +} + +div.calendar { position: relative; z-index: 30;} + +.calendar, .calendar table { + border: 1px solid #556; + font-size: 11px; + color: #000; + cursor: default; + background: #fafbfc; + font-family: tahoma,verdana,sans-serif; +} + +/* Header part -- contains navigation buttons and day names. */ + +.calendar .button { /* "<<", "<", ">", ">>" buttons have this class */ + text-align: center; /* They are the navigation buttons */ + padding: 2px; /* Make the buttons seem like they're pressing */ +} + +.calendar .nav { + background: #467aa7; +} + +.calendar thead .title { /* This holds the current "month, year" */ + font-weight: bold; /* Pressing it will take you to the current date */ + text-align: center; + background: #fff; + color: #000; + padding: 2px; +} + +.calendar thead .headrow { /* Row <TR> containing navigation buttons */ + background: #467aa7; + color: #fff; +} + +.calendar thead .daynames { /* Row <TR> containing the day names */ + background: #bdf; +} + +.calendar thead .name { /* Cells <TD> containing the day names */ + border-bottom: 1px solid #556; + padding: 2px; + text-align: center; + color: #000; +} + +.calendar thead .weekend { /* How a weekend day name shows in header */ + color: #a66; +} + +.calendar thead .hilite { /* How do the buttons in header appear when hover */ + background-color: #80b0da; + color: #000; + padding: 1px; +} + +.calendar thead .active { /* Active (pressed) buttons in header */ + background-color: #77c; + padding: 2px 0px 0px 2px; +} + +/* The body part -- contains all the days in month. */ + +.calendar tbody .day { /* Cells <TD> containing month days dates */ + width: 2em; + color: #456; + text-align: right; + padding: 2px 4px 2px 2px; +} +.calendar tbody .day.othermonth { + font-size: 80%; + color: #bbb; +} +.calendar tbody .day.othermonth.oweekend { + color: #fbb; +} + +.calendar table .wn { + padding: 2px 3px 2px 2px; + border-right: 1px solid #000; + background: #bdf; +} + +.calendar tbody .rowhilite td { + background: #def; +} + +.calendar tbody .rowhilite td.wn { + background: #80b0da; +} + +.calendar tbody td.hilite { /* Hovered cells <TD> */ + background: #80b0da; + padding: 1px 3px 1px 1px; + border: 1px solid #bbb; +} + +.calendar tbody td.active { /* Active (pressed) cells <TD> */ + background: #cde; + padding: 2px 2px 0px 2px; +} + +.calendar tbody td.selected { /* Cell showing today date */ + font-weight: bold; + border: 1px solid #000; + padding: 1px 3px 1px 1px; + background: #fff; + color: #000; +} + +.calendar tbody td.weekend { /* Cells showing weekend days */ + color: #a66; +} + +.calendar tbody td.today { /* Cell showing selected date */ + font-weight: bold; + color: #f00; +} + +.calendar tbody .disabled { color: #999; } + +.calendar tbody .emptycell { /* Empty cells (the best is to hide them) */ + visibility: hidden; +} + +.calendar tbody .emptyrow { /* Empty row (some months need less than 6 rows) */ + display: none; +} + +/* The footer part -- status bar and "Close" button */ + +.calendar tfoot .footrow { /* The <TR> in footer (only one right now) */ + text-align: center; + background: #556; + color: #fff; +} + +.calendar tfoot .ttip { /* Tooltip (status bar) cell <TD> */ + background: #fff; + color: #445; + border-top: 1px solid #556; + padding: 1px; +} + +.calendar tfoot .hilite { /* Hover style for buttons in footer */ + background: #aaf; + border: 1px solid #04f; + color: #000; + padding: 1px; +} + +.calendar tfoot .active { /* Active (pressed) style for buttons in footer */ + background: #77c; + padding: 2px 0px 0px 2px; +} + +/* Combo boxes (menus that display months/years for direct selection) */ + +.calendar .combo { + position: absolute; + display: none; + top: 0px; + left: 0px; + width: 4em; + cursor: default; + border: 1px solid #655; + background: #def; + color: #000; + font-size: 90%; + z-index: 100; +} + +.calendar .combo .label, +.calendar .combo .label-IEfix { + text-align: center; + padding: 1px; +} + +.calendar .combo .label-IEfix { + width: 4em; +} + +.calendar .combo .hilite { + background: #acf; +} + +.calendar .combo .active { + border-top: 1px solid #46a; + border-bottom: 1px solid #46a; + background: #eef; + font-weight: bold; +} + +.calendar td.time { + border-top: 1px solid #000; + padding: 1px 0px; + text-align: center; + background-color: #f4f0e8; +} + +.calendar td.time .hour, +.calendar td.time .minute, +.calendar td.time .ampm { + padding: 0px 3px 0px 4px; + border: 1px solid #889; + font-weight: bold; + background-color: #fff; +} + +.calendar td.time .ampm { + text-align: center; +} + +.calendar td.time .colon { + padding: 0px 2px 0px 3px; + font-weight: bold; +} + +.calendar td.time span.hilite { + border-color: #000; + background-color: #667; + color: #fff; +} + +.calendar td.time span.active { + border-color: #f00; + background-color: #000; + color: #0f0; +} diff --git a/app/webroot/css/context_menu.css b/app/webroot/css/context_menu.css new file mode 100644 index 0000000..b3aa1ac --- /dev/null +++ b/app/webroot/css/context_menu.css @@ -0,0 +1,52 @@ +#context-menu { position: absolute; z-index: 40; font-size: 0.9em;} + +#context-menu ul, #context-menu li, #context-menu a { + display:block; + margin:0; + padding:0; + border:0; +} + +#context-menu ul { + width:150px; + border-top:1px solid #ddd; + border-left:1px solid #ddd; + border-bottom:1px solid #777; + border-right:1px solid #777; + background:white; + list-style:none; +} + +#context-menu li { + position:relative; + padding:1px; + z-index:39; +} +#context-menu li.folder ul { position:absolute; left:168px; /* IE6 */ top:-2px; } +#context-menu li.folder>ul { left:148px; } + +#context-menu.reverse-y li.folder>ul { top:auto; bottom:0; } +#context-menu.reverse-x li.folder ul { left:auto; right:168px; /* IE6 */ } +#context-menu.reverse-x li.folder>ul { right:148px; } + +#context-menu a { + border:1px solid white; + text-decoration:none !important; + background-repeat: no-repeat; + background-position: 1px 50%; + padding: 1px 0px 1px 20px; + width:100%; /* IE */ +} +#context-menu li>a { width:auto; } /* others */ +#context-menu a.disabled, #context-menu a.disabled:hover {color: #ccc;} +#context-menu li a.submenu { background:url("../images/sub.gif") right no-repeat; } +#context-menu a:hover { border-color:gray; background-color:#eee; color:#2A5685; } +#context-menu li.folder a:hover { background-color:#eee; } +#context-menu li.folder:hover { z-index:40; } +#context-menu ul ul, #context-menu li:hover ul ul { display:none; } +#context-menu li:hover ul, #context-menu li:hover li:hover ul { display:block; } + +/* selected element */ +.context-menu-selection { background-color:#507AAA !important; color:#f8f8f8 !important; } +.context-menu-selection a, .context-menu-selection a:hover { color:#f8f8f8 !important; } +.context-menu-selection:hover { background-color:#507AAA !important; color:#f8f8f8 !important; } diff --git a/app/webroot/css/csshover.htc b/app/webroot/css/csshover.htc new file mode 100644 index 0000000..5b58bf0 --- /dev/null +++ b/app/webroot/css/csshover.htc @@ -0,0 +1,122 @@ +<attach event="ondocumentready" handler="parseStylesheets" /> +<script> +/** + * Whatever:hover - V1.42.060206 - hover & active + * ------------------------------------------------------------ + * (c) 2005 - Peter Nederlof + * Peterned - http://www.xs4all.nl/~peterned/ + * License - http://creativecommons.org/licenses/LGPL/2.1/ + * + * Whatever:hover is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * Whatever:hover 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 + * Lesser General Public License for more details. + * + * Credits and thanks to: + * Arnoud Berendsen, Martin Reurings, Robert Hanson + * + * howto: body { behavior:url("csshover.htc"); } + * ------------------------------------------------------------ + */ + +var csshoverReg = /(^|\s)(([^a]([^ ]+)?)|(a([^#.][^ ]+)+)):(hover|active)/i, +currentSheet, doc = window.document, hoverEvents = [], activators = { + onhover:{on:'onmouseover', off:'onmouseout'}, + onactive:{on:'onmousedown', off:'onmouseup'} +} + +function parseStylesheets() { + if(!/MSIE (5|6)/.test(navigator.userAgent)) return; + window.attachEvent('onunload', unhookHoverEvents); + var sheets = doc.styleSheets, l = sheets.length; + for(var i=0; i<l; i++) + parseStylesheet(sheets[i]); +} + function parseStylesheet(sheet) { + if(sheet.imports) { + try { + var imports = sheet.imports, l = imports.length; + for(var i=0; i<l; i++) parseStylesheet(sheet.imports[i]); + } catch(securityException){} + } + + try { + var rules = (currentSheet = sheet).rules, l = rules.length; + for(var j=0; j<l; j++) parseCSSRule(rules[j]); + } catch(securityException){} + } + + function parseCSSRule(rule) { + var select = rule.selectorText, style = rule.style.cssText; + if(!csshoverReg.test(select) || !style) return; + + var pseudo = select.replace(/[^:]+:([a-z-]+).*/i, 'on$1'); + var newSelect = select.replace(/(\.([a-z0-9_-]+):[a-z]+)|(:[a-z]+)/gi, '.$2' + pseudo); + var className = (/\.([a-z0-9_-]*on(hover|active))/i).exec(newSelect)[1]; + var affected = select.replace(/:(hover|active).*$/, ''); + var elements = getElementsBySelect(affected); + if(elements.length == 0) return; + + currentSheet.addRule(newSelect, style); + for(var i=0; i<elements.length; i++) + new HoverElement(elements[i], className, activators[pseudo]); + } + +function HoverElement(node, className, events) { + if(!node.hovers) node.hovers = {}; + if(node.hovers[className]) return; + node.hovers[className] = true; + hookHoverEvent(node, events.on, function() { node.className += ' ' + className; }); + hookHoverEvent(node, events.off, function() { node.className = node.className.replace(new RegExp('\\s+'+className, 'g'),''); }); +} + function hookHoverEvent(node, type, handler) { + node.attachEvent(type, handler); + hoverEvents[hoverEvents.length] = { + node:node, type:type, handler:handler + }; + } + + function unhookHoverEvents() { + for(var e,i=0; i<hoverEvents.length; i++) { + e = hoverEvents[i]; + e.node.detachEvent(e.type, e.handler); + } + } + +function getElementsBySelect(rule) { + var parts, nodes = [doc]; + parts = rule.split(' '); + for(var i=0; i<parts.length; i++) { + nodes = getSelectedNodes(parts[i], nodes); + } return nodes; +} + function getSelectedNodes(select, elements) { + var result, node, nodes = []; + var identify = (/\#([a-z0-9_-]+)/i).exec(select); + if(identify) { + var element = doc.getElementById(identify[1]); + return element? [element]:nodes; + } + + var classname = (/\.([a-z0-9_-]+)/i).exec(select); + var tagName = select.replace(/(\.|\#|\:)[a-z0-9_-]+/i, ''); + var classReg = classname? new RegExp('\\b' + classname[1] + '\\b'):false; + for(var i=0; i<elements.length; i++) { + result = tagName? elements[i].all.tags(tagName):elements[i].all; + for(var j=0; j<result.length; j++) { + node = result[j]; + if(classReg && !classReg.test(node.className)) continue; + nodes[nodes.length] = node; + } + } + + return nodes; + } + +window.parseStylesheets = parseStylesheets; +</script> \ No newline at end of file diff --git a/app/webroot/css/jstoolbar.css b/app/webroot/css/jstoolbar.css new file mode 100644 index 0000000..9514e3e --- /dev/null +++ b/app/webroot/css/jstoolbar.css @@ -0,0 +1,95 @@ +.jstEditor { + padding-left: 0px; +} +.jstEditor textarea, .jstEditor iframe { + margin: 0; +} + +.jstHandle { + height: 10px; + font-size: 0.1em; + cursor: s-resize; + /*background: transparent url(img/resizer.png) no-repeat 45% 50%;*/ +} + +.jstElements { + padding: 3px 3px; +} + +.jstElements button { + margin-right : 6px; + width : 24px; + height: 24px; + padding: 4px; + border-style: solid; + border-width: 1px; + border-color: #ddd; + background-color : #f7f7f7; + background-position : 50% 50%; + background-repeat: no-repeat; +} +.jstElements button:hover { + border-color : #000; +} +.jstElements button span { + display : none; +} +.jstElements span { + display : inline; +} + +.jstSpacer { + width : 0px; + font-size: 1px; + margin-right: 4px; +} + +.jstElements .help { float: right; margin-right: 1em; padding-top: 8px; font-size: 0.9em; } + +/* Buttons +-------------------------------------------------------- */ +.jstb_strong { + background-image: url(../images/jstoolbar/bt_strong.png); +} +.jstb_em { + background-image: url(../images/jstoolbar/bt_em.png); +} +.jstb_ins { + background-image: url(../images/jstoolbar/bt_ins.png); +} +.jstb_del { + background-image: url(../images/jstoolbar/bt_del.png); +} +.jstb_code { + background-image: url(../images/jstoolbar/bt_code.png); +} +.jstb_h1 { + background-image: url(../images/jstoolbar/bt_h1.png); +} +.jstb_h2 { + background-image: url(../images/jstoolbar/bt_h2.png); +} +.jstb_h3 { + background-image: url(../images/jstoolbar/bt_h3.png); +} +.jstb_ul { + background-image: url(../images/jstoolbar/bt_ul.png); +} +.jstb_ol { + background-image: url(../images/jstoolbar/bt_ol.png); +} +.jstb_bq { + background-image: url(../images/jstoolbar/bt_bq.png); +} +.jstb_unbq { + background-image: url(../images/jstoolbar/bt_bq_remove.png); +} +.jstb_pre { + background-image: url(../images/jstoolbar/bt_pre.png); +} +.jstb_link { + background-image: url(../images/jstoolbar/bt_link.png); +} +.jstb_img { + background-image: url(../images/jstoolbar/bt_img.png); +} diff --git a/app/webroot/css/scm.css b/app/webroot/css/scm.css new file mode 100644 index 0000000..ecd3193 --- /dev/null +++ b/app/webroot/css/scm.css @@ -0,0 +1,180 @@ + +div.changeset-changes ul { margin: 0; padding: 0; } +div.changeset-changes ul > ul { margin-left: 18px; padding: 0; } + +li.change { + list-style-type:none; + background-image: url(../images/bullet_black.png); + background-position: 1px 1px; + background-repeat: no-repeat; + padding-top: 1px; + padding-bottom: 1px; + padding-left: 20px; + margin: 0; +} +li.change.folder { background-image: url(../images/folder_open.png); } +li.change.folder.change-A { background-image: url(../images/folder_open_add.png); } +li.change.folder.change-M { background-image: url(../images/folder_open_orange.png); } +li.change.change-A { background-image: url(../images/bullet_add.png); } +li.change.change-M { background-image: url(../images/bullet_orange.png); } +li.change.change-C { background-image: url(../images/bullet_blue.png); } +li.change.change-R { background-image: url(../images/bullet_purple.png); } +li.change.change-D { background-image: url(../images/bullet_delete.png); } + +li.change .copied-from { font-style: italic; color: #999; font-size: 0.9em; } +li.change .copied-from:before { content: " - "} + +#changes-legend { float: right; font-size: 0.8em; margin: 0; } +#changes-legend li { float: left; background-position: 5px 0; } + +table.filecontent { border: 1px solid #ccc; border-collapse: collapse; width:98%; } +table.filecontent th { border: 1px solid #ccc; background-color: #eee; } +table.filecontent th.filename { background-color: #e4e4d4; text-align: left; padding: 0.2em;} +table.filecontent tr.spacing th { text-align:center; } +table.filecontent tr.spacing td { height: 0.4em; background: #EAF2F5;} +table.filecontent th.line-num { + border: 1px solid #d7d7d7; + font-size: 0.8em; + text-align: right; + width: 2%; + padding-right: 3px; + color: #999; +} +table.filecontent td.line-code pre { + white-space: pre-wrap; /* CSS2.1 compliant */ + white-space: -moz-pre-wrap; /* Mozilla-based browsers */ + white-space: -o-pre-wrap; /* Opera 7+ */ +} + +/* 12 different colors for the annonate view */ +table.annotate tr.bloc-0 {background: #FFFFBF;} +table.annotate tr.bloc-1 {background: #EABFFF;} +table.annotate tr.bloc-2 {background: #BFFFFF;} +table.annotate tr.bloc-3 {background: #FFD9BF;} +table.annotate tr.bloc-4 {background: #E6FFBF;} +table.annotate tr.bloc-5 {background: #BFCFFF;} +table.annotate tr.bloc-6 {background: #FFBFEF;} +table.annotate tr.bloc-7 {background: #FFE6BF;} +table.annotate tr.bloc-8 {background: #FFE680;} +table.annotate tr.bloc-9 {background: #AA80FF;} +table.annotate tr.bloc-10 {background: #FFBFDC;} +table.annotate tr.bloc-11 {background: #BFE4FF;} + +table.annotate td.revision { + text-align: center; + width: 2%; + padding-left: 1em; + background: inherit; +} + +table.annotate td.author { + text-align: center; + border-right: 1px solid #d7d7d7; + white-space: nowrap; + padding-left: 1em; + padding-right: 1em; + width: 3%; + background: inherit; + font-size: 90%; +} + +table.annotate td.line-code { background-color: #fafafa; } + +div.action_M { background: #fd8 } +div.action_D { background: #f88 } +div.action_A { background: #bfb } + +/************* Coderay styles *************/ + +table.CodeRay { + background-color: #fafafa; +} +.CodeRay pre { margin: 0px } + +span.CodeRay { white-space: pre; border: 0px; padding: 2px } + +.CodeRay .no { padding: 0px 4px } +.CodeRay .code { } + +ol.CodeRay { font-size: 10pt } +ol.CodeRay li { white-space: pre } + +.CodeRay .code pre { overflow: auto } + +.CodeRay .debug { color:white ! important; background:blue ! important; } + +.CodeRay .af { color:#00C } +.CodeRay .an { color:#007 } +.CodeRay .av { color:#700 } +.CodeRay .aw { color:#C00 } +.CodeRay .bi { color:#509; font-weight:bold } +.CodeRay .c { color:#666; } + +.CodeRay .ch { color:#04D } +.CodeRay .ch .k { color:#04D } +.CodeRay .ch .dl { color:#039 } + +.CodeRay .cl { color:#B06; font-weight:bold } +.CodeRay .co { color:#036; font-weight:bold } +.CodeRay .cr { color:#0A0 } +.CodeRay .cv { color:#369 } +.CodeRay .df { color:#099; font-weight:bold } +.CodeRay .di { color:#088; font-weight:bold } +.CodeRay .dl { color:black } +.CodeRay .do { color:#970 } +.CodeRay .ds { color:#D42; font-weight:bold } +.CodeRay .e { color:#666; font-weight:bold } +.CodeRay .en { color:#800; font-weight:bold } +.CodeRay .er { color:#F00; background-color:#FAA } +.CodeRay .ex { color:#F00; font-weight:bold } +.CodeRay .fl { color:#60E; font-weight:bold } +.CodeRay .fu { color:#06B; font-weight:bold } +.CodeRay .gv { color:#d70; font-weight:bold } +.CodeRay .hx { color:#058; font-weight:bold } +.CodeRay .i { color:#00D; font-weight:bold } +.CodeRay .ic { color:#B44; font-weight:bold } + +.CodeRay .il { background: #eee } +.CodeRay .il .il { background: #ddd } +.CodeRay .il .il .il { background: #ccc } +.CodeRay .il .idl { font-weight: bold; color: #888 } + +.CodeRay .in { color:#B2B; font-weight:bold } +.CodeRay .iv { color:#33B } +.CodeRay .la { color:#970; font-weight:bold } +.CodeRay .lv { color:#963 } +.CodeRay .oc { color:#40E; font-weight:bold } +.CodeRay .of { color:#000; font-weight:bold } +.CodeRay .op { } +.CodeRay .pc { color:#038; font-weight:bold } +.CodeRay .pd { color:#369; font-weight:bold } +.CodeRay .pp { color:#579 } +.CodeRay .pt { color:#339; font-weight:bold } +.CodeRay .r { color:#080; font-weight:bold } + +.CodeRay .rx { background-color:#fff0ff } +.CodeRay .rx .k { color:#808 } +.CodeRay .rx .dl { color:#404 } +.CodeRay .rx .mod { color:#C2C } +.CodeRay .rx .fu { color:#404; font-weight: bold } + +.CodeRay .s { background-color:#fff0f0 } +.CodeRay .s .s { background-color:#ffe0e0 } +.CodeRay .s .s .s { background-color:#ffd0d0 } +.CodeRay .s .k { color:#D20 } +.CodeRay .s .dl { color:#710 } + +.CodeRay .sh { background-color:#f0fff0 } +.CodeRay .sh .k { color:#2B2 } +.CodeRay .sh .dl { color:#161 } + +.CodeRay .sy { color:#A60 } +.CodeRay .sy .k { color:#A60 } +.CodeRay .sy .dl { color:#630 } + +.CodeRay .ta { color:#070 } +.CodeRay .tf { color:#070; font-weight:bold } +.CodeRay .ts { color:#D70; font-weight:bold } +.CodeRay .ty { color:#339; font-weight:bold } +.CodeRay .v { color:#036 } +.CodeRay .xt { color:#444 } diff --git a/app/webroot/favicon.ico b/app/webroot/favicon.ico new file mode 100755 index 0000000..b36e81f Binary files /dev/null and b/app/webroot/favicon.ico differ diff --git a/app/webroot/img/1downarrow.png b/app/webroot/img/1downarrow.png new file mode 100644 index 0000000..dd5b65d Binary files /dev/null and b/app/webroot/img/1downarrow.png differ diff --git a/app/webroot/img/1uparrow.png b/app/webroot/img/1uparrow.png new file mode 100644 index 0000000..cd514d1 Binary files /dev/null and b/app/webroot/img/1uparrow.png differ diff --git a/app/webroot/img/22x22/authent.png b/app/webroot/img/22x22/authent.png new file mode 100644 index 0000000..d2b2994 Binary files /dev/null and b/app/webroot/img/22x22/authent.png differ diff --git a/app/webroot/img/22x22/comment.png b/app/webroot/img/22x22/comment.png new file mode 100644 index 0000000..e2f4e70 Binary files /dev/null and b/app/webroot/img/22x22/comment.png differ diff --git a/app/webroot/img/22x22/file.png b/app/webroot/img/22x22/file.png new file mode 100644 index 0000000..96c56a2 Binary files /dev/null and b/app/webroot/img/22x22/file.png differ diff --git a/app/webroot/img/22x22/info.png b/app/webroot/img/22x22/info.png new file mode 100644 index 0000000..cf54e2c Binary files /dev/null and b/app/webroot/img/22x22/info.png differ diff --git a/app/webroot/img/22x22/notifications.png b/app/webroot/img/22x22/notifications.png new file mode 100644 index 0000000..972f4a2 Binary files /dev/null and b/app/webroot/img/22x22/notifications.png differ diff --git a/app/webroot/img/22x22/options.png b/app/webroot/img/22x22/options.png new file mode 100644 index 0000000..48da151 Binary files /dev/null and b/app/webroot/img/22x22/options.png differ diff --git a/app/webroot/img/22x22/package.png b/app/webroot/img/22x22/package.png new file mode 100644 index 0000000..f1a98dc Binary files /dev/null and b/app/webroot/img/22x22/package.png differ diff --git a/app/webroot/img/22x22/plugin.png b/app/webroot/img/22x22/plugin.png new file mode 100644 index 0000000..455fa6a Binary files /dev/null and b/app/webroot/img/22x22/plugin.png differ diff --git a/app/webroot/img/22x22/projects.png b/app/webroot/img/22x22/projects.png new file mode 100644 index 0000000..4f023be Binary files /dev/null and b/app/webroot/img/22x22/projects.png differ diff --git a/app/webroot/img/22x22/role.png b/app/webroot/img/22x22/role.png new file mode 100644 index 0000000..4de98ed Binary files /dev/null and b/app/webroot/img/22x22/role.png differ diff --git a/app/webroot/img/22x22/settings.png b/app/webroot/img/22x22/settings.png new file mode 100644 index 0000000..54a3b47 Binary files /dev/null and b/app/webroot/img/22x22/settings.png differ diff --git a/app/webroot/img/22x22/tracker.png b/app/webroot/img/22x22/tracker.png new file mode 100644 index 0000000..f513941 Binary files /dev/null and b/app/webroot/img/22x22/tracker.png differ diff --git a/app/webroot/img/22x22/users.png b/app/webroot/img/22x22/users.png new file mode 100644 index 0000000..92f3962 Binary files /dev/null and b/app/webroot/img/22x22/users.png differ diff --git a/app/webroot/img/22x22/workflow.png b/app/webroot/img/22x22/workflow.png new file mode 100644 index 0000000..9d1b9d8 Binary files /dev/null and b/app/webroot/img/22x22/workflow.png differ diff --git a/app/webroot/img/2downarrow.png b/app/webroot/img/2downarrow.png new file mode 100644 index 0000000..05880f3 Binary files /dev/null and b/app/webroot/img/2downarrow.png differ diff --git a/app/webroot/img/2uparrow.png b/app/webroot/img/2uparrow.png new file mode 100644 index 0000000..6a87aab Binary files /dev/null and b/app/webroot/img/2uparrow.png differ diff --git a/app/webroot/img/32x32/file.png b/app/webroot/img/32x32/file.png new file mode 100644 index 0000000..1662b53 Binary files /dev/null and b/app/webroot/img/32x32/file.png differ diff --git a/app/webroot/img/add.png b/app/webroot/img/add.png new file mode 100644 index 0000000..db59058 Binary files /dev/null and b/app/webroot/img/add.png differ diff --git a/app/webroot/img/admin.png b/app/webroot/img/admin.png new file mode 100644 index 0000000..c98330c Binary files /dev/null and b/app/webroot/img/admin.png differ diff --git a/app/webroot/img/arrow_bw.png b/app/webroot/img/arrow_bw.png new file mode 100644 index 0000000..2af9e2c Binary files /dev/null and b/app/webroot/img/arrow_bw.png differ diff --git a/app/webroot/img/arrow_down.png b/app/webroot/img/arrow_down.png new file mode 100644 index 0000000..ea37f3a Binary files /dev/null and b/app/webroot/img/arrow_down.png differ diff --git a/app/webroot/img/arrow_from.png b/app/webroot/img/arrow_from.png new file mode 100644 index 0000000..7d94ad1 Binary files /dev/null and b/app/webroot/img/arrow_from.png differ diff --git a/app/webroot/img/arrow_to.png b/app/webroot/img/arrow_to.png new file mode 100644 index 0000000..f021e98 Binary files /dev/null and b/app/webroot/img/arrow_to.png differ diff --git a/app/webroot/img/attachment.png b/app/webroot/img/attachment.png new file mode 100644 index 0000000..b7ce3c4 Binary files /dev/null and b/app/webroot/img/attachment.png differ diff --git a/app/webroot/img/bullet_add.png b/app/webroot/img/bullet_add.png new file mode 100644 index 0000000..41ff833 Binary files /dev/null and b/app/webroot/img/bullet_add.png differ diff --git a/app/webroot/img/bullet_black.png b/app/webroot/img/bullet_black.png new file mode 100644 index 0000000..5761970 Binary files /dev/null and b/app/webroot/img/bullet_black.png differ diff --git a/app/webroot/img/bullet_blue.png b/app/webroot/img/bullet_blue.png new file mode 100644 index 0000000..a7651ec Binary files /dev/null and b/app/webroot/img/bullet_blue.png differ diff --git a/app/webroot/img/bullet_delete.png b/app/webroot/img/bullet_delete.png new file mode 100644 index 0000000..bd6271b Binary files /dev/null and b/app/webroot/img/bullet_delete.png differ diff --git a/app/webroot/img/bullet_orange.png b/app/webroot/img/bullet_orange.png new file mode 100644 index 0000000..638b1ef Binary files /dev/null and b/app/webroot/img/bullet_orange.png differ diff --git a/app/webroot/img/bullet_purple.png b/app/webroot/img/bullet_purple.png new file mode 100644 index 0000000..52ba503 Binary files /dev/null and b/app/webroot/img/bullet_purple.png differ diff --git a/app/webroot/img/bullet_toggle_minus.png b/app/webroot/img/bullet_toggle_minus.png new file mode 100644 index 0000000..5ce7593 Binary files /dev/null and b/app/webroot/img/bullet_toggle_minus.png differ diff --git a/app/webroot/img/bullet_toggle_plus.png b/app/webroot/img/bullet_toggle_plus.png new file mode 100644 index 0000000..b3603d3 Binary files /dev/null and b/app/webroot/img/bullet_toggle_plus.png differ diff --git a/app/webroot/img/cake.icon.gif b/app/webroot/img/cake.icon.gif new file mode 100755 index 0000000..f29f72e Binary files /dev/null and b/app/webroot/img/cake.icon.gif differ diff --git a/app/webroot/img/cake.power.gif b/app/webroot/img/cake.power.gif new file mode 100755 index 0000000..8f8d570 Binary files /dev/null and b/app/webroot/img/cake.power.gif differ diff --git a/app/webroot/img/calendar.png b/app/webroot/img/calendar.png new file mode 100644 index 0000000..619172a Binary files /dev/null and b/app/webroot/img/calendar.png differ diff --git a/app/webroot/img/cancel.png b/app/webroot/img/cancel.png new file mode 100644 index 0000000..0840438 Binary files /dev/null and b/app/webroot/img/cancel.png differ diff --git a/app/webroot/img/changeset.png b/app/webroot/img/changeset.png new file mode 100644 index 0000000..67de2c6 Binary files /dev/null and b/app/webroot/img/changeset.png differ diff --git a/app/webroot/img/close.png b/app/webroot/img/close.png new file mode 100644 index 0000000..3501ed4 Binary files /dev/null and b/app/webroot/img/close.png differ diff --git a/app/webroot/img/close_hl.png b/app/webroot/img/close_hl.png new file mode 100644 index 0000000..a433f75 Binary files /dev/null and b/app/webroot/img/close_hl.png differ diff --git a/app/webroot/img/comment.png b/app/webroot/img/comment.png new file mode 100644 index 0000000..7bc9233 Binary files /dev/null and b/app/webroot/img/comment.png differ diff --git a/app/webroot/img/comments.png b/app/webroot/img/comments.png new file mode 100644 index 0000000..39433cf Binary files /dev/null and b/app/webroot/img/comments.png differ diff --git a/app/webroot/img/copy.png b/app/webroot/img/copy.png new file mode 100644 index 0000000..dccaa06 Binary files /dev/null and b/app/webroot/img/copy.png differ diff --git a/app/webroot/img/csv.png b/app/webroot/img/csv.png new file mode 100644 index 0000000..4058631 Binary files /dev/null and b/app/webroot/img/csv.png differ diff --git a/app/webroot/img/delete.png b/app/webroot/img/delete.png new file mode 100644 index 0000000..a1af31d Binary files /dev/null and b/app/webroot/img/delete.png differ diff --git a/app/webroot/img/document.png b/app/webroot/img/document.png new file mode 100644 index 0000000..d00b9b2 Binary files /dev/null and b/app/webroot/img/document.png differ diff --git a/app/webroot/img/draft.png b/app/webroot/img/draft.png new file mode 100644 index 0000000..9eda38b Binary files /dev/null and b/app/webroot/img/draft.png differ diff --git a/app/webroot/img/edit.png b/app/webroot/img/edit.png new file mode 100644 index 0000000..1b6a9e3 Binary files /dev/null and b/app/webroot/img/edit.png differ diff --git a/app/webroot/img/external.png b/app/webroot/img/external.png new file mode 100644 index 0000000..45df640 Binary files /dev/null and b/app/webroot/img/external.png differ diff --git a/app/webroot/img/false.png b/app/webroot/img/false.png new file mode 100644 index 0000000..e308ddc Binary files /dev/null and b/app/webroot/img/false.png differ diff --git a/app/webroot/img/fav.png b/app/webroot/img/fav.png new file mode 100644 index 0000000..49c0f47 Binary files /dev/null and b/app/webroot/img/fav.png differ diff --git a/app/webroot/img/fav_off.png b/app/webroot/img/fav_off.png new file mode 100644 index 0000000..5b10e9d Binary files /dev/null and b/app/webroot/img/fav_off.png differ diff --git a/app/webroot/img/feed.png b/app/webroot/img/feed.png new file mode 100644 index 0000000..900188a Binary files /dev/null and b/app/webroot/img/feed.png differ diff --git a/app/webroot/img/file.png b/app/webroot/img/file.png new file mode 100644 index 0000000..f387dd3 Binary files /dev/null and b/app/webroot/img/file.png differ diff --git a/app/webroot/img/folder.png b/app/webroot/img/folder.png new file mode 100644 index 0000000..d2ab69a Binary files /dev/null and b/app/webroot/img/folder.png differ diff --git a/app/webroot/img/folder_open.png b/app/webroot/img/folder_open.png new file mode 100644 index 0000000..e8e8c41 Binary files /dev/null and b/app/webroot/img/folder_open.png differ diff --git a/app/webroot/img/folder_open_add.png b/app/webroot/img/folder_open_add.png new file mode 100644 index 0000000..1ce4dca Binary files /dev/null and b/app/webroot/img/folder_open_add.png differ diff --git a/app/webroot/img/folder_open_orange.png b/app/webroot/img/folder_open_orange.png new file mode 100644 index 0000000..fb909ba Binary files /dev/null and b/app/webroot/img/folder_open_orange.png differ diff --git a/app/webroot/img/help.png b/app/webroot/img/help.png new file mode 100644 index 0000000..af4e6ff Binary files /dev/null and b/app/webroot/img/help.png differ diff --git a/app/webroot/img/history.png b/app/webroot/img/history.png new file mode 100644 index 0000000..c6a9607 Binary files /dev/null and b/app/webroot/img/history.png differ diff --git a/app/webroot/img/home.png b/app/webroot/img/home.png new file mode 100644 index 0000000..21ee547 Binary files /dev/null and b/app/webroot/img/home.png differ diff --git a/app/webroot/img/html.png b/app/webroot/img/html.png new file mode 100644 index 0000000..efb32e7 Binary files /dev/null and b/app/webroot/img/html.png differ diff --git a/app/webroot/img/image.png b/app/webroot/img/image.png new file mode 100644 index 0000000..a22cf7f Binary files /dev/null and b/app/webroot/img/image.png differ diff --git a/app/webroot/img/index.png b/app/webroot/img/index.png new file mode 100644 index 0000000..1ada3b2 Binary files /dev/null and b/app/webroot/img/index.png differ diff --git a/app/webroot/img/jstoolbar/bt_bq.png b/app/webroot/img/jstoolbar/bt_bq.png new file mode 100644 index 0000000..c3af4e0 Binary files /dev/null and b/app/webroot/img/jstoolbar/bt_bq.png differ diff --git a/app/webroot/img/jstoolbar/bt_bq_remove.png b/app/webroot/img/jstoolbar/bt_bq_remove.png new file mode 100644 index 0000000..05d5ff7 Binary files /dev/null and b/app/webroot/img/jstoolbar/bt_bq_remove.png differ diff --git a/app/webroot/img/jstoolbar/bt_code.png b/app/webroot/img/jstoolbar/bt_code.png new file mode 100644 index 0000000..8b6aefb Binary files /dev/null and b/app/webroot/img/jstoolbar/bt_code.png differ diff --git a/app/webroot/img/jstoolbar/bt_del.png b/app/webroot/img/jstoolbar/bt_del.png new file mode 100644 index 0000000..36a912b Binary files /dev/null and b/app/webroot/img/jstoolbar/bt_del.png differ diff --git a/app/webroot/img/jstoolbar/bt_em.png b/app/webroot/img/jstoolbar/bt_em.png new file mode 100644 index 0000000..caa8082 Binary files /dev/null and b/app/webroot/img/jstoolbar/bt_em.png differ diff --git a/app/webroot/img/jstoolbar/bt_h1.png b/app/webroot/img/jstoolbar/bt_h1.png new file mode 100644 index 0000000..b64e759 Binary files /dev/null and b/app/webroot/img/jstoolbar/bt_h1.png differ diff --git a/app/webroot/img/jstoolbar/bt_h2.png b/app/webroot/img/jstoolbar/bt_h2.png new file mode 100644 index 0000000..1e88cb9 Binary files /dev/null and b/app/webroot/img/jstoolbar/bt_h2.png differ diff --git a/app/webroot/img/jstoolbar/bt_h3.png b/app/webroot/img/jstoolbar/bt_h3.png new file mode 100644 index 0000000..646bad1 Binary files /dev/null and b/app/webroot/img/jstoolbar/bt_h3.png differ diff --git a/app/webroot/img/jstoolbar/bt_img.png b/app/webroot/img/jstoolbar/bt_img.png new file mode 100644 index 0000000..ddded46 Binary files /dev/null and b/app/webroot/img/jstoolbar/bt_img.png differ diff --git a/app/webroot/img/jstoolbar/bt_ins.png b/app/webroot/img/jstoolbar/bt_ins.png new file mode 100644 index 0000000..92c8dff Binary files /dev/null and b/app/webroot/img/jstoolbar/bt_ins.png differ diff --git a/app/webroot/img/jstoolbar/bt_link.png b/app/webroot/img/jstoolbar/bt_link.png new file mode 100644 index 0000000..8d67e3d Binary files /dev/null and b/app/webroot/img/jstoolbar/bt_link.png differ diff --git a/app/webroot/img/jstoolbar/bt_ol.png b/app/webroot/img/jstoolbar/bt_ol.png new file mode 100644 index 0000000..0cce2fa Binary files /dev/null and b/app/webroot/img/jstoolbar/bt_ol.png differ diff --git a/app/webroot/img/jstoolbar/bt_pre.png b/app/webroot/img/jstoolbar/bt_pre.png new file mode 100644 index 0000000..99a7d52 Binary files /dev/null and b/app/webroot/img/jstoolbar/bt_pre.png differ diff --git a/app/webroot/img/jstoolbar/bt_strong.png b/app/webroot/img/jstoolbar/bt_strong.png new file mode 100644 index 0000000..33e6daa Binary files /dev/null and b/app/webroot/img/jstoolbar/bt_strong.png differ diff --git a/app/webroot/img/jstoolbar/bt_ul.png b/app/webroot/img/jstoolbar/bt_ul.png new file mode 100644 index 0000000..df9ecc0 Binary files /dev/null and b/app/webroot/img/jstoolbar/bt_ul.png differ diff --git a/app/webroot/img/loading.gif b/app/webroot/img/loading.gif new file mode 100644 index 0000000..085ccae Binary files /dev/null and b/app/webroot/img/loading.gif differ diff --git a/app/webroot/img/locked.png b/app/webroot/img/locked.png new file mode 100644 index 0000000..82d6299 Binary files /dev/null and b/app/webroot/img/locked.png differ diff --git a/app/webroot/img/message.png b/app/webroot/img/message.png new file mode 100644 index 0000000..252ea14 Binary files /dev/null and b/app/webroot/img/message.png differ diff --git a/app/webroot/img/milestone.png b/app/webroot/img/milestone.png new file mode 100644 index 0000000..3df96fc Binary files /dev/null and b/app/webroot/img/milestone.png differ diff --git a/app/webroot/img/move.png b/app/webroot/img/move.png new file mode 100644 index 0000000..32fdb84 Binary files /dev/null and b/app/webroot/img/move.png differ diff --git a/app/webroot/img/news.png b/app/webroot/img/news.png new file mode 100644 index 0000000..6a2ecce Binary files /dev/null and b/app/webroot/img/news.png differ diff --git a/app/webroot/img/package.png b/app/webroot/img/package.png new file mode 100644 index 0000000..ff629d1 Binary files /dev/null and b/app/webroot/img/package.png differ diff --git a/app/webroot/img/pdf.png b/app/webroot/img/pdf.png new file mode 100644 index 0000000..68c9bad Binary files /dev/null and b/app/webroot/img/pdf.png differ diff --git a/app/webroot/img/projects.png b/app/webroot/img/projects.png new file mode 100644 index 0000000..073c721 Binary files /dev/null and b/app/webroot/img/projects.png differ diff --git a/app/webroot/img/reload.png b/app/webroot/img/reload.png new file mode 100644 index 0000000..c5eb34e Binary files /dev/null and b/app/webroot/img/reload.png differ diff --git a/app/webroot/img/report.png b/app/webroot/img/report.png new file mode 100644 index 0000000..05386ac Binary files /dev/null and b/app/webroot/img/report.png differ diff --git a/app/webroot/img/save.png b/app/webroot/img/save.png new file mode 100644 index 0000000..f379d9f Binary files /dev/null and b/app/webroot/img/save.png differ diff --git a/app/webroot/img/sort_asc.png b/app/webroot/img/sort_asc.png new file mode 100644 index 0000000..e9cb0f4 Binary files /dev/null and b/app/webroot/img/sort_asc.png differ diff --git a/app/webroot/img/sort_desc.png b/app/webroot/img/sort_desc.png new file mode 100644 index 0000000..fc80a5c Binary files /dev/null and b/app/webroot/img/sort_desc.png differ diff --git a/app/webroot/img/stats.png b/app/webroot/img/stats.png new file mode 100644 index 0000000..22ae78a Binary files /dev/null and b/app/webroot/img/stats.png differ diff --git a/app/webroot/img/sticky.png b/app/webroot/img/sticky.png new file mode 100644 index 0000000..d32ee63 Binary files /dev/null and b/app/webroot/img/sticky.png differ diff --git a/app/webroot/img/sub.gif b/app/webroot/img/sub.gif new file mode 100644 index 0000000..52e4065 Binary files /dev/null and b/app/webroot/img/sub.gif differ diff --git a/app/webroot/img/task_done.png b/app/webroot/img/task_done.png new file mode 100644 index 0000000..2a4c81e Binary files /dev/null and b/app/webroot/img/task_done.png differ diff --git a/app/webroot/img/task_late.png b/app/webroot/img/task_late.png new file mode 100644 index 0000000..2e8a40d Binary files /dev/null and b/app/webroot/img/task_late.png differ diff --git a/app/webroot/img/task_todo.png b/app/webroot/img/task_todo.png new file mode 100644 index 0000000..43c1eb9 Binary files /dev/null and b/app/webroot/img/task_todo.png differ diff --git a/app/webroot/img/ticket.png b/app/webroot/img/ticket.png new file mode 100644 index 0000000..244e6ca Binary files /dev/null and b/app/webroot/img/ticket.png differ diff --git a/app/webroot/img/ticket_checked.png b/app/webroot/img/ticket_checked.png new file mode 100644 index 0000000..4b1dfbc Binary files /dev/null and b/app/webroot/img/ticket_checked.png differ diff --git a/app/webroot/img/ticket_edit.png b/app/webroot/img/ticket_edit.png new file mode 100644 index 0000000..291bfc7 Binary files /dev/null and b/app/webroot/img/ticket_edit.png differ diff --git a/app/webroot/img/ticket_note.png b/app/webroot/img/ticket_note.png new file mode 100644 index 0000000..c69db22 Binary files /dev/null and b/app/webroot/img/ticket_note.png differ diff --git a/app/webroot/img/time.png b/app/webroot/img/time.png new file mode 100644 index 0000000..81aa780 Binary files /dev/null and b/app/webroot/img/time.png differ diff --git a/app/webroot/img/toggle_check.png b/app/webroot/img/toggle_check.png new file mode 100644 index 0000000..aca5e43 Binary files /dev/null and b/app/webroot/img/toggle_check.png differ diff --git a/app/webroot/img/true.png b/app/webroot/img/true.png new file mode 100644 index 0000000..cecf618 Binary files /dev/null and b/app/webroot/img/true.png differ diff --git a/app/webroot/img/txt.png b/app/webroot/img/txt.png new file mode 100644 index 0000000..2978385 Binary files /dev/null and b/app/webroot/img/txt.png differ diff --git a/app/webroot/img/unlock.png b/app/webroot/img/unlock.png new file mode 100644 index 0000000..f15fead Binary files /dev/null and b/app/webroot/img/unlock.png differ diff --git a/app/webroot/img/user.png b/app/webroot/img/user.png new file mode 100644 index 0000000..5f55e7e Binary files /dev/null and b/app/webroot/img/user.png differ diff --git a/app/webroot/img/user_new.png b/app/webroot/img/user_new.png new file mode 100644 index 0000000..aaa430d Binary files /dev/null and b/app/webroot/img/user_new.png differ diff --git a/app/webroot/img/user_page.png b/app/webroot/img/user_page.png new file mode 100644 index 0000000..7814486 Binary files /dev/null and b/app/webroot/img/user_page.png differ diff --git a/app/webroot/img/users.png b/app/webroot/img/users.png new file mode 100644 index 0000000..f3a07c3 Binary files /dev/null and b/app/webroot/img/users.png differ diff --git a/app/webroot/img/warning.png b/app/webroot/img/warning.png new file mode 100644 index 0000000..628cf2d Binary files /dev/null and b/app/webroot/img/warning.png differ diff --git a/app/webroot/img/wiki_edit.png b/app/webroot/img/wiki_edit.png new file mode 100644 index 0000000..bdc333a Binary files /dev/null and b/app/webroot/img/wiki_edit.png differ diff --git a/app/webroot/img/zoom_in.png b/app/webroot/img/zoom_in.png new file mode 100644 index 0000000..d9abe7f Binary files /dev/null and b/app/webroot/img/zoom_in.png differ diff --git a/app/webroot/img/zoom_in_g.png b/app/webroot/img/zoom_in_g.png new file mode 100644 index 0000000..72b271c Binary files /dev/null and b/app/webroot/img/zoom_in_g.png differ diff --git a/app/webroot/img/zoom_out.png b/app/webroot/img/zoom_out.png new file mode 100644 index 0000000..906e4a4 Binary files /dev/null and b/app/webroot/img/zoom_out.png differ diff --git a/app/webroot/img/zoom_out_g.png b/app/webroot/img/zoom_out_g.png new file mode 100644 index 0000000..7f2416b Binary files /dev/null and b/app/webroot/img/zoom_out_g.png differ diff --git a/app/webroot/index.php b/app/webroot/index.php new file mode 100755 index 0000000..3f5a255 --- /dev/null +++ b/app/webroot/index.php @@ -0,0 +1,93 @@ +<?php +/* SVN FILE: $Id: index.php 7945 2008-12-19 02:16:01Z gwoo $ */ +/** + * Short description for file. + * + * Long description for file + * + * PHP versions 4 and 5 + * + * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org) + * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project + * @package cake + * @subpackage cake.app.webroot + * @since CakePHP(tm) v 0.2.9 + * @version $Revision: 7945 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2008-12-18 18:16:01 -0800 (Thu, 18 Dec 2008) $ + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ +/** + * Use the DS to separate the directories in other defines + */ + if (!defined('DS')) { + define('DS', DIRECTORY_SEPARATOR); + } +/** + * These defines should only be edited if you have cake installed in + * a directory layout other than the way it is distributed. + * When using custom settings be sure to use the DS and do not add a trailing DS. + */ + +/** + * The full path to the directory which holds "app", WITHOUT a trailing DS. + * + */ + if (!defined('ROOT')) { + define('ROOT', dirname(dirname(dirname(__FILE__)))); + } +/** + * The actual directory name for the "app". + * + */ + if (!defined('APP_DIR')) { + define('APP_DIR', basename(dirname(dirname(__FILE__)))); + } +/** + * The absolute path to the "cake" directory, WITHOUT a trailing DS. + * + */ + if (!defined('CAKE_CORE_INCLUDE_PATH')) { + define('CAKE_CORE_INCLUDE_PATH', ROOT); + } + +/** + * Editing below this line should NOT be necessary. + * Change at your own risk. + * + */ + if (!defined('WEBROOT_DIR')) { + define('WEBROOT_DIR', basename(dirname(__FILE__))); + } + if (!defined('WWW_ROOT')) { + define('WWW_ROOT', dirname(__FILE__) . DS); + } + if (!defined('CORE_PATH')) { + if (function_exists('ini_set') && ini_set('include_path', CAKE_CORE_INCLUDE_PATH . PATH_SEPARATOR . ROOT . DS . APP_DIR . DS . PATH_SEPARATOR . ini_get('include_path'))) { + define('APP_PATH', null); + define('CORE_PATH', null); + } else { + define('APP_PATH', ROOT . DS . APP_DIR . DS); + define('CORE_PATH', CAKE_CORE_INCLUDE_PATH . DS); + } + } + if (!include(CORE_PATH . 'cake' . DS . 'bootstrap.php')) { + trigger_error("CakePHP core could not be found. Check the value of CAKE_CORE_INCLUDE_PATH in APP/webroot/index.php. It should point to the directory containing your " . DS . "cake core directory and your " . DS . "vendors root directory.", E_USER_ERROR); + } + if (isset($_GET['url']) && $_GET['url'] === 'favicon.ico') { + return; + } else { + $Dispatcher = new Dispatcher(); + $Dispatcher->dispatch($url); + } + if (Configure::read() > 0) { + echo "<!-- " . round(getMicrotime() - $TIME_START, 4) . "s -->"; + } +?> \ No newline at end of file diff --git a/app/webroot/js/application.js b/app/webroot/js/application.js new file mode 100644 index 0000000..3becbeb --- /dev/null +++ b/app/webroot/js/application.js @@ -0,0 +1,144 @@ +/* redMine - project management software + Copyright (C) 2006-2008 Jean-Philippe Lang */ + +function checkAll (id, checked) { + var els = Element.descendants(id); + for (var i = 0; i < els.length; i++) { + if (els[i].disabled==false) { + els[i].checked = checked; + } + } +} + +function toggleCheckboxesBySelector(selector) { + boxes = $$(selector); + var all_checked = true; + for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } } + for (i = 0; i < boxes.length; i++) { boxes[i].checked = !all_checked; } +} + +function showAndScrollTo(id, focus) { + Element.show(id); + if (focus!=null) { Form.Element.focus(focus); } + Element.scrollTo(id); +} + +var fileFieldCount = 1; + +function addFileField() { + if (fileFieldCount >= 10) return false + fileFieldCount++; + var f = document.createElement("input"); + f.type = "file"; + f.name = "attachments[" + fileFieldCount + "][file]"; + f.size = 30; + var d = document.createElement("input"); + d.type = "text"; + d.name = "attachments[" + fileFieldCount + "][description]"; + d.size = 60; + + p = document.getElementById("attachments_fields"); + p.appendChild(document.createElement("br")); + p.appendChild(f); + p.appendChild(d); +} + +function showTab(name) { + var f = $$('div#content .tab-content'); + for(var i=0; i<f.length; i++){ + Element.hide(f[i]); + } + var f = $$('div.tabs a'); + for(var i=0; i<f.length; i++){ + Element.removeClassName(f[i], "selected"); + } + Element.show('tab-content-' + name); + Element.addClassName('tab-' + name, "selected"); + return false; +} + +function setPredecessorFieldsVisibility() { + relationType = $('relation_relation_type'); + if (relationType && relationType.value == "precedes") { + Element.show('predecessor_fields'); + } else { + Element.hide('predecessor_fields'); + } +} + +function promptToRemote(text, param, url) { + value = prompt(text + ':'); + if (value) { + new Ajax.Request(url + '?' + param + '=' + encodeURIComponent(value), {asynchronous:true, evalScripts:true}); + return false; + } +} + +function collapseScmEntry(id) { + var els = document.getElementsByClassName(id, 'browser'); + for (var i = 0; i < els.length; i++) { + if (els[i].hasClassName('open')) { + collapseScmEntry(els[i].id); + } + Element.hide(els[i]); + } + $(id).removeClassName('open'); +} + +function expandScmEntry(id) { + var els = document.getElementsByClassName(id, 'browser'); + for (var i = 0; i < els.length; i++) { + Element.show(els[i]); + if (els[i].hasClassName('loaded') && !els[i].hasClassName('collapsed')) { + expandScmEntry(els[i].id); + } + } + $(id).addClassName('open'); +} + +function scmEntryClick(id) { + el = $(id); + if (el.hasClassName('open')) { + collapseScmEntry(id); + el.addClassName('collapsed'); + return false; + } else if (el.hasClassName('loaded')) { + expandScmEntry(id); + el.removeClassName('collapsed'); + return false; + } + if (el.hasClassName('loading')) { + return false; + } + el.addClassName('loading'); + return true; +} + +function scmEntryLoaded(id) { + Element.addClassName(id, 'open'); + Element.addClassName(id, 'loaded'); + Element.removeClassName(id, 'loading'); +} + +function randomKey(size) { + var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'); + var key = ''; + for (i = 0; i < size; i++) { + key += chars[Math.floor(Math.random() * chars.length)]; + } + return key; +} + +/* shows and hides ajax indicator */ +Ajax.Responders.register({ + onCreate: function(){ + if ($('ajax-indicator') && Ajax.activeRequestCount > 0) { + Element.show('ajax-indicator'); + } + }, + onComplete: function(){ + if ($('ajax-indicator') && Ajax.activeRequestCount == 0) { + Element.hide('ajax-indicator'); + } + } +}); diff --git a/app/webroot/js/calendar/calendar-setup.js b/app/webroot/js/calendar/calendar-setup.js new file mode 100644 index 0000000..f2b4854 --- /dev/null +++ b/app/webroot/js/calendar/calendar-setup.js @@ -0,0 +1,200 @@ +/* Copyright Mihai Bazon, 2002, 2003 | http://dynarch.com/mishoo/ + * --------------------------------------------------------------------------- + * + * The DHTML Calendar + * + * Details and latest version at: + * http://dynarch.com/mishoo/calendar.epl + * + * This script is distributed under the GNU Lesser General Public License. + * Read the entire license text here: http://www.gnu.org/licenses/lgpl.html + * + * This file defines helper functions for setting up the calendar. They are + * intended to help non-programmers get a working calendar on their site + * quickly. This script should not be seen as part of the calendar. It just + * shows you what one can do with the calendar, while in the same time + * providing a quick and simple method for setting it up. If you need + * exhaustive customization of the calendar creation process feel free to + * modify this code to suit your needs (this is recommended and much better + * than modifying calendar.js itself). + */ + +// $Id: calendar-setup.js,v 1.25 2005/03/07 09:51:33 mishoo Exp $ + +/** + * This function "patches" an input field (or other element) to use a calendar + * widget for date selection. + * + * The "params" is a single object that can have the following properties: + * + * prop. name | description + * ------------------------------------------------------------------------------------------------- + * inputField | the ID of an input field to store the date + * displayArea | the ID of a DIV or other element to show the date + * button | ID of a button or other element that will trigger the calendar + * eventName | event that will trigger the calendar, without the "on" prefix (default: "click") + * ifFormat | date format that will be stored in the input field + * daFormat | the date format that will be used to display the date in displayArea + * singleClick | (true/false) wether the calendar is in single click mode or not (default: true) + * firstDay | numeric: 0 to 6. "0" means display Sunday first, "1" means display Monday first, etc. + * align | alignment (default: "Br"); if you don't know what's this see the calendar documentation + * range | array with 2 elements. Default: [1900, 2999] -- the range of years available + * weekNumbers | (true/false) if it's true (default) the calendar will display week numbers + * flat | null or element ID; if not null the calendar will be a flat calendar having the parent with the given ID + * flatCallback | function that receives a JS Date object and returns an URL to point the browser to (for flat calendar) + * disableFunc | function that receives a JS Date object and should return true if that date has to be disabled in the calendar + * onSelect | function that gets called when a date is selected. You don't _have_ to supply this (the default is generally okay) + * onClose | function that gets called when the calendar is closed. [default] + * onUpdate | function that gets called after the date is updated in the input field. Receives a reference to the calendar. + * date | the date that the calendar will be initially displayed to + * showsTime | default: false; if true the calendar will include a time selector + * timeFormat | the time format; can be "12" or "24", default is "12" + * electric | if true (default) then given fields/date areas are updated for each move; otherwise they're updated only on close + * step | configures the step of the years in drop-down boxes; default: 2 + * position | configures the calendar absolute position; default: null + * cache | if "true" (but default: "false") it will reuse the same calendar object, where possible + * showOthers | if "true" (but default: "false") it will show days from other months too + * + * None of them is required, they all have default values. However, if you + * pass none of "inputField", "displayArea" or "button" you'll get a warning + * saying "nothing to setup". + */ +Calendar.setup = function (params) { + function param_default(pname, def) { if (typeof params[pname] == "undefined") { params[pname] = def; } }; + + param_default("inputField", null); + param_default("displayArea", null); + param_default("button", null); + param_default("eventName", "click"); + param_default("ifFormat", "%Y/%m/%d"); + param_default("daFormat", "%Y/%m/%d"); + param_default("singleClick", true); + param_default("disableFunc", null); + param_default("dateStatusFunc", params["disableFunc"]); // takes precedence if both are defined + param_default("dateText", null); + param_default("firstDay", null); + param_default("align", "Br"); + param_default("range", [1900, 2999]); + param_default("weekNumbers", true); + param_default("flat", null); + param_default("flatCallback", null); + param_default("onSelect", null); + param_default("onClose", null); + param_default("onUpdate", null); + param_default("date", null); + param_default("showsTime", false); + param_default("timeFormat", "24"); + param_default("electric", true); + param_default("step", 2); + param_default("position", null); + param_default("cache", false); + param_default("showOthers", false); + param_default("multiple", null); + + var tmp = ["inputField", "displayArea", "button"]; + for (var i in tmp) { + if (typeof params[tmp[i]] == "string") { + params[tmp[i]] = document.getElementById(params[tmp[i]]); + } + } + if (!(params.flat || params.multiple || params.inputField || params.displayArea || params.button)) { + alert("Calendar.setup:\n Nothing to setup (no fields found). Please check your code"); + return false; + } + + function onSelect(cal) { + var p = cal.params; + var update = (cal.dateClicked || p.electric); + if (update && p.inputField) { + p.inputField.value = cal.date.print(p.ifFormat); + if (typeof p.inputField.onchange == "function") + p.inputField.onchange(); + } + if (update && p.displayArea) + p.displayArea.innerHTML = cal.date.print(p.daFormat); + if (update && typeof p.onUpdate == "function") + p.onUpdate(cal); + if (update && p.flat) { + if (typeof p.flatCallback == "function") + p.flatCallback(cal); + } + if (update && p.singleClick && cal.dateClicked) + cal.callCloseHandler(); + }; + + if (params.flat != null) { + if (typeof params.flat == "string") + params.flat = document.getElementById(params.flat); + if (!params.flat) { + alert("Calendar.setup:\n Flat specified but can't find parent."); + return false; + } + var cal = new Calendar(params.firstDay, params.date, params.onSelect || onSelect); + cal.showsOtherMonths = params.showOthers; + cal.showsTime = params.showsTime; + cal.time24 = (params.timeFormat == "24"); + cal.params = params; + cal.weekNumbers = params.weekNumbers; + cal.setRange(params.range[0], params.range[1]); + cal.setDateStatusHandler(params.dateStatusFunc); + cal.getDateText = params.dateText; + if (params.ifFormat) { + cal.setDateFormat(params.ifFormat); + } + if (params.inputField && typeof params.inputField.value == "string") { + cal.parseDate(params.inputField.value); + } + cal.create(params.flat); + cal.show(); + return false; + } + + var triggerEl = params.button || params.displayArea || params.inputField; + triggerEl["on" + params.eventName] = function() { + var dateEl = params.inputField || params.displayArea; + var dateFmt = params.inputField ? params.ifFormat : params.daFormat; + var mustCreate = false; + var cal = window.calendar; + if (dateEl) + params.date = Date.parseDate(dateEl.value || dateEl.innerHTML, dateFmt); + if (!(cal && params.cache)) { + window.calendar = cal = new Calendar(params.firstDay, + params.date, + params.onSelect || onSelect, + params.onClose || function(cal) { cal.hide(); }); + cal.showsTime = params.showsTime; + cal.time24 = (params.timeFormat == "24"); + cal.weekNumbers = params.weekNumbers; + mustCreate = true; + } else { + if (params.date) + cal.setDate(params.date); + cal.hide(); + } + if (params.multiple) { + cal.multiple = {}; + for (var i = params.multiple.length; --i >= 0;) { + var d = params.multiple[i]; + var ds = d.print("%Y%m%d"); + cal.multiple[ds] = d; + } + } + cal.showsOtherMonths = params.showOthers; + cal.yearStep = params.step; + cal.setRange(params.range[0], params.range[1]); + cal.params = params; + cal.setDateStatusHandler(params.dateStatusFunc); + cal.getDateText = params.dateText; + cal.setDateFormat(dateFmt); + if (mustCreate) + cal.create(); + cal.refresh(); + if (!params.position) + cal.showAtElement(params.button || params.displayArea || params.inputField, params.align); + else + cal.showAt(params.position[0], params.position[1]); + return false; + }; + + return cal; +}; diff --git a/app/webroot/js/calendar/calendar.js b/app/webroot/js/calendar/calendar.js new file mode 100644 index 0000000..9088e0e --- /dev/null +++ b/app/webroot/js/calendar/calendar.js @@ -0,0 +1,1806 @@ +/* Copyright Mihai Bazon, 2002-2005 | www.bazon.net/mishoo + * ----------------------------------------------------------- + * + * The DHTML Calendar, version 1.0 "It is happening again" + * + * Details and latest version at: + * www.dynarch.com/projects/calendar + * + * This script is developed by Dynarch.com. Visit us at www.dynarch.com. + * + * This script is distributed under the GNU Lesser General Public License. + * Read the entire license text here: http://www.gnu.org/licenses/lgpl.html + */ + +// $Id: calendar.js,v 1.51 2005/03/07 16:44:31 mishoo Exp $ + +/** The Calendar object constructor. */ +Calendar = function (firstDayOfWeek, dateStr, onSelected, onClose) { + // member variables + this.activeDiv = null; + this.currentDateEl = null; + this.getDateStatus = null; + this.getDateToolTip = null; + this.getDateText = null; + this.timeout = null; + this.onSelected = onSelected || null; + this.onClose = onClose || null; + this.dragging = false; + this.hidden = false; + this.minYear = 1970; + this.maxYear = 2050; + this.dateFormat = Calendar._TT["DEF_DATE_FORMAT"]; + this.ttDateFormat = Calendar._TT["TT_DATE_FORMAT"]; + this.isPopup = true; + this.weekNumbers = true; + this.firstDayOfWeek = typeof firstDayOfWeek == "number" ? firstDayOfWeek : Calendar._FD; // 0 for Sunday, 1 for Monday, etc. + this.showsOtherMonths = false; + this.dateStr = dateStr; + this.ar_days = null; + this.showsTime = false; + this.time24 = true; + this.yearStep = 2; + this.hiliteToday = true; + this.multiple = null; + // HTML elements + this.table = null; + this.element = null; + this.tbody = null; + this.firstdayname = null; + // Combo boxes + this.monthsCombo = null; + this.yearsCombo = null; + this.hilitedMonth = null; + this.activeMonth = null; + this.hilitedYear = null; + this.activeYear = null; + // Information + this.dateClicked = false; + + // one-time initializations + if (typeof Calendar._SDN == "undefined") { + // table of short day names + if (typeof Calendar._SDN_len == "undefined") + Calendar._SDN_len = 3; + var ar = new Array(); + for (var i = 8; i > 0;) { + ar[--i] = Calendar._DN[i].substr(0, Calendar._SDN_len); + } + Calendar._SDN = ar; + // table of short month names + if (typeof Calendar._SMN_len == "undefined") + Calendar._SMN_len = 3; + ar = new Array(); + for (var i = 12; i > 0;) { + ar[--i] = Calendar._MN[i].substr(0, Calendar._SMN_len); + } + Calendar._SMN = ar; + } +}; + +// ** constants + +/// "static", needed for event handlers. +Calendar._C = null; + +/// detect a special case of "web browser" +Calendar.is_ie = ( /msie/i.test(navigator.userAgent) && + !/opera/i.test(navigator.userAgent) ); + +Calendar.is_ie5 = ( Calendar.is_ie && /msie 5\.0/i.test(navigator.userAgent) ); + +/// detect Opera browser +Calendar.is_opera = /opera/i.test(navigator.userAgent); + +/// detect KHTML-based browsers +Calendar.is_khtml = /Konqueror|Safari|KHTML/i.test(navigator.userAgent); + +// BEGIN: UTILITY FUNCTIONS; beware that these might be moved into a separate +// library, at some point. + +Calendar.getAbsolutePos = function(el) { + var SL = 0, ST = 0; + var is_div = /^div$/i.test(el.tagName); + if (is_div && el.scrollLeft) + SL = el.scrollLeft; + if (is_div && el.scrollTop) + ST = el.scrollTop; + var r = { x: el.offsetLeft - SL, y: el.offsetTop - ST }; + if (el.offsetParent) { + var tmp = this.getAbsolutePos(el.offsetParent); + r.x += tmp.x; + r.y += tmp.y; + } + return r; +}; + +Calendar.isRelated = function (el, evt) { + var related = evt.relatedTarget; + if (!related) { + var type = evt.type; + if (type == "mouseover") { + related = evt.fromElement; + } else if (type == "mouseout") { + related = evt.toElement; + } + } + while (related) { + if (related == el) { + return true; + } + related = related.parentNode; + } + return false; +}; + +Calendar.removeClass = function(el, className) { + if (!(el && el.className)) { + return; + } + var cls = el.className.split(" "); + var ar = new Array(); + for (var i = cls.length; i > 0;) { + if (cls[--i] != className) { + ar[ar.length] = cls[i]; + } + } + el.className = ar.join(" "); +}; + +Calendar.addClass = function(el, className) { + Calendar.removeClass(el, className); + el.className += " " + className; +}; + +// FIXME: the following 2 functions totally suck, are useless and should be replaced immediately. +Calendar.getElement = function(ev) { + var f = Calendar.is_ie ? window.event.srcElement : ev.currentTarget; + while (f.nodeType != 1 || /^div$/i.test(f.tagName)) + f = f.parentNode; + return f; +}; + +Calendar.getTargetElement = function(ev) { + var f = Calendar.is_ie ? window.event.srcElement : ev.target; + while (f.nodeType != 1) + f = f.parentNode; + return f; +}; + +Calendar.stopEvent = function(ev) { + ev || (ev = window.event); + if (Calendar.is_ie) { + ev.cancelBubble = true; + ev.returnValue = false; + } else { + ev.preventDefault(); + ev.stopPropagation(); + } + return false; +}; + +Calendar.addEvent = function(el, evname, func) { + if (el.attachEvent) { // IE + el.attachEvent("on" + evname, func); + } else if (el.addEventListener) { // Gecko / W3C + el.addEventListener(evname, func, true); + } else { + el["on" + evname] = func; + } +}; + +Calendar.removeEvent = function(el, evname, func) { + if (el.detachEvent) { // IE + el.detachEvent("on" + evname, func); + } else if (el.removeEventListener) { // Gecko / W3C + el.removeEventListener(evname, func, true); + } else { + el["on" + evname] = null; + } +}; + +Calendar.createElement = function(type, parent) { + var el = null; + if (document.createElementNS) { + // use the XHTML namespace; IE won't normally get here unless + // _they_ "fix" the DOM2 implementation. + el = document.createElementNS("http://www.w3.org/1999/xhtml", type); + } else { + el = document.createElement(type); + } + if (typeof parent != "undefined") { + parent.appendChild(el); + } + return el; +}; + +// END: UTILITY FUNCTIONS + +// BEGIN: CALENDAR STATIC FUNCTIONS + +/** Internal -- adds a set of events to make some element behave like a button. */ +Calendar._add_evs = function(el) { + with (Calendar) { + addEvent(el, "mouseover", dayMouseOver); + addEvent(el, "mousedown", dayMouseDown); + addEvent(el, "mouseout", dayMouseOut); + if (is_ie) { + addEvent(el, "dblclick", dayMouseDblClick); + el.setAttribute("unselectable", true); + } + } +}; + +Calendar.findMonth = function(el) { + if (typeof el.month != "undefined") { + return el; + } else if (typeof el.parentNode.month != "undefined") { + return el.parentNode; + } + return null; +}; + +Calendar.findYear = function(el) { + if (typeof el.year != "undefined") { + return el; + } else if (typeof el.parentNode.year != "undefined") { + return el.parentNode; + } + return null; +}; + +Calendar.showMonthsCombo = function () { + var cal = Calendar._C; + if (!cal) { + return false; + } + var cal = cal; + var cd = cal.activeDiv; + var mc = cal.monthsCombo; + if (cal.hilitedMonth) { + Calendar.removeClass(cal.hilitedMonth, "hilite"); + } + if (cal.activeMonth) { + Calendar.removeClass(cal.activeMonth, "active"); + } + var mon = cal.monthsCombo.getElementsByTagName("div")[cal.date.getMonth()]; + Calendar.addClass(mon, "active"); + cal.activeMonth = mon; + var s = mc.style; + s.display = "block"; + if (cd.navtype < 0) + s.left = cd.offsetLeft + "px"; + else { + var mcw = mc.offsetWidth; + if (typeof mcw == "undefined") + // Konqueror brain-dead techniques + mcw = 50; + s.left = (cd.offsetLeft + cd.offsetWidth - mcw) + "px"; + } + s.top = (cd.offsetTop + cd.offsetHeight) + "px"; +}; + +Calendar.showYearsCombo = function (fwd) { + var cal = Calendar._C; + if (!cal) { + return false; + } + var cal = cal; + var cd = cal.activeDiv; + var yc = cal.yearsCombo; + if (cal.hilitedYear) { + Calendar.removeClass(cal.hilitedYear, "hilite"); + } + if (cal.activeYear) { + Calendar.removeClass(cal.activeYear, "active"); + } + cal.activeYear = null; + var Y = cal.date.getFullYear() + (fwd ? 1 : -1); + var yr = yc.firstChild; + var show = false; + for (var i = 12; i > 0; --i) { + if (Y >= cal.minYear && Y <= cal.maxYear) { + yr.innerHTML = Y; + yr.year = Y; + yr.style.display = "block"; + show = true; + } else { + yr.style.display = "none"; + } + yr = yr.nextSibling; + Y += fwd ? cal.yearStep : -cal.yearStep; + } + if (show) { + var s = yc.style; + s.display = "block"; + if (cd.navtype < 0) + s.left = cd.offsetLeft + "px"; + else { + var ycw = yc.offsetWidth; + if (typeof ycw == "undefined") + // Konqueror brain-dead techniques + ycw = 50; + s.left = (cd.offsetLeft + cd.offsetWidth - ycw) + "px"; + } + s.top = (cd.offsetTop + cd.offsetHeight) + "px"; + } +}; + +// event handlers + +Calendar.tableMouseUp = function(ev) { + var cal = Calendar._C; + if (!cal) { + return false; + } + if (cal.timeout) { + clearTimeout(cal.timeout); + } + var el = cal.activeDiv; + if (!el) { + return false; + } + var target = Calendar.getTargetElement(ev); + ev || (ev = window.event); + Calendar.removeClass(el, "active"); + if (target == el || target.parentNode == el) { + Calendar.cellClick(el, ev); + } + var mon = Calendar.findMonth(target); + var date = null; + if (mon) { + date = new Date(cal.date); + if (mon.month != date.getMonth()) { + date.setMonth(mon.month); + cal.setDate(date); + cal.dateClicked = false; + cal.callHandler(); + } + } else { + var year = Calendar.findYear(target); + if (year) { + date = new Date(cal.date); + if (year.year != date.getFullYear()) { + date.setFullYear(year.year); + cal.setDate(date); + cal.dateClicked = false; + cal.callHandler(); + } + } + } + with (Calendar) { + removeEvent(document, "mouseup", tableMouseUp); + removeEvent(document, "mouseover", tableMouseOver); + removeEvent(document, "mousemove", tableMouseOver); + cal._hideCombos(); + _C = null; + return stopEvent(ev); + } +}; + +Calendar.tableMouseOver = function (ev) { + var cal = Calendar._C; + if (!cal) { + return; + } + var el = cal.activeDiv; + var target = Calendar.getTargetElement(ev); + if (target == el || target.parentNode == el) { + Calendar.addClass(el, "hilite active"); + Calendar.addClass(el.parentNode, "rowhilite"); + } else { + if (typeof el.navtype == "undefined" || (el.navtype != 50 && (el.navtype == 0 || Math.abs(el.navtype) > 2))) + Calendar.removeClass(el, "active"); + Calendar.removeClass(el, "hilite"); + Calendar.removeClass(el.parentNode, "rowhilite"); + } + ev || (ev = window.event); + if (el.navtype == 50 && target != el) { + var pos = Calendar.getAbsolutePos(el); + var w = el.offsetWidth; + var x = ev.clientX; + var dx; + var decrease = true; + if (x > pos.x + w) { + dx = x - pos.x - w; + decrease = false; + } else + dx = pos.x - x; + + if (dx < 0) dx = 0; + var range = el._range; + var current = el._current; + var count = Math.floor(dx / 10) % range.length; + for (var i = range.length; --i >= 0;) + if (range[i] == current) + break; + while (count-- > 0) + if (decrease) { + if (--i < 0) + i = range.length - 1; + } else if ( ++i >= range.length ) + i = 0; + var newval = range[i]; + el.innerHTML = newval; + + cal.onUpdateTime(); + } + var mon = Calendar.findMonth(target); + if (mon) { + if (mon.month != cal.date.getMonth()) { + if (cal.hilitedMonth) { + Calendar.removeClass(cal.hilitedMonth, "hilite"); + } + Calendar.addClass(mon, "hilite"); + cal.hilitedMonth = mon; + } else if (cal.hilitedMonth) { + Calendar.removeClass(cal.hilitedMonth, "hilite"); + } + } else { + if (cal.hilitedMonth) { + Calendar.removeClass(cal.hilitedMonth, "hilite"); + } + var year = Calendar.findYear(target); + if (year) { + if (year.year != cal.date.getFullYear()) { + if (cal.hilitedYear) { + Calendar.removeClass(cal.hilitedYear, "hilite"); + } + Calendar.addClass(year, "hilite"); + cal.hilitedYear = year; + } else if (cal.hilitedYear) { + Calendar.removeClass(cal.hilitedYear, "hilite"); + } + } else if (cal.hilitedYear) { + Calendar.removeClass(cal.hilitedYear, "hilite"); + } + } + return Calendar.stopEvent(ev); +}; + +Calendar.tableMouseDown = function (ev) { + if (Calendar.getTargetElement(ev) == Calendar.getElement(ev)) { + return Calendar.stopEvent(ev); + } +}; + +Calendar.calDragIt = function (ev) { + var cal = Calendar._C; + if (!(cal && cal.dragging)) { + return false; + } + var posX; + var posY; + if (Calendar.is_ie) { + posY = window.event.clientY + document.body.scrollTop; + posX = window.event.clientX + document.body.scrollLeft; + } else { + posX = ev.pageX; + posY = ev.pageY; + } + cal.hideShowCovered(); + var st = cal.element.style; + st.left = (posX - cal.xOffs) + "px"; + st.top = (posY - cal.yOffs) + "px"; + return Calendar.stopEvent(ev); +}; + +Calendar.calDragEnd = function (ev) { + var cal = Calendar._C; + if (!cal) { + return false; + } + cal.dragging = false; + with (Calendar) { + removeEvent(document, "mousemove", calDragIt); + removeEvent(document, "mouseup", calDragEnd); + tableMouseUp(ev); + } + cal.hideShowCovered(); +}; + +Calendar.dayMouseDown = function(ev) { + var el = Calendar.getElement(ev); + if (el.disabled) { + return false; + } + var cal = el.calendar; + cal.activeDiv = el; + Calendar._C = cal; + if (el.navtype != 300) with (Calendar) { + if (el.navtype == 50) { + el._current = el.innerHTML; + addEvent(document, "mousemove", tableMouseOver); + } else + addEvent(document, Calendar.is_ie5 ? "mousemove" : "mouseover", tableMouseOver); + addClass(el, "hilite active"); + addEvent(document, "mouseup", tableMouseUp); + } else if (cal.isPopup) { + cal._dragStart(ev); + } + if (el.navtype == -1 || el.navtype == 1) { + if (cal.timeout) clearTimeout(cal.timeout); + cal.timeout = setTimeout("Calendar.showMonthsCombo()", 250); + } else if (el.navtype == -2 || el.navtype == 2) { + if (cal.timeout) clearTimeout(cal.timeout); + cal.timeout = setTimeout((el.navtype > 0) ? "Calendar.showYearsCombo(true)" : "Calendar.showYearsCombo(false)", 250); + } else { + cal.timeout = null; + } + return Calendar.stopEvent(ev); +}; + +Calendar.dayMouseDblClick = function(ev) { + Calendar.cellClick(Calendar.getElement(ev), ev || window.event); + if (Calendar.is_ie) { + document.selection.empty(); + } +}; + +Calendar.dayMouseOver = function(ev) { + var el = Calendar.getElement(ev); + if (Calendar.isRelated(el, ev) || Calendar._C || el.disabled) { + return false; + } + if (el.ttip) { + if (el.ttip.substr(0, 1) == "_") { + el.ttip = el.caldate.print(el.calendar.ttDateFormat) + el.ttip.substr(1); + } + el.calendar.tooltips.innerHTML = el.ttip; + } + if (el.navtype != 300) { + Calendar.addClass(el, "hilite"); + if (el.caldate) { + Calendar.addClass(el.parentNode, "rowhilite"); + } + } + return Calendar.stopEvent(ev); +}; + +Calendar.dayMouseOut = function(ev) { + with (Calendar) { + var el = getElement(ev); + if (isRelated(el, ev) || _C || el.disabled) + return false; + removeClass(el, "hilite"); + if (el.caldate) + removeClass(el.parentNode, "rowhilite"); + if (el.calendar) + el.calendar.tooltips.innerHTML = _TT["SEL_DATE"]; + return stopEvent(ev); + } +}; + +/** + * A generic "click" handler :) handles all types of buttons defined in this + * calendar. + */ +Calendar.cellClick = function(el, ev) { + var cal = el.calendar; + var closing = false; + var newdate = false; + var date = null; + if (typeof el.navtype == "undefined") { + if (cal.currentDateEl) { + Calendar.removeClass(cal.currentDateEl, "selected"); + Calendar.addClass(el, "selected"); + closing = (cal.currentDateEl == el); + if (!closing) { + cal.currentDateEl = el; + } + } + cal.date.setDateOnly(el.caldate); + date = cal.date; + var other_month = !(cal.dateClicked = !el.otherMonth); + if (!other_month && !cal.currentDateEl) + cal._toggleMultipleDate(new Date(date)); + else + newdate = !el.disabled; + // a date was clicked + if (other_month) + cal._init(cal.firstDayOfWeek, date); + } else { + if (el.navtype == 200) { + Calendar.removeClass(el, "hilite"); + cal.callCloseHandler(); + return; + } + date = new Date(cal.date); + if (el.navtype == 0) + date.setDateOnly(new Date()); // TODAY + // unless "today" was clicked, we assume no date was clicked so + // the selected handler will know not to close the calenar when + // in single-click mode. + // cal.dateClicked = (el.navtype == 0); + cal.dateClicked = false; + var year = date.getFullYear(); + var mon = date.getMonth(); + function setMonth(m) { + var day = date.getDate(); + var max = date.getMonthDays(m); + if (day > max) { + date.setDate(max); + } + date.setMonth(m); + }; + switch (el.navtype) { + case 400: + Calendar.removeClass(el, "hilite"); + var text = Calendar._TT["ABOUT"]; + if (typeof text != "undefined") { + text += cal.showsTime ? Calendar._TT["ABOUT_TIME"] : ""; + } else { + // FIXME: this should be removed as soon as lang files get updated! + text = "Help and about box text is not translated into this language.\n" + + "If you know this language and you feel generous please update\n" + + "the corresponding file in \"lang\" subdir to match calendar-en.js\n" + + "and send it back to <mihai_bazon@yahoo.com> to get it into the distribution ;-)\n\n" + + "Thank you!\n" + + "http://dynarch.com/mishoo/calendar.epl\n"; + } + alert(text); + return; + case -2: + if (year > cal.minYear) { + date.setFullYear(year - 1); + } + break; + case -1: + if (mon > 0) { + setMonth(mon - 1); + } else if (year-- > cal.minYear) { + date.setFullYear(year); + setMonth(11); + } + break; + case 1: + if (mon < 11) { + setMonth(mon + 1); + } else if (year < cal.maxYear) { + date.setFullYear(year + 1); + setMonth(0); + } + break; + case 2: + if (year < cal.maxYear) { + date.setFullYear(year + 1); + } + break; + case 100: + cal.setFirstDayOfWeek(el.fdow); + return; + case 50: + var range = el._range; + var current = el.innerHTML; + for (var i = range.length; --i >= 0;) + if (range[i] == current) + break; + if (ev && ev.shiftKey) { + if (--i < 0) + i = range.length - 1; + } else if ( ++i >= range.length ) + i = 0; + var newval = range[i]; + el.innerHTML = newval; + cal.onUpdateTime(); + return; + case 0: + // TODAY will bring us here + if ((typeof cal.getDateStatus == "function") && + cal.getDateStatus(date, date.getFullYear(), date.getMonth(), date.getDate())) { + return false; + } + break; + } + if (!date.equalsTo(cal.date)) { + cal.setDate(date); + newdate = true; + } else if (el.navtype == 0) + newdate = closing = true; + } + if (newdate) { + ev && cal.callHandler(); + } + if (closing) { + Calendar.removeClass(el, "hilite"); + ev && cal.callCloseHandler(); + } +}; + +// END: CALENDAR STATIC FUNCTIONS + +// BEGIN: CALENDAR OBJECT FUNCTIONS + +/** + * This function creates the calendar inside the given parent. If _par is + * null than it creates a popup calendar inside the BODY element. If _par is + * an element, be it BODY, then it creates a non-popup calendar (still + * hidden). Some properties need to be set before calling this function. + */ +Calendar.prototype.create = function (_par) { + var parent = null; + if (! _par) { + // default parent is the document body, in which case we create + // a popup calendar. + parent = document.getElementsByTagName("body")[0]; + this.isPopup = true; + } else { + parent = _par; + this.isPopup = false; + } + this.date = this.dateStr ? new Date(this.dateStr) : new Date(); + + var table = Calendar.createElement("table"); + this.table = table; + table.cellSpacing = 0; + table.cellPadding = 0; + table.calendar = this; + Calendar.addEvent(table, "mousedown", Calendar.tableMouseDown); + + var div = Calendar.createElement("div"); + this.element = div; + div.className = "calendar"; + if (this.isPopup) { + div.style.position = "absolute"; + div.style.display = "none"; + } + div.appendChild(table); + + var thead = Calendar.createElement("thead", table); + var cell = null; + var row = null; + + var cal = this; + var hh = function (text, cs, navtype) { + cell = Calendar.createElement("td", row); + cell.colSpan = cs; + cell.className = "button"; + if (navtype != 0 && Math.abs(navtype) <= 2) + cell.className += " nav"; + Calendar._add_evs(cell); + cell.calendar = cal; + cell.navtype = navtype; + cell.innerHTML = "<div unselectable='on'>" + text + "</div>"; + return cell; + }; + + row = Calendar.createElement("tr", thead); + var title_length = 6; + (this.isPopup) && --title_length; + (this.weekNumbers) && ++title_length; + + hh("?", 1, 400).ttip = Calendar._TT["INFO"]; + this.title = hh("", title_length, 300); + this.title.className = "title"; + if (this.isPopup) { + this.title.ttip = Calendar._TT["DRAG_TO_MOVE"]; + this.title.style.cursor = "move"; + hh("&#x00d7;", 1, 200).ttip = Calendar._TT["CLOSE"]; + } + + row = Calendar.createElement("tr", thead); + row.className = "headrow"; + + this._nav_py = hh("&#x00ab;", 1, -2); + this._nav_py.ttip = Calendar._TT["PREV_YEAR"]; + + this._nav_pm = hh("&#x2039;", 1, -1); + this._nav_pm.ttip = Calendar._TT["PREV_MONTH"]; + + this._nav_now = hh(Calendar._TT["TODAY"], this.weekNumbers ? 4 : 3, 0); + this._nav_now.ttip = Calendar._TT["GO_TODAY"]; + + this._nav_nm = hh("&#x203a;", 1, 1); + this._nav_nm.ttip = Calendar._TT["NEXT_MONTH"]; + + this._nav_ny = hh("&#x00bb;", 1, 2); + this._nav_ny.ttip = Calendar._TT["NEXT_YEAR"]; + + // day names + row = Calendar.createElement("tr", thead); + row.className = "daynames"; + if (this.weekNumbers) { + cell = Calendar.createElement("td", row); + cell.className = "name wn"; + cell.innerHTML = Calendar._TT["WK"]; + } + for (var i = 7; i > 0; --i) { + cell = Calendar.createElement("td", row); + if (!i) { + cell.navtype = 100; + cell.calendar = this; + Calendar._add_evs(cell); + } + } + this.firstdayname = (this.weekNumbers) ? row.firstChild.nextSibling : row.firstChild; + this._displayWeekdays(); + + var tbody = Calendar.createElement("tbody", table); + this.tbody = tbody; + + for (i = 6; i > 0; --i) { + row = Calendar.createElement("tr", tbody); + if (this.weekNumbers) { + cell = Calendar.createElement("td", row); + } + for (var j = 7; j > 0; --j) { + cell = Calendar.createElement("td", row); + cell.calendar = this; + Calendar._add_evs(cell); + } + } + + if (this.showsTime) { + row = Calendar.createElement("tr", tbody); + row.className = "time"; + + cell = Calendar.createElement("td", row); + cell.className = "time"; + cell.colSpan = 2; + cell.innerHTML = Calendar._TT["TIME"] || "&nbsp;"; + + cell = Calendar.createElement("td", row); + cell.className = "time"; + cell.colSpan = this.weekNumbers ? 4 : 3; + + (function(){ + function makeTimePart(className, init, range_start, range_end) { + var part = Calendar.createElement("span", cell); + part.className = className; + part.innerHTML = init; + part.calendar = cal; + part.ttip = Calendar._TT["TIME_PART"]; + part.navtype = 50; + part._range = []; + if (typeof range_start != "number") + part._range = range_start; + else { + for (var i = range_start; i <= range_end; ++i) { + var txt; + if (i < 10 && range_end >= 10) txt = '0' + i; + else txt = '' + i; + part._range[part._range.length] = txt; + } + } + Calendar._add_evs(part); + return part; + }; + var hrs = cal.date.getHours(); + var mins = cal.date.getMinutes(); + var t12 = !cal.time24; + var pm = (hrs > 12); + if (t12 && pm) hrs -= 12; + var H = makeTimePart("hour", hrs, t12 ? 1 : 0, t12 ? 12 : 23); + var span = Calendar.createElement("span", cell); + span.innerHTML = ":"; + span.className = "colon"; + var M = makeTimePart("minute", mins, 0, 59); + var AP = null; + cell = Calendar.createElement("td", row); + cell.className = "time"; + cell.colSpan = 2; + if (t12) + AP = makeTimePart("ampm", pm ? "pm" : "am", ["am", "pm"]); + else + cell.innerHTML = "&nbsp;"; + + cal.onSetTime = function() { + var pm, hrs = this.date.getHours(), + mins = this.date.getMinutes(); + if (t12) { + pm = (hrs >= 12); + if (pm) hrs -= 12; + if (hrs == 0) hrs = 12; + AP.innerHTML = pm ? "pm" : "am"; + } + H.innerHTML = (hrs < 10) ? ("0" + hrs) : hrs; + M.innerHTML = (mins < 10) ? ("0" + mins) : mins; + }; + + cal.onUpdateTime = function() { + var date = this.date; + var h = parseInt(H.innerHTML, 10); + if (t12) { + if (/pm/i.test(AP.innerHTML) && h < 12) + h += 12; + else if (/am/i.test(AP.innerHTML) && h == 12) + h = 0; + } + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + date.setHours(h); + date.setMinutes(parseInt(M.innerHTML, 10)); + date.setFullYear(y); + date.setMonth(m); + date.setDate(d); + this.dateClicked = false; + this.callHandler(); + }; + })(); + } else { + this.onSetTime = this.onUpdateTime = function() {}; + } + + var tfoot = Calendar.createElement("tfoot", table); + + row = Calendar.createElement("tr", tfoot); + row.className = "footrow"; + + cell = hh(Calendar._TT["SEL_DATE"], this.weekNumbers ? 8 : 7, 300); + cell.className = "ttip"; + if (this.isPopup) { + cell.ttip = Calendar._TT["DRAG_TO_MOVE"]; + cell.style.cursor = "move"; + } + this.tooltips = cell; + + div = Calendar.createElement("div", this.element); + this.monthsCombo = div; + div.className = "combo"; + for (i = 0; i < Calendar._MN.length; ++i) { + var mn = Calendar.createElement("div"); + mn.className = Calendar.is_ie ? "label-IEfix" : "label"; + mn.month = i; + mn.innerHTML = Calendar._SMN[i]; + div.appendChild(mn); + } + + div = Calendar.createElement("div", this.element); + this.yearsCombo = div; + div.className = "combo"; + for (i = 12; i > 0; --i) { + var yr = Calendar.createElement("div"); + yr.className = Calendar.is_ie ? "label-IEfix" : "label"; + div.appendChild(yr); + } + + this._init(this.firstDayOfWeek, this.date); + parent.appendChild(this.element); +}; + +/** keyboard navigation, only for popup calendars */ +Calendar._keyEvent = function(ev) { + var cal = window._dynarch_popupCalendar; + if (!cal || cal.multiple) + return false; + (Calendar.is_ie) && (ev = window.event); + var act = (Calendar.is_ie || ev.type == "keypress"), + K = ev.keyCode; + if (ev.ctrlKey) { + switch (K) { + case 37: // KEY left + act && Calendar.cellClick(cal._nav_pm); + break; + case 38: // KEY up + act && Calendar.cellClick(cal._nav_py); + break; + case 39: // KEY right + act && Calendar.cellClick(cal._nav_nm); + break; + case 40: // KEY down + act && Calendar.cellClick(cal._nav_ny); + break; + default: + return false; + } + } else switch (K) { + case 32: // KEY space (now) + Calendar.cellClick(cal._nav_now); + break; + case 27: // KEY esc + act && cal.callCloseHandler(); + break; + case 37: // KEY left + case 38: // KEY up + case 39: // KEY right + case 40: // KEY down + if (act) { + var prev, x, y, ne, el, step; + prev = K == 37 || K == 38; + step = (K == 37 || K == 39) ? 1 : 7; + function setVars() { + el = cal.currentDateEl; + var p = el.pos; + x = p & 15; + y = p >> 4; + ne = cal.ar_days[y][x]; + };setVars(); + function prevMonth() { + var date = new Date(cal.date); + date.setDate(date.getDate() - step); + cal.setDate(date); + }; + function nextMonth() { + var date = new Date(cal.date); + date.setDate(date.getDate() + step); + cal.setDate(date); + }; + while (1) { + switch (K) { + case 37: // KEY left + if (--x >= 0) + ne = cal.ar_days[y][x]; + else { + x = 6; + K = 38; + continue; + } + break; + case 38: // KEY up + if (--y >= 0) + ne = cal.ar_days[y][x]; + else { + prevMonth(); + setVars(); + } + break; + case 39: // KEY right + if (++x < 7) + ne = cal.ar_days[y][x]; + else { + x = 0; + K = 40; + continue; + } + break; + case 40: // KEY down + if (++y < cal.ar_days.length) + ne = cal.ar_days[y][x]; + else { + nextMonth(); + setVars(); + } + break; + } + break; + } + if (ne) { + if (!ne.disabled) + Calendar.cellClick(ne); + else if (prev) + prevMonth(); + else + nextMonth(); + } + } + break; + case 13: // KEY enter + if (act) + Calendar.cellClick(cal.currentDateEl, ev); + break; + default: + return false; + } + return Calendar.stopEvent(ev); +}; + +/** + * (RE)Initializes the calendar to the given date and firstDayOfWeek + */ +Calendar.prototype._init = function (firstDayOfWeek, date) { + var today = new Date(), + TY = today.getFullYear(), + TM = today.getMonth(), + TD = today.getDate(); + this.table.style.visibility = "hidden"; + var year = date.getFullYear(); + if (year < this.minYear) { + year = this.minYear; + date.setFullYear(year); + } else if (year > this.maxYear) { + year = this.maxYear; + date.setFullYear(year); + } + this.firstDayOfWeek = firstDayOfWeek; + this.date = new Date(date); + var month = date.getMonth(); + var mday = date.getDate(); + var no_days = date.getMonthDays(); + + // calendar voodoo for computing the first day that would actually be + // displayed in the calendar, even if it's from the previous month. + // WARNING: this is magic. ;-) + date.setDate(1); + var day1 = (date.getDay() - this.firstDayOfWeek) % 7; + if (day1 < 0) + day1 += 7; + date.setDate(-day1); + date.setDate(date.getDate() + 1); + + var row = this.tbody.firstChild; + var MN = Calendar._SMN[month]; + var ar_days = this.ar_days = new Array(); + var weekend = Calendar._TT["WEEKEND"]; + var dates = this.multiple ? (this.datesCells = {}) : null; + for (var i = 0; i < 6; ++i, row = row.nextSibling) { + var cell = row.firstChild; + if (this.weekNumbers) { + cell.className = "day wn"; + cell.innerHTML = date.getWeekNumber(); + cell = cell.nextSibling; + } + row.className = "daysrow"; + var hasdays = false, iday, dpos = ar_days[i] = []; + for (var j = 0; j < 7; ++j, cell = cell.nextSibling, date.setDate(iday + 1)) { + iday = date.getDate(); + var wday = date.getDay(); + cell.className = "day"; + cell.pos = i << 4 | j; + dpos[j] = cell; + var current_month = (date.getMonth() == month); + if (!current_month) { + if (this.showsOtherMonths) { + cell.className += " othermonth"; + cell.otherMonth = true; + } else { + cell.className = "emptycell"; + cell.innerHTML = "&nbsp;"; + cell.disabled = true; + continue; + } + } else { + cell.otherMonth = false; + hasdays = true; + } + cell.disabled = false; + cell.innerHTML = this.getDateText ? this.getDateText(date, iday) : iday; + if (dates) + dates[date.print("%Y%m%d")] = cell; + if (this.getDateStatus) { + var status = this.getDateStatus(date, year, month, iday); + if (this.getDateToolTip) { + var toolTip = this.getDateToolTip(date, year, month, iday); + if (toolTip) + cell.title = toolTip; + } + if (status === true) { + cell.className += " disabled"; + cell.disabled = true; + } else { + if (/disabled/i.test(status)) + cell.disabled = true; + cell.className += " " + status; + } + } + if (!cell.disabled) { + cell.caldate = new Date(date); + cell.ttip = "_"; + if (!this.multiple && current_month + && iday == mday && this.hiliteToday) { + cell.className += " selected"; + this.currentDateEl = cell; + } + if (date.getFullYear() == TY && + date.getMonth() == TM && + iday == TD) { + cell.className += " today"; + cell.ttip += Calendar._TT["PART_TODAY"]; + } + if (weekend.indexOf(wday.toString()) != -1) + cell.className += cell.otherMonth ? " oweekend" : " weekend"; + } + } + if (!(hasdays || this.showsOtherMonths)) + row.className = "emptyrow"; + } + this.title.innerHTML = Calendar._MN[month] + ", " + year; + this.onSetTime(); + this.table.style.visibility = "visible"; + this._initMultipleDates(); + // PROFILE + // this.tooltips.innerHTML = "Generated in " + ((new Date()) - today) + " ms"; +}; + +Calendar.prototype._initMultipleDates = function() { + if (this.multiple) { + for (var i in this.multiple) { + var cell = this.datesCells[i]; + var d = this.multiple[i]; + if (!d) + continue; + if (cell) + cell.className += " selected"; + } + } +}; + +Calendar.prototype._toggleMultipleDate = function(date) { + if (this.multiple) { + var ds = date.print("%Y%m%d"); + var cell = this.datesCells[ds]; + if (cell) { + var d = this.multiple[ds]; + if (!d) { + Calendar.addClass(cell, "selected"); + this.multiple[ds] = date; + } else { + Calendar.removeClass(cell, "selected"); + delete this.multiple[ds]; + } + } + } +}; + +Calendar.prototype.setDateToolTipHandler = function (unaryFunction) { + this.getDateToolTip = unaryFunction; +}; + +/** + * Calls _init function above for going to a certain date (but only if the + * date is different than the currently selected one). + */ +Calendar.prototype.setDate = function (date) { + if (!date.equalsTo(this.date)) { + this._init(this.firstDayOfWeek, date); + } +}; + +/** + * Refreshes the calendar. Useful if the "disabledHandler" function is + * dynamic, meaning that the list of disabled date can change at runtime. + * Just * call this function if you think that the list of disabled dates + * should * change. + */ +Calendar.prototype.refresh = function () { + this._init(this.firstDayOfWeek, this.date); +}; + +/** Modifies the "firstDayOfWeek" parameter (pass 0 for Synday, 1 for Monday, etc.). */ +Calendar.prototype.setFirstDayOfWeek = function (firstDayOfWeek) { + this._init(firstDayOfWeek, this.date); + this._displayWeekdays(); +}; + +/** + * Allows customization of what dates are enabled. The "unaryFunction" + * parameter must be a function object that receives the date (as a JS Date + * object) and returns a boolean value. If the returned value is true then + * the passed date will be marked as disabled. + */ +Calendar.prototype.setDateStatusHandler = Calendar.prototype.setDisabledHandler = function (unaryFunction) { + this.getDateStatus = unaryFunction; +}; + +/** Customization of allowed year range for the calendar. */ +Calendar.prototype.setRange = function (a, z) { + this.minYear = a; + this.maxYear = z; +}; + +/** Calls the first user handler (selectedHandler). */ +Calendar.prototype.callHandler = function () { + if (this.onSelected) { + this.onSelected(this, this.date.print(this.dateFormat)); + } +}; + +/** Calls the second user handler (closeHandler). */ +Calendar.prototype.callCloseHandler = function () { + if (this.onClose) { + this.onClose(this); + } + this.hideShowCovered(); +}; + +/** Removes the calendar object from the DOM tree and destroys it. */ +Calendar.prototype.destroy = function () { + var el = this.element.parentNode; + el.removeChild(this.element); + Calendar._C = null; + window._dynarch_popupCalendar = null; +}; + +/** + * Moves the calendar element to a different section in the DOM tree (changes + * its parent). + */ +Calendar.prototype.reparent = function (new_parent) { + var el = this.element; + el.parentNode.removeChild(el); + new_parent.appendChild(el); +}; + +// This gets called when the user presses a mouse button anywhere in the +// document, if the calendar is shown. If the click was outside the open +// calendar this function closes it. +Calendar._checkCalendar = function(ev) { + var calendar = window._dynarch_popupCalendar; + if (!calendar) { + return false; + } + var el = Calendar.is_ie ? Calendar.getElement(ev) : Calendar.getTargetElement(ev); + for (; el != null && el != calendar.element; el = el.parentNode); + if (el == null) { + // calls closeHandler which should hide the calendar. + window._dynarch_popupCalendar.callCloseHandler(); + return Calendar.stopEvent(ev); + } +}; + +/** Shows the calendar. */ +Calendar.prototype.show = function () { + var rows = this.table.getElementsByTagName("tr"); + for (var i = rows.length; i > 0;) { + var row = rows[--i]; + Calendar.removeClass(row, "rowhilite"); + var cells = row.getElementsByTagName("td"); + for (var j = cells.length; j > 0;) { + var cell = cells[--j]; + Calendar.removeClass(cell, "hilite"); + Calendar.removeClass(cell, "active"); + } + } + this.element.style.display = "block"; + this.hidden = false; + if (this.isPopup) { + window._dynarch_popupCalendar = this; + Calendar.addEvent(document, "keydown", Calendar._keyEvent); + Calendar.addEvent(document, "keypress", Calendar._keyEvent); + Calendar.addEvent(document, "mousedown", Calendar._checkCalendar); + } + this.hideShowCovered(); +}; + +/** + * Hides the calendar. Also removes any "hilite" from the class of any TD + * element. + */ +Calendar.prototype.hide = function () { + if (this.isPopup) { + Calendar.removeEvent(document, "keydown", Calendar._keyEvent); + Calendar.removeEvent(document, "keypress", Calendar._keyEvent); + Calendar.removeEvent(document, "mousedown", Calendar._checkCalendar); + } + this.element.style.display = "none"; + this.hidden = true; + this.hideShowCovered(); +}; + +/** + * Shows the calendar at a given absolute position (beware that, depending on + * the calendar element style -- position property -- this might be relative + * to the parent's containing rectangle). + */ +Calendar.prototype.showAt = function (x, y) { + var s = this.element.style; + s.left = x + "px"; + s.top = y + "px"; + this.show(); +}; + +/** Shows the calendar near a given element. */ +Calendar.prototype.showAtElement = function (el, opts) { + var self = this; + var p = Calendar.getAbsolutePos(el); + if (!opts || typeof opts != "string") { + this.showAt(p.x, p.y + el.offsetHeight); + return true; + } + function fixPosition(box) { + if (box.x < 0) + box.x = 0; + if (box.y < 0) + box.y = 0; + var cp = document.createElement("div"); + var s = cp.style; + s.position = "absolute"; + s.right = s.bottom = s.width = s.height = "0px"; + document.body.appendChild(cp); + var br = Calendar.getAbsolutePos(cp); + document.body.removeChild(cp); + if (Calendar.is_ie) { + br.y += document.body.scrollTop; + br.x += document.body.scrollLeft; + } else { + br.y += window.scrollY; + br.x += window.scrollX; + } + var tmp = box.x + box.width - br.x; + if (tmp > 0) box.x -= tmp; + tmp = box.y + box.height - br.y; + if (tmp > 0) box.y -= tmp; + }; + this.element.style.display = "block"; + Calendar.continuation_for_the_fucking_khtml_browser = function() { + var w = self.element.offsetWidth; + var h = self.element.offsetHeight; + self.element.style.display = "none"; + var valign = opts.substr(0, 1); + var halign = "l"; + if (opts.length > 1) { + halign = opts.substr(1, 1); + } + // vertical alignment + switch (valign) { + case "T": p.y -= h; break; + case "B": p.y += el.offsetHeight; break; + case "C": p.y += (el.offsetHeight - h) / 2; break; + case "t": p.y += el.offsetHeight - h; break; + case "b": break; // already there + } + // horizontal alignment + switch (halign) { + case "L": p.x -= w; break; + case "R": p.x += el.offsetWidth; break; + case "C": p.x += (el.offsetWidth - w) / 2; break; + case "l": p.x += el.offsetWidth - w; break; + case "r": break; // already there + } + p.width = w; + p.height = h + 40; + self.monthsCombo.style.display = "none"; + fixPosition(p); + self.showAt(p.x, p.y); + }; + if (Calendar.is_khtml) + setTimeout("Calendar.continuation_for_the_fucking_khtml_browser()", 10); + else + Calendar.continuation_for_the_fucking_khtml_browser(); +}; + +/** Customizes the date format. */ +Calendar.prototype.setDateFormat = function (str) { + this.dateFormat = str; +}; + +/** Customizes the tooltip date format. */ +Calendar.prototype.setTtDateFormat = function (str) { + this.ttDateFormat = str; +}; + +/** + * Tries to identify the date represented in a string. If successful it also + * calls this.setDate which moves the calendar to the given date. + */ +Calendar.prototype.parseDate = function(str, fmt) { + if (!fmt) + fmt = this.dateFormat; + this.setDate(Date.parseDate(str, fmt)); +}; + +Calendar.prototype.hideShowCovered = function () { + if (!Calendar.is_ie && !Calendar.is_opera) + return; + function getVisib(obj){ + var value = obj.style.visibility; + if (!value) { + if (document.defaultView && typeof (document.defaultView.getComputedStyle) == "function") { // Gecko, W3C + if (!Calendar.is_khtml) + value = document.defaultView. + getComputedStyle(obj, "").getPropertyValue("visibility"); + else + value = ''; + } else if (obj.currentStyle) { // IE + value = obj.currentStyle.visibility; + } else + value = ''; + } + return value; + }; + + var tags = new Array("applet", "iframe", "select"); + var el = this.element; + + var p = Calendar.getAbsolutePos(el); + var EX1 = p.x; + var EX2 = el.offsetWidth + EX1; + var EY1 = p.y; + var EY2 = el.offsetHeight + EY1; + + for (var k = tags.length; k > 0; ) { + var ar = document.getElementsByTagName(tags[--k]); + var cc = null; + + for (var i = ar.length; i > 0;) { + cc = ar[--i]; + + p = Calendar.getAbsolutePos(cc); + var CX1 = p.x; + var CX2 = cc.offsetWidth + CX1; + var CY1 = p.y; + var CY2 = cc.offsetHeight + CY1; + + if (this.hidden || (CX1 > EX2) || (CX2 < EX1) || (CY1 > EY2) || (CY2 < EY1)) { + if (!cc.__msh_save_visibility) { + cc.__msh_save_visibility = getVisib(cc); + } + cc.style.visibility = cc.__msh_save_visibility; + } else { + if (!cc.__msh_save_visibility) { + cc.__msh_save_visibility = getVisib(cc); + } + cc.style.visibility = "hidden"; + } + } + } +}; + +/** Internal function; it displays the bar with the names of the weekday. */ +Calendar.prototype._displayWeekdays = function () { + var fdow = this.firstDayOfWeek; + var cell = this.firstdayname; + var weekend = Calendar._TT["WEEKEND"]; + for (var i = 0; i < 7; ++i) { + cell.className = "day name"; + var realday = (i + fdow) % 7; + if (i) { + cell.ttip = Calendar._TT["DAY_FIRST"].replace("%s", Calendar._DN[realday]); + cell.navtype = 100; + cell.calendar = this; + cell.fdow = realday; + Calendar._add_evs(cell); + } + if (weekend.indexOf(realday.toString()) != -1) { + Calendar.addClass(cell, "weekend"); + } + cell.innerHTML = Calendar._SDN[(i + fdow) % 7]; + cell = cell.nextSibling; + } +}; + +/** Internal function. Hides all combo boxes that might be displayed. */ +Calendar.prototype._hideCombos = function () { + this.monthsCombo.style.display = "none"; + this.yearsCombo.style.display = "none"; +}; + +/** Internal function. Starts dragging the element. */ +Calendar.prototype._dragStart = function (ev) { + if (this.dragging) { + return; + } + this.dragging = true; + var posX; + var posY; + if (Calendar.is_ie) { + posY = window.event.clientY + document.body.scrollTop; + posX = window.event.clientX + document.body.scrollLeft; + } else { + posY = ev.clientY + window.scrollY; + posX = ev.clientX + window.scrollX; + } + var st = this.element.style; + this.xOffs = posX - parseInt(st.left); + this.yOffs = posY - parseInt(st.top); + with (Calendar) { + addEvent(document, "mousemove", calDragIt); + addEvent(document, "mouseup", calDragEnd); + } +}; + +// BEGIN: DATE OBJECT PATCHES + +/** Adds the number of days array to the Date object. */ +Date._MD = new Array(31,28,31,30,31,30,31,31,30,31,30,31); + +/** Constants used for time computations */ +Date.SECOND = 1000 /* milliseconds */; +Date.MINUTE = 60 * Date.SECOND; +Date.HOUR = 60 * Date.MINUTE; +Date.DAY = 24 * Date.HOUR; +Date.WEEK = 7 * Date.DAY; + +Date.parseDate = function(str, fmt) { + var today = new Date(); + var y = 0; + var m = -1; + var d = 0; + var a = str.split(/\W+/); + var b = fmt.match(/%./g); + var i = 0, j = 0; + var hr = 0; + var min = 0; + for (i = 0; i < a.length; ++i) { + if (!a[i]) + continue; + switch (b[i]) { + case "%d": + case "%e": + d = parseInt(a[i], 10); + break; + + case "%m": + m = parseInt(a[i], 10) - 1; + break; + + case "%Y": + case "%y": + y = parseInt(a[i], 10); + (y < 100) && (y += (y > 29) ? 1900 : 2000); + break; + + case "%b": + case "%B": + for (j = 0; j < 12; ++j) { + if (Calendar._MN[j].substr(0, a[i].length).toLowerCase() == a[i].toLowerCase()) { m = j; break; } + } + break; + + case "%H": + case "%I": + case "%k": + case "%l": + hr = parseInt(a[i], 10); + break; + + case "%P": + case "%p": + if (/pm/i.test(a[i]) && hr < 12) + hr += 12; + else if (/am/i.test(a[i]) && hr >= 12) + hr -= 12; + break; + + case "%M": + min = parseInt(a[i], 10); + break; + } + } + if (isNaN(y)) y = today.getFullYear(); + if (isNaN(m)) m = today.getMonth(); + if (isNaN(d)) d = today.getDate(); + if (isNaN(hr)) hr = today.getHours(); + if (isNaN(min)) min = today.getMinutes(); + if (y != 0 && m != -1 && d != 0) + return new Date(y, m, d, hr, min, 0); + y = 0; m = -1; d = 0; + for (i = 0; i < a.length; ++i) { + if (a[i].search(/[a-zA-Z]+/) != -1) { + var t = -1; + for (j = 0; j < 12; ++j) { + if (Calendar._MN[j].substr(0, a[i].length).toLowerCase() == a[i].toLowerCase()) { t = j; break; } + } + if (t != -1) { + if (m != -1) { + d = m+1; + } + m = t; + } + } else if (parseInt(a[i], 10) <= 12 && m == -1) { + m = a[i]-1; + } else if (parseInt(a[i], 10) > 31 && y == 0) { + y = parseInt(a[i], 10); + (y < 100) && (y += (y > 29) ? 1900 : 2000); + } else if (d == 0) { + d = a[i]; + } + } + if (y == 0) + y = today.getFullYear(); + if (m != -1 && d != 0) + return new Date(y, m, d, hr, min, 0); + return today; +}; + +/** Returns the number of days in the current month */ +Date.prototype.getMonthDays = function(month) { + var year = this.getFullYear(); + if (typeof month == "undefined") { + month = this.getMonth(); + } + if (((0 == (year%4)) && ( (0 != (year%100)) || (0 == (year%400)))) && month == 1) { + return 29; + } else { + return Date._MD[month]; + } +}; + +/** Returns the number of day in the year. */ +Date.prototype.getDayOfYear = function() { + var now = new Date(this.getFullYear(), this.getMonth(), this.getDate(), 0, 0, 0); + var then = new Date(this.getFullYear(), 0, 0, 0, 0, 0); + var time = now - then; + return Math.floor(time / Date.DAY); +}; + +/** Returns the number of the week in year, as defined in ISO 8601. */ +Date.prototype.getWeekNumber = function() { + var d = new Date(this.getFullYear(), this.getMonth(), this.getDate(), 0, 0, 0); + var DoW = d.getDay(); + d.setDate(d.getDate() - (DoW + 6) % 7 + 3); // Nearest Thu + var ms = d.valueOf(); // GMT + d.setMonth(0); + d.setDate(4); // Thu in Week 1 + return Math.round((ms - d.valueOf()) / (7 * 864e5)) + 1; +}; + +/** Checks date and time equality */ +Date.prototype.equalsTo = function(date) { + return ((this.getFullYear() == date.getFullYear()) && + (this.getMonth() == date.getMonth()) && + (this.getDate() == date.getDate()) && + (this.getHours() == date.getHours()) && + (this.getMinutes() == date.getMinutes())); +}; + +/** Set only the year, month, date parts (keep existing time) */ +Date.prototype.setDateOnly = function(date) { + var tmp = new Date(date); + this.setDate(1); + this.setFullYear(tmp.getFullYear()); + this.setMonth(tmp.getMonth()); + this.setDate(tmp.getDate()); +}; + +/** Prints the date in a string according to the given format. */ +Date.prototype.print = function (str) { + var m = this.getMonth(); + var d = this.getDate(); + var y = this.getFullYear(); + var wn = this.getWeekNumber(); + var w = this.getDay(); + var s = {}; + var hr = this.getHours(); + var pm = (hr >= 12); + var ir = (pm) ? (hr - 12) : hr; + var dy = this.getDayOfYear(); + if (ir == 0) + ir = 12; + var min = this.getMinutes(); + var sec = this.getSeconds(); + s["%a"] = Calendar._SDN[w]; // abbreviated weekday name [FIXME: I18N] + s["%A"] = Calendar._DN[w]; // full weekday name + s["%b"] = Calendar._SMN[m]; // abbreviated month name [FIXME: I18N] + s["%B"] = Calendar._MN[m]; // full month name + // FIXME: %c : preferred date and time representation for the current locale + s["%C"] = 1 + Math.floor(y / 100); // the century number + s["%d"] = (d < 10) ? ("0" + d) : d; // the day of the month (range 01 to 31) + s["%e"] = d; // the day of the month (range 1 to 31) + // FIXME: %D : american date style: %m/%d/%y + // FIXME: %E, %F, %G, %g, %h (man strftime) + s["%H"] = (hr < 10) ? ("0" + hr) : hr; // hour, range 00 to 23 (24h format) + s["%I"] = (ir < 10) ? ("0" + ir) : ir; // hour, range 01 to 12 (12h format) + s["%j"] = (dy < 100) ? ((dy < 10) ? ("00" + dy) : ("0" + dy)) : dy; // day of the year (range 001 to 366) + s["%k"] = hr; // hour, range 0 to 23 (24h format) + s["%l"] = ir; // hour, range 1 to 12 (12h format) + s["%m"] = (m < 9) ? ("0" + (1+m)) : (1+m); // month, range 01 to 12 + s["%M"] = (min < 10) ? ("0" + min) : min; // minute, range 00 to 59 + s["%n"] = "\n"; // a newline character + s["%p"] = pm ? "PM" : "AM"; + s["%P"] = pm ? "pm" : "am"; + // FIXME: %r : the time in am/pm notation %I:%M:%S %p + // FIXME: %R : the time in 24-hour notation %H:%M + s["%s"] = Math.floor(this.getTime() / 1000); + s["%S"] = (sec < 10) ? ("0" + sec) : sec; // seconds, range 00 to 59 + s["%t"] = "\t"; // a tab character + // FIXME: %T : the time in 24-hour notation (%H:%M:%S) + s["%U"] = s["%W"] = s["%V"] = (wn < 10) ? ("0" + wn) : wn; + s["%u"] = w + 1; // the day of the week (range 1 to 7, 1 = MON) + s["%w"] = w; // the day of the week (range 0 to 6, 0 = SUN) + // FIXME: %x : preferred date representation for the current locale without the time + // FIXME: %X : preferred time representation for the current locale without the date + s["%y"] = ('' + y).substr(2, 2); // year without the century (range 00 to 99) + s["%Y"] = y; // year with the century + s["%%"] = "%"; // a literal '%' character + + var re = /%./g; + if (!Calendar.is_ie5 && !Calendar.is_khtml) + return str.replace(re, function (par) { return s[par] || par; }); + + var a = str.match(re); + for (var i = 0; i < a.length; i++) { + var tmp = s[a[i]]; + if (tmp) { + re = new RegExp(a[i], 'g'); + str = str.replace(re, tmp); + } + } + + return str; +}; + +Date.prototype.__msh_oldSetFullYear = Date.prototype.setFullYear; +Date.prototype.setFullYear = function(y) { + var d = new Date(this); + d.__msh_oldSetFullYear(y); + if (d.getMonth() != this.getMonth()) + this.setDate(28); + this.__msh_oldSetFullYear(y); +}; + +// END: DATE OBJECT PATCHES + + +// global object that remembers the calendar +window._dynarch_popupCalendar = null; diff --git a/app/webroot/js/calendar/lang/calendar-bg.js b/app/webroot/js/calendar/lang/calendar-bg.js new file mode 100644 index 0000000..edc870e --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-bg.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar BG language +// Author: Nikolay Solakov, <thoranga@gmail.com> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Неделя", + "Понеделник", + "Вторник", + "Сряда", + "Четвъртък", + "Петък", + "Събота", + "Неделя"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Нед", + "Пон", + "Вто", + "Сря", + "Чет", + "Пет", + "Съб", + "Нед"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("Януари", + "Февруари", + "Март", + "Април", + "Май", + "Юни", + "Юли", + "Август", + "Септември", + "Октомври", + "Ноември", + "Декември"); + +// short month names +Calendar._SMN = new Array +("Яну", + "Фев", + "Мар", + "Апр", + "Май", + "Юни", + "Юли", + "Авг", + "Сеп", + "Окт", + "Ное", + "Дек"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "За календара"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Избор на дата:\n" + +"- Използвайте \xab, \xbb за избор на година\n" + +"- Използвайте " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " за избор на месец\n" + +"- Задръжте натиснат бутона за списък с години/месеци."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Избор на час:\n" + +"- Кликнете на числата от часа за да ги увеличите\n" + +"- или Shift-click за намаляването им\n" + +"- или кликнете и влачете за по-бърза промяна."; + +Calendar._TT["PREV_YEAR"] = "Предишна година (задръжте за списък)"; +Calendar._TT["PREV_MONTH"] = "Предишен месец (задръжте за списък)"; +Calendar._TT["GO_TODAY"] = "Днешна дата"; +Calendar._TT["NEXT_MONTH"] = "Следващ месец (задръжте за списък)"; +Calendar._TT["NEXT_YEAR"] = "Следваща година (задръжте за списък)"; +Calendar._TT["SEL_DATE"] = "Избор на дата"; +Calendar._TT["DRAG_TO_MOVE"] = "Дръпнете за преместване"; +Calendar._TT["PART_TODAY"] = " (днес)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Седмицата започва с %s"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Затвори"; +Calendar._TT["TODAY"] = "Днес"; +Calendar._TT["TIME_PART"] = "(Shift-)Click или влачене за промяна на стойност"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "седм"; +Calendar._TT["TIME"] = "Час:"; diff --git a/app/webroot/js/calendar/lang/calendar-ca.js b/app/webroot/js/calendar/lang/calendar-ca.js new file mode 100644 index 0000000..303f21d --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-ca.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, <mihai_bazon@yahoo.com> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Diumenge", + "Dilluns", + "Dimarts", + "Dimecres", + "Dijous", + "Divendres", + "Dissabte", + "Diumenge"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("dg", + "dl", + "dt", + "dc", + "dj", + "dv", + "ds", + "dg"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 0; + +// full month names +Calendar._MN = new Array +("Gener", + "Febrer", + "Març", + "Abril", + "Maig", + "Juny", + "Juliol", + "Agost", + "Setembre", + "Octubre", + "Novembre", + "Desembre"); + +// short month names +Calendar._SMN = new Array +("Gen", + "Feb", + "Mar", + "Abr", + "Mai", + "Jun", + "Jul", + "Ago", + "Set", + "Oct", + "Nov", + "Des"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "Quant al calendari"; + +Calendar._TT["ABOUT"] = +"Selector DHTML de data/hora\n" + +"(c) dynarch.com 2002-2005 / Autor: Mihai Bazon\n" + // don't translate this this ;-) +"Per a aconseguir l'última versió visiteu: http://www.dynarch.com/projects/calendar/\n" + +"Distribuït sota la llicència GNU LGPL. Vegeu http://gnu.org/licenses/lgpl.html per a més detalls." + +"\n\n" + +"Selecció de la data:\n" + +"- Utilitzeu els botons \xab, \xbb per a seleccionar l'any\n" + +"- Utilitzeu els botons " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " per a selecciona el mes\n" + +"- Mantingueu premut el botó del ratolí sobre qualsevol d'aquests botons per a uns selecció més ràpida."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Selecció de l'hora:\n" + +"- Feu clic en qualsevol part de l'hora per a incrementar-la\n" + +"- o premeu majúscules per a disminuir-la\n" + +"- o feu clic i arrossegueu per a una selecció més ràpida."; + +Calendar._TT["PREV_YEAR"] = "Any anterior (mantenir per menú)"; +Calendar._TT["PREV_MONTH"] = "Mes anterior (mantenir per menú)"; +Calendar._TT["GO_TODAY"] = "Anar a avui"; +Calendar._TT["NEXT_MONTH"] = "Mes següent (mantenir per menú)"; +Calendar._TT["NEXT_YEAR"] = "Any següent (mantenir per menú)"; +Calendar._TT["SEL_DATE"] = "Sel·lecciona data"; +Calendar._TT["DRAG_TO_MOVE"] = "Arrossega per a moure"; +Calendar._TT["PART_TODAY"] = " (avui)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Primer mostra el %s"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Tanca"; +Calendar._TT["TODAY"] = "Avui"; +Calendar._TT["TIME_PART"] = "(Majúscules-)Feu clic o arrossegueu per a canviar el valor"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%d-%m-%Y"; +Calendar._TT["TT_DATE_FORMAT"] = "%A, %e de %B de %Y"; + +Calendar._TT["WK"] = "set"; +Calendar._TT["TIME"] = "Hora:"; diff --git a/app/webroot/js/calendar/lang/calendar-cs.js b/app/webroot/js/calendar/lang/calendar-cs.js new file mode 100644 index 0000000..406ac66 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-cs.js @@ -0,0 +1,69 @@ +/* + calendar-cs-win.js + language: Czech + encoding: windows-1250 + author: Lubos Jerabek (xnet@seznam.cz) + Jan Uhlir (espinosa@centrum.cz) +*/ + +// ** I18N +Calendar._DN = new Array('Neděle','Pondělí','Úterý','Středa','Čtvrtek','Pátek','Sobota','Neděle'); +Calendar._SDN = new Array('Ne','Po','Út','St','Čt','Pá','So','Ne'); +Calendar._MN = new Array('Leden','Únor','Březen','Duben','Květen','Červen','Červenec','Srpen','Září','Říjen','Listopad','Prosinec'); +Calendar._SMN = new Array('Led','Úno','Bře','Dub','Kvě','Črv','Čvc','Srp','Zář','Říj','Lis','Pro'); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "O komponentě kalendář"; +Calendar._TT["TOGGLE"] = "Změna prvního dne v týdnu"; +Calendar._TT["PREV_YEAR"] = "Předchozí rok (přidrž pro menu)"; +Calendar._TT["PREV_MONTH"] = "Předchozí měsíc (přidrž pro menu)"; +Calendar._TT["GO_TODAY"] = "Dnešní datum"; +Calendar._TT["NEXT_MONTH"] = "Další měsíc (přidrž pro menu)"; +Calendar._TT["NEXT_YEAR"] = "Další rok (přidrž pro menu)"; +Calendar._TT["SEL_DATE"] = "Vyber datum"; +Calendar._TT["DRAG_TO_MOVE"] = "Chyť a táhni, pro přesun"; +Calendar._TT["PART_TODAY"] = " (dnes)"; +Calendar._TT["MON_FIRST"] = "Ukaž jako první Pondělí"; +//Calendar._TT["SUN_FIRST"] = "Ukaž jako první Neděli"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Výběr datumu:\n" + +"- Use the \xab, \xbb buttons to select year\n" + +"- Použijte tlačítka " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " k výběru měsíce\n" + +"- Podržte tlačítko myši na jakémkoliv z těch tlačítek pro rychlejší výběr."; + +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Výběr času:\n" + +"- Klikněte na jakoukoliv z částí výběru času pro zvýšení.\n" + +"- nebo Shift-click pro snížení\n" + +"- nebo klikněte a táhněte pro rychlejší výběr."; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Zobraz %s první"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Zavřít"; +Calendar._TT["TODAY"] = "Dnes"; +Calendar._TT["TIME_PART"] = "(Shift-)Klikni nebo táhni pro změnu hodnoty"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "d.m.yy"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "wk"; +Calendar._TT["TIME"] = "Čas:"; diff --git a/app/webroot/js/calendar/lang/calendar-da.js b/app/webroot/js/calendar/lang/calendar-da.js new file mode 100644 index 0000000..2cba5f6 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-da.js @@ -0,0 +1,128 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, <mihai_bazon@yahoo.com> +// Encoding: any +// Translater: Mads N. Vestergaard <mnv@coolsms.dk> +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Søndag", + "Mandag", + "Tirsdag", + "Onsdag", + "Torsdag", + "Fredag", + "Lørdag", + "Søndag"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Søn", + "Man", + "Tir", + "Ons", + "Tor", + "Fre", + "Lør", + "Søn"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("Januar", + "Februar", + "Marts", + "April", + "Maj", + "Juni", + "Juli", + "August", + "September", + "Oktober", + "November", + "December"); + +// short month names +Calendar._SMN = new Array +("Jan", + "Feb", + "Mar", + "Apr", + "Maj", + "Jun", + "Jul", + "Aug", + "Sep", + "Okt", + "Nov", + "Dec"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "Om denne kalender"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For seneste version, besøg: http://www.dynarch.com/projects/calendar/\n" + +"Distribueret under GNU LGPL. Se http://gnu.org/licenses/lgpl.html for detaljer." + +"\n\n" + +"Dato valg:\n" + +"- Benyt \xab, \xbb tasterne til at vælge år\n" + +"- Benyt " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " tasterne til at vælge måned\n" + +"- Hold muse tasten inde på punkterne for at vælge hurtigere."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Tids valg:\n" + +"- Klik på en af tidsramerne for at forhøje det\n" + +"- eller Shift-klik for at mindske det\n" + +"- eller klik og træk for hurtigere valg."; + +Calendar._TT["PREV_YEAR"] = "Forrige år (hold for menu)"; +Calendar._TT["PREV_MONTH"] = "Forrige måned (hold for menu)"; +Calendar._TT["GO_TODAY"] = "Gå til idag"; +Calendar._TT["NEXT_MONTH"] = "Næste måned (hold for menu)"; +Calendar._TT["NEXT_YEAR"] = "Næste år (hold for menu)"; +Calendar._TT["SEL_DATE"] = "Vælg dato"; +Calendar._TT["DRAG_TO_MOVE"] = "Træk for at flytte"; +Calendar._TT["PART_TODAY"] = " (idag)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Vis %s først"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "6,7"; + +Calendar._TT["CLOSE"] = "Luk"; +Calendar._TT["TODAY"] = "Idag"; +Calendar._TT["TIME_PART"] = "(Shift-)Klik eller træk for at ændre værdi"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "uge"; +Calendar._TT["TIME"] = "Tid:"; diff --git a/app/webroot/js/calendar/lang/calendar-de.js b/app/webroot/js/calendar/lang/calendar-de.js new file mode 100644 index 0000000..c320699 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-de.js @@ -0,0 +1,128 @@ +// ** I18N + +// Calendar DE language +// Author: Jack (tR), <jack@jtr.de> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Sonntag", + "Montag", + "Dienstag", + "Mittwoch", + "Donnerstag", + "Freitag", + "Samstag", + "Sonntag"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// short day names +Calendar._SDN = new Array +("So", + "Mo", + "Di", + "Mi", + "Do", + "Fr", + "Sa", + "So"); + +// full month names +Calendar._MN = new Array +("Januar", + "Februar", + "M\u00e4rz", + "April", + "Mai", + "Juni", + "Juli", + "August", + "September", + "Oktober", + "November", + "Dezember"); + +// short month names +Calendar._SMN = new Array +("Jan", + "Feb", + "M\u00e4r", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Okt", + "Nov", + "Dez"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "\u00DCber dieses Kalendarmodul"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Datum ausw\u00e4hlen:\n" + +"- Benutzen Sie die \xab, \xbb Buttons um das Jahr zu w\u00e4hlen\n" + +"- Benutzen Sie die " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " Buttons um den Monat zu w\u00e4hlen\n" + +"- F\u00fcr eine Schnellauswahl halten Sie die Maustaste \u00fcber diesen Buttons fest."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Zeit ausw\u00e4hlen:\n" + +"- Klicken Sie auf die Teile der Uhrzeit, um diese zu erh\u00F6hen\n" + +"- oder klicken Sie mit festgehaltener Shift-Taste um diese zu verringern\n" + +"- oder klicken und festhalten f\u00fcr Schnellauswahl."; + +Calendar._TT["TOGGLE"] = "Ersten Tag der Woche w\u00e4hlen"; +Calendar._TT["PREV_YEAR"] = "Voriges Jahr (Festhalten f\u00fcr Schnellauswahl)"; +Calendar._TT["PREV_MONTH"] = "Voriger Monat (Festhalten f\u00fcr Schnellauswahl)"; +Calendar._TT["GO_TODAY"] = "Heute ausw\u00e4hlen"; +Calendar._TT["NEXT_MONTH"] = "N\u00e4chst. Monat (Festhalten f\u00fcr Schnellauswahl)"; +Calendar._TT["NEXT_YEAR"] = "N\u00e4chst. Jahr (Festhalten f\u00fcr Schnellauswahl)"; +Calendar._TT["SEL_DATE"] = "Datum ausw\u00e4hlen"; +Calendar._TT["DRAG_TO_MOVE"] = "Zum Bewegen festhalten"; +Calendar._TT["PART_TODAY"] = " (Heute)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Woche beginnt mit %s "; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Schlie\u00dfen"; +Calendar._TT["TODAY"] = "Heute"; +Calendar._TT["TIME_PART"] = "(Shift-)Klick oder Festhalten und Ziehen um den Wert zu \u00e4ndern"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%d.%m.%Y"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "wk"; +Calendar._TT["TIME"] = "Zeit:"; diff --git a/app/webroot/js/calendar/lang/calendar-en.js b/app/webroot/js/calendar/lang/calendar-en.js new file mode 100644 index 0000000..0dbde79 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-en.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, <mihai_bazon@yahoo.com> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 0; + +// full month names +Calendar._MN = new Array +("January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December"); + +// short month names +Calendar._SMN = new Array +("Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "About the calendar"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Date selection:\n" + +"- Use the \xab, \xbb buttons to select year\n" + +"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" + +"- Hold mouse button on any of the above buttons for faster selection."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Time selection:\n" + +"- Click on any of the time parts to increase it\n" + +"- or Shift-click to decrease it\n" + +"- or click and drag for faster selection."; + +Calendar._TT["PREV_YEAR"] = "Prev. year (hold for menu)"; +Calendar._TT["PREV_MONTH"] = "Prev. month (hold for menu)"; +Calendar._TT["GO_TODAY"] = "Go Today"; +Calendar._TT["NEXT_MONTH"] = "Next month (hold for menu)"; +Calendar._TT["NEXT_YEAR"] = "Next year (hold for menu)"; +Calendar._TT["SEL_DATE"] = "Select date"; +Calendar._TT["DRAG_TO_MOVE"] = "Drag to move"; +Calendar._TT["PART_TODAY"] = " (today)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Display %s first"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Close"; +Calendar._TT["TODAY"] = "Today"; +Calendar._TT["TIME_PART"] = "(Shift-)Click or drag to change value"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "wk"; +Calendar._TT["TIME"] = "Time:"; diff --git a/app/webroot/js/calendar/lang/calendar-es.js b/app/webroot/js/calendar/lang/calendar-es.js new file mode 100644 index 0000000..11d0b53 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-es.js @@ -0,0 +1,129 @@ +// ** I18N + +// Calendar ES (spanish) language +// Author: Mihai Bazon, <mihai_bazon@yahoo.com> +// Updater: Servilio Afre Puentes <servilios@yahoo.com> +// Updated: 2004-06-03 +// Encoding: utf-8 +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Domingo", + "Lunes", + "Martes", + "Miércoles", + "Jueves", + "Viernes", + "Sábado", + "Domingo"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Dom", + "Lun", + "Mar", + "Mié", + "Jue", + "Vie", + "Sáb", + "Dom"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("Enero", + "Febrero", + "Marzo", + "Abril", + "Mayo", + "Junio", + "Julio", + "Agosto", + "Septiembre", + "Octubre", + "Noviembre", + "Diciembre"); + +// short month names +Calendar._SMN = new Array +("Ene", + "Feb", + "Mar", + "Abr", + "May", + "Jun", + "Jul", + "Ago", + "Sep", + "Oct", + "Nov", + "Dic"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "Acerca del calendario"; + +Calendar._TT["ABOUT"] = +"Selector DHTML de Fecha/Hora\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"Para conseguir la última versión visite: http://www.dynarch.com/projects/calendar/\n" + +"Distribuido bajo licencia GNU LGPL. Visite http://gnu.org/licenses/lgpl.html para más detalles." + +"\n\n" + +"Selección de fecha:\n" + +"- Use los botones \xab, \xbb para seleccionar el año\n" + +"- Use los botones " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " para seleccionar el mes\n" + +"- Mantenga pulsado el ratón en cualquiera de estos botones para una selección rápida."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Selección de hora:\n" + +"- Pulse en cualquiera de las partes de la hora para incrementarla\n" + +"- o pulse las mayúsculas mientras hace clic para decrementarla\n" + +"- o haga clic y arrastre el ratón para una selección más rápida."; + +Calendar._TT["PREV_YEAR"] = "Año anterior (mantener para menú)"; +Calendar._TT["PREV_MONTH"] = "Mes anterior (mantener para menú)"; +Calendar._TT["GO_TODAY"] = "Ir a hoy"; +Calendar._TT["NEXT_MONTH"] = "Mes siguiente (mantener para menú)"; +Calendar._TT["NEXT_YEAR"] = "Año siguiente (mantener para menú)"; +Calendar._TT["SEL_DATE"] = "Seleccionar fecha"; +Calendar._TT["DRAG_TO_MOVE"] = "Arrastrar para mover"; +Calendar._TT["PART_TODAY"] = " (hoy)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Hacer %s primer día de la semana"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Cerrar"; +Calendar._TT["TODAY"] = "Hoy"; +Calendar._TT["TIME_PART"] = "(Mayúscula-)Clic o arrastre para cambiar valor"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%d/%m/%Y"; +Calendar._TT["TT_DATE_FORMAT"] = "%A, %e de %B de %Y"; + +Calendar._TT["WK"] = "sem"; +Calendar._TT["TIME"] = "Hora:"; diff --git a/app/webroot/js/calendar/lang/calendar-fi.js b/app/webroot/js/calendar/lang/calendar-fi.js new file mode 100644 index 0000000..1e65eee --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-fi.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar FI language +// Author: Antti Perkiömäki <antti.perkiomaki@gmail.com> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Sunnuntai", + "Maanantai", + "Tiistai", + "Keskiviikko", + "Torstai", + "Perjantai", + "Lauantai", + "Sunnuntai"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Su", + "Ma", + "Ti", + "Ke", + "To", + "Pe", + "La", + "Su"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("Tammikuu", + "Helmikuu", + "Maaliskuu", + "Huhtikuu", + "Toukokuu", + "Kesäkuu", + "Heinäkuu", + "Elokuu", + "Syyskuu", + "Lokakuu", + "Marraskuu", + "Joulukuu"); + +// short month names +Calendar._SMN = new Array +("Tammi", + "Helmi", + "Maalis", + "Huhti", + "Touko", + "Kesä", + "Heinä", + "Elo", + "Syys", + "Loka", + "Marras", + "Dec"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "Tietoa kalenterista"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Tekijä: Mihai Bazon\n" + // don't translate this this ;-) +"Viimeisin versio: http://www.dynarch.com/projects/calendar/\n" + +"Jaettu GNU LGPL alaisena. Katso lisätiedot http://gnu.org/licenses/lgpl.html" + +"\n\n" + +"Päivä valitsin:\n" + +"- Käytä \xab, \xbb painikkeita valitaksesi vuoden\n" + +"- Käytä " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " painikkeita valitaksesi kuukauden\n" + +"- Pidä alhaalla hiiren painiketta missä tahansa yllämainituissa painikkeissa valitaksesi nopeammin."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Ajan valinta:\n" + +"- Paina mitä tahansa ajan osaa kasvattaaksesi sitä\n" + +"- tai Vaihtonäppäin-paina laskeaksesi sitä\n" + +"- tai paina ja raahaa valitaksesi nopeammin."; + +Calendar._TT["PREV_YEAR"] = "Edellinen vuosi (valikko tulee painaessa)"; +Calendar._TT["PREV_MONTH"] = "Edellinen kuukausi (valikko tulee painaessa)"; +Calendar._TT["GO_TODAY"] = "Siirry Tänään"; +Calendar._TT["NEXT_MONTH"] = "Seuraava kuukausi (valikko tulee painaessa)"; +Calendar._TT["NEXT_YEAR"] = "Seuraava vuosi (valikko tulee painaessa)"; +Calendar._TT["SEL_DATE"] = "Valitse päivä"; +Calendar._TT["DRAG_TO_MOVE"] = "Rahaa siirtääksesi"; +Calendar._TT["PART_TODAY"] = " (tänään)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Näytä %s ensin"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "6,0"; + +Calendar._TT["CLOSE"] = "Sulje"; +Calendar._TT["TODAY"] = "Tänään"; +Calendar._TT["TIME_PART"] = "(Vaihtonäppäin-)Paina tai raahaa vaihtaaksesi arvoa"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%d.%m.%Y"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "vko"; +Calendar._TT["TIME"] = "Aika:"; diff --git a/app/webroot/js/calendar/lang/calendar-fr.js b/app/webroot/js/calendar/lang/calendar-fr.js new file mode 100644 index 0000000..ee2a486 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-fr.js @@ -0,0 +1,129 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, <mihai_bazon@yahoo.com> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// Translator: David Duret, <pilgrim@mala-template.net> from previous french version + +// full day names +Calendar._DN = new Array +("Dimanche", + "Lundi", + "Mardi", + "Mercredi", + "Jeudi", + "Vendredi", + "Samedi", + "Dimanche"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Dim", + "Lun", + "Mar", + "Mer", + "Jeu", + "Ven", + "Sam", + "Dim"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("Janvier", + "Février", + "Mars", + "Avril", + "Mai", + "Juin", + "Juillet", + "Août", + "Septembre", + "Octobre", + "Novembre", + "Décembre"); + +// short month names +Calendar._SMN = new Array +("Jan", + "Fev", + "Mar", + "Avr", + "Mai", + "Juin", + "Juil", + "Aout", + "Sep", + "Oct", + "Nov", + "Dec"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "A propos du calendrier"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Heure Selecteur\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"Pour la derniere version visitez : http://www.dynarch.com/projects/calendar/\n" + +"Distribué par GNU LGPL. Voir http://gnu.org/licenses/lgpl.html pour les details." + +"\n\n" + +"Selection de la date :\n" + +"- Utiliser les bouttons \xab, \xbb pour selectionner l\'annee\n" + +"- Utiliser les bouttons " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " pour selectionner les mois\n" + +"- Garder la souris sur n'importe quels boutons pour une selection plus rapide"; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Selection de l\'heure :\n" + +"- Cliquer sur heures ou minutes pour incrementer\n" + +"- ou Maj-clic pour decrementer\n" + +"- ou clic et glisser-deplacer pour une selection plus rapide"; + +Calendar._TT["PREV_YEAR"] = "Année préc. (maintenir pour menu)"; +Calendar._TT["PREV_MONTH"] = "Mois préc. (maintenir pour menu)"; +Calendar._TT["GO_TODAY"] = "Atteindre la date du jour"; +Calendar._TT["NEXT_MONTH"] = "Mois suiv. (maintenir pour menu)"; +Calendar._TT["NEXT_YEAR"] = "Année suiv. (maintenir pour menu)"; +Calendar._TT["SEL_DATE"] = "Sélectionner une date"; +Calendar._TT["DRAG_TO_MOVE"] = "Déplacer"; +Calendar._TT["PART_TODAY"] = " (Aujourd'hui)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Afficher %s en premier"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Fermer"; +Calendar._TT["TODAY"] = "Aujourd'hui"; +Calendar._TT["TIME_PART"] = "(Maj-)Clic ou glisser pour modifier la valeur"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%d/%m/%Y"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "Sem."; +Calendar._TT["TIME"] = "Heure :"; diff --git a/app/webroot/js/calendar/lang/calendar-he.js b/app/webroot/js/calendar/lang/calendar-he.js new file mode 100644 index 0000000..9d4c87d --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-he.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar HE language +// Author: Saggi Mizrahi +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("ראשון", + "שני", + "שלישי", + "רביעי", + "חמישי", + "שישי", + "שבת", + "ראשון"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("א", + "ב", + "ג", + "ד", + "ה", + "ו", + "ש", + "א"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 0; + +// full month names +Calendar._MN = new Array +("ינואר", + "פברואר", + "מרץ", + "אפריל", + "מאי", + "יוני", + "יולי", + "אוגוסט", + "ספטמבר", + "אוקטובר", + "נובמבר", + "דצמבר"); + +// short month names +Calendar._SMN = new Array +("ינו'", + "פבו'", + "מרץ", + "אפר'", + "מאי", + "יונ'", + "יול'", + "אוג'", + "ספט'", + "אוקט'", + "נוב'", + "דצמ'"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "אודות לוח השנה"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Date selection:\n" + +"- Use the \xab, \xbb buttons to select year\n" + +"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" + +"- Hold mouse button on any of the above buttons for faster selection."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Time selection:\n" + +"- Click on any of the time parts to increase it\n" + +"- or Shift-click to decrease it\n" + +"- or click and drag for faster selection."; + +Calendar._TT["PREV_YEAR"] = "שנה קודמת (החזק לתפריט)"; +Calendar._TT["PREV_MONTH"] = "חודש קודם (החזק לתפריט)"; +Calendar._TT["GO_TODAY"] = "לך להיום"; +Calendar._TT["NEXT_MONTH"] = "חודש הבא (החזק לתפריט)"; +Calendar._TT["NEXT_YEAR"] = "שנה הבאה (החזק לתפריט)"; +Calendar._TT["SEL_DATE"] = "בחר תאריך"; +Calendar._TT["DRAG_TO_MOVE"] = "משוך כדי להזיז"; +Calendar._TT["PART_TODAY"] = " (היום)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "הצג %s קודם"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "5,6"; + +Calendar._TT["CLOSE"] = "סגור"; +Calendar._TT["TODAY"] = "היום"; +Calendar._TT["TIME_PART"] = "(Shift-)לחץ או משוך כדי לשנות את הערך"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%d-%m-%Y"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "wk"; +Calendar._TT["TIME"] = "זמן:"; diff --git a/app/webroot/js/calendar/lang/calendar-hu.js b/app/webroot/js/calendar/lang/calendar-hu.js new file mode 100644 index 0000000..0e219c1 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-hu.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar HU language +// Author: Takács Gábor +// Encoding: UTF-8 +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Vasárnap", + "Hétfő", + "Kedd", + "Szerda", + "Csütörtök", + "Péntek", + "Szombat", + "Vasárnap"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Vas", + "Hét", + "Ked", + "Sze", + "Csü", + "Pén", + "Szo", + "Vas"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("Január", + "Február", + "Március", + "Április", + "Május", + "Június", + "Július", + "Augusztus", + "Szeptember", + "Október", + "November", + "December"); + +// short month names +Calendar._SMN = new Array +("Jan", + "Feb", + "Már", + "Ápr", + "Máj", + "Jún", + "Júl", + "Aug", + "Szep", + "Okt", + "Nov", + "Dec"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "A naptár leírása"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Date selection:\n" + +"- Use the \xab, \xbb buttons to select year\n" + +"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" + +"- Hold mouse button on any of the above buttons for faster selection."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Time selection:\n" + +"- Click on any of the time parts to increase it\n" + +"- or Shift-click to decrease it\n" + +"- or click and drag for faster selection."; + +Calendar._TT["PREV_YEAR"] = "Előző év (nyomvatart = menü)"; +Calendar._TT["PREV_MONTH"] = "Előző hónap (nyomvatart = menü)"; +Calendar._TT["GO_TODAY"] = "Irány a Ma"; +Calendar._TT["NEXT_MONTH"] = "Következő hónap (nyomvatart = menü)"; +Calendar._TT["NEXT_YEAR"] = "Következő év (nyomvatart = menü)"; +Calendar._TT["SEL_DATE"] = "Válasszon dátumot"; +Calendar._TT["DRAG_TO_MOVE"] = "Fogd és vidd"; +Calendar._TT["PART_TODAY"] = " (ma)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "%s megjelenítése elsőként"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Bezár"; +Calendar._TT["TODAY"] = "Ma"; +Calendar._TT["TIME_PART"] = "(Shift-)Click vagy húzd az érték változtatásához"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y.%m.%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%B %e, %A"; + +Calendar._TT["WK"] = "hét"; +Calendar._TT["TIME"] = "Idő:"; diff --git a/app/webroot/js/calendar/lang/calendar-it.js b/app/webroot/js/calendar/lang/calendar-it.js new file mode 100644 index 0000000..fbc80c9 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-it.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, <mihai_bazon@yahoo.com> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Domenica", + "Lunedì", + "Martedì", + "Mercoledì", + "Giovedì", + "Venerdì", + "Sabato", + "Domenica"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Dom", + "Lun", + "Mar", + "Mer", + "Gio", + "Ven", + "Sab", + "Dom"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("Gennaio", + "Febbraio", + "Marzo", + "Aprile", + "Maggio", + "Giugno", + "Luglio", + "Agosto", + "Settembre", + "Ottobre", + "Novembre", + "Dicembre"); + +// short month names +Calendar._SMN = new Array +("Gen", + "Feb", + "Mar", + "Apr", + "Mag", + "Giu", + "Lug", + "Ago", + "Set", + "Ott", + "Nov", + "Dic"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "Informazioni sul calendario"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Date selection:\n" + +"- Use the \xab, \xbb buttons to select year\n" + +"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" + +"- Hold mouse button on any of the above buttons for faster selection."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Time selection:\n" + +"- Click on any of the time parts to increase it\n" + +"- or Shift-click to decrease it\n" + +"- or click and drag for faster selection."; + +Calendar._TT["PREV_YEAR"] = "Anno prec. (tieni premuto per menu)"; +Calendar._TT["PREV_MONTH"] = "Mese prec. (tieni premuto per menu)"; +Calendar._TT["GO_TODAY"] = "Oggi"; +Calendar._TT["NEXT_MONTH"] = "Mese succ. (tieni premuto per menu)"; +Calendar._TT["NEXT_YEAR"] = "Anno succ. (tieni premuto per menu)"; +Calendar._TT["SEL_DATE"] = "Seleziona data"; +Calendar._TT["DRAG_TO_MOVE"] = "Trascina per spostare"; +Calendar._TT["PART_TODAY"] = " (oggi)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Mostra %s per primo"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Chiudi"; +Calendar._TT["TODAY"] = "Oggi"; +Calendar._TT["TIME_PART"] = "(Shift-)Click o trascina per modificare"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "sett"; +Calendar._TT["TIME"] = "Ora:"; diff --git a/app/webroot/js/calendar/lang/calendar-ja.js b/app/webroot/js/calendar/lang/calendar-ja.js new file mode 100644 index 0000000..1bcc8c3 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-ja.js @@ -0,0 +1,87 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, <mihai_bazon@yahoo.com> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array ("日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array ("日", "月", "火", "水", "木", "金", "土"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 0; + +// full month names +Calendar._MN = new Array ("1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"); + +// short month names +Calendar._SMN = new Array ("1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "このカレンダーについて"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"日付の選択方法:\n" + +"- \xab, \xbb ボタンで年を選択。\n" + +"- " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " ボタンで年を選択。\n" + +"- 上記ボタンの長押しでメニューから選択。"; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Time selection:\n" + +"- Click on any of the time parts to increase it\n" + +"- or Shift-click to decrease it\n" + +"- or click and drag for faster selection."; + +Calendar._TT["PREV_YEAR"] = "前年 (長押しでメニュー表示)"; +Calendar._TT["PREV_MONTH"] = "前月 (長押しでメニュー表示)"; +Calendar._TT["GO_TODAY"] = "今日の日付を選択"; +Calendar._TT["NEXT_MONTH"] = "翌月 (長押しでメニュー表示)"; +Calendar._TT["NEXT_YEAR"] = "翌年 (長押しでメニュー表示)"; +Calendar._TT["SEL_DATE"] = "日付を選択してください"; +Calendar._TT["DRAG_TO_MOVE"] = "ドラッグで移動"; +Calendar._TT["PART_TODAY"] = " (今日)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "%s始まりで表示"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "閉じる"; +Calendar._TT["TODAY"] = "今日"; +Calendar._TT["TIME_PART"] = "(Shift-)Click or drag to change value"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%b%e日(%a)"; + +Calendar._TT["WK"] = "週"; +Calendar._TT["TIME"] = "Time:"; diff --git a/app/webroot/js/calendar/lang/calendar-ko.js b/app/webroot/js/calendar/lang/calendar-ko.js new file mode 100644 index 0000000..6570bb6 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-ko.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, <mihai_bazon@yahoo.com> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("일요일", + "월요일", + "화요일", + "수요일", + "목요일", + "금요일", + "토요일", + "일요일"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("일", + "월", + "화", + "수", + "목", + "금", + "토", + "일"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 0; + +// full month names +Calendar._MN = new Array +("1월", + "2월", + "3월", + "4월", + "5월", + "6월", + "7월", + "8월", + "9월", + "10월", + "11월", + "12월"); + +// short month names +Calendar._SMN = new Array +("1월", + "2월", + "3월", + "4월", + "5월", + "6월", + "7월", + "8월", + "9월", + "10월", + "11월", + "12월"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "이 달력은 ... & 도움말"; + +Calendar._TT["ABOUT"] = +"DHTML 날짜/시간 선택기\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"최신 버전을 구하려면 여기로: http://www.dynarch.com/projects/calendar/\n" + +"배포라이센스:GNU LGPL. 참조:http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"날짜 선택:\n" + +"- 해를 선택하려면 \xab, \xbb 버튼을 사용하세요.\n" + +"- 달을 선택하려면 " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " 버튼을 사용하세요.\n" + +"- 좀 더 빠르게 선택하려면 위의 버튼을 꾹 눌러주세요."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"시간 선택:\n" + +"- 시, 분을 더하려면 클릭하세요.\n" + +"- 시, 분을 빼려면 쉬프트 누르고 클릭하세요.\n" + +"- 좀 더 빠르게 선택하려면 클릭하고 드래그하세요."; + +Calendar._TT["PREV_YEAR"] = "이전 해"; +Calendar._TT["PREV_MONTH"] = "이전 달"; +Calendar._TT["GO_TODAY"] = "오늘로 이동"; +Calendar._TT["NEXT_MONTH"] = "다음 달"; +Calendar._TT["NEXT_YEAR"] = "다음 해"; +Calendar._TT["SEL_DATE"] = "날짜 선택"; +Calendar._TT["DRAG_TO_MOVE"] = "이동(드래그)"; +Calendar._TT["PART_TODAY"] = " (오늘)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "[%s]을 처음으로"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "닫기"; +Calendar._TT["TODAY"] = "오늘"; +Calendar._TT["TIME_PART"] = "클릭(+),쉬프트+클릭(-),드래그"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "주"; +Calendar._TT["TIME"] = "시간:"; diff --git a/app/webroot/js/calendar/lang/calendar-lt.js b/app/webroot/js/calendar/lang/calendar-lt.js new file mode 100644 index 0000000..888cfc8 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-lt.js @@ -0,0 +1,128 @@ +// ** I18N + +// Calendar LT language +// Author: Gediminas Muižis, <gediminas.muizis@elgama.eu> +// Encoding: UTF-8 +// Distributed under the same terms as the calendar itself. +// Ver: 0.2 + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Sekmadienis", + "Pirmadienis", + "Antradienis", + "Trečiadienis", + "Ketvirtadienis", + "Penktadienis", + "Šeštadienis", + "Sekmadienis"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Sek", + "Pir", + "Ant", + "Tre", + "Ket", + "Pen", + "Šeš", + "Sek"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("Sausis", + "Vasaris", + "Kovas", + "Balandis", + "Gegužė", + "Birželis", + "Liepa", + "Rudpjūtis", + "Rugsėjis", + "Spalis", + "Lapkritis", + "Gruodis"); + +// short month names +Calendar._SMN = new Array +("Sau", + "Vas", + "Kov", + "Bal", + "Geg", + "Brž", + "Lie", + "Rgp", + "Rgs", + "Spl", + "Lap", + "Grd"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "Apie kalendorių"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Datos pasirinkimas:\n" + +"- Naudoti \xab, \xbb mygtukus norint pasirinkti metus\n" + +"- Naudoti " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " mygtukus norint pasirinkti mėnesį\n" + +"- PAlaikykite nuspaudę bet kurį nygtuką norėdami iškviesti greitąjį meniu."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Datos pasirinkimas:\n" + +"- Paspaudus ant valandos ar minutės, jų reikšmės padidėja\n" + +"- arba Shift-paspaudimas norint sumažinti reikšmę\n" + +"- arba paspauskite ir tempkite norint greičiau keisti reikšmę."; + +Calendar._TT["PREV_YEAR"] = "Ankst. metai (laikyti, norint iškviesti meniu)"; +Calendar._TT["PREV_MONTH"] = "Ankst. mėnuo (laikyti, norint iškviesti meniu)"; +Calendar._TT["GO_TODAY"] = "Šiandien"; +Calendar._TT["NEXT_MONTH"] = "Kitas mėnuo (laikyti, norint iškviesti meniu)"; +Calendar._TT["NEXT_YEAR"] = "Kiti metai (laikyti, norint iškviesti meniu)"; +Calendar._TT["SEL_DATE"] = "Pasirinkti datą"; +Calendar._TT["DRAG_TO_MOVE"] = "Perkelkite pėlyte"; +Calendar._TT["PART_TODAY"] = " (šiandien)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Rodyti %s pirmiau"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Uždaryti"; +Calendar._TT["TODAY"] = "Šiandien"; +Calendar._TT["TIME_PART"] = "(Shift-)Spausti ar tempti, norint pakeisti reikšmę"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "sav"; +Calendar._TT["TIME"] = "Laikas:"; diff --git a/app/webroot/js/calendar/lang/calendar-nl.js b/app/webroot/js/calendar/lang/calendar-nl.js new file mode 100644 index 0000000..69a0d8d --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-nl.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar NL language +// Author: Linda van den Brink, <linda@dynasol.nl> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Zondag", + "Maandag", + "Dinsdag", + "Woensdag", + "Donderdag", + "Vrijdag", + "Zaterdag", + "Zondag"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Zo", + "Ma", + "Di", + "Wo", + "Do", + "Vr", + "Za", + "Zo"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 0; + +// full month names +Calendar._MN = new Array +("Januari", + "Februari", + "Maart", + "April", + "Mei", + "Juni", + "Juli", + "Augustus", + "September", + "Oktober", + "November", + "December"); + +// short month names +Calendar._SMN = new Array +("Jan", + "Feb", + "Maa", + "Apr", + "Mei", + "Jun", + "Jul", + "Aug", + "Sep", + "Okt", + "Nov", + "Dec"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "Over de kalender"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Datum selectie:\n" + +"- Gebruik de \xab, \xbb knoppen om het jaar te selecteren\n" + +"- Gebruik de " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " knoppen om de maand te selecteren\n" + +"- Houd de muisknop ingedrukt op een van de knoppen voor snellere selectie."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Tijd selectie:\n" + +"- Klik op een deel van de tijd om het te verhogen\n" + +"- of Shift-click om het te verlagen\n" + +"- of klik en sleep voor snellere selectie."; + +Calendar._TT["PREV_YEAR"] = "Vorig jaar (vasthouden voor menu)"; +Calendar._TT["PREV_MONTH"] = "Vorige maand (vasthouden voor menu)"; +Calendar._TT["GO_TODAY"] = "Ga naar vandaag"; +Calendar._TT["NEXT_MONTH"] = "Volgende maand (vasthouden voor menu)"; +Calendar._TT["NEXT_YEAR"] = "Volgend jaar(vasthouden voor menu)"; +Calendar._TT["SEL_DATE"] = "Selecteer datum"; +Calendar._TT["DRAG_TO_MOVE"] = "Sleep om te verplaatsen"; +Calendar._TT["PART_TODAY"] = " (vandaag)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Toon %s eerst"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Sluiten"; +Calendar._TT["TODAY"] = "Vandaag"; +Calendar._TT["TIME_PART"] = "(Shift-)klik of sleep om waarde te wijzigen"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "wk"; +Calendar._TT["TIME"] = "Tijd:"; diff --git a/app/webroot/js/calendar/lang/calendar-no.js b/app/webroot/js/calendar/lang/calendar-no.js new file mode 100644 index 0000000..0506b83 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-no.js @@ -0,0 +1,86 @@ +// ** I18N + +// Calendar NO language (Norwegian/Norsk bokmål) +// Author: Kai Olav Fredriksen <k@i.fredriksen.net> + +// full day names +Calendar._DN = new Array +("Søndag", + "Mandag", + "Tirsdag", + "Onsdag", + "Torsdag", + "Fredag", + "Lørdag", + "Søndag"); + +Calendar._SDN_len = 3; // short day name length +Calendar._SMN_len = 3; // short month name length + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("Januar", + "Februar", + "Mars", + "April", + "Mai", + "Juni", + "Juli", + "August", + "September", + "Oktober", + "November", + "Desember"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "Om kalenderen"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Date selection:\n" + +"- Use the \xab, \xbb buttons to select year\n" + +"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" + +"- Hold mouse button on any of the above buttons for faster selection."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Time selection:\n" + +"- Click on any of the time parts to increase it\n" + +"- or Shift-click to decrease it\n" + +"- or click and drag for faster selection."; + +Calendar._TT["PREV_YEAR"] = "Forrige år (hold for meny)"; +Calendar._TT["PREV_MONTH"] = "Forrige måned (hold for meny)"; +Calendar._TT["GO_TODAY"] = "Gå til idag"; +Calendar._TT["NEXT_MONTH"] = "Neste måned (hold for meny)"; +Calendar._TT["NEXT_YEAR"] = "Neste år (hold for meny)"; +Calendar._TT["SEL_DATE"] = "Velg dato"; +Calendar._TT["DRAG_TO_MOVE"] = "Dra for å flytte"; +Calendar._TT["PART_TODAY"] = " (idag)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Vis %s først"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Lukk"; +Calendar._TT["TODAY"] = "Idag"; +Calendar._TT["TIME_PART"] = "(Shift-)Klikk eller dra for å endre verdi"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%%d.%m.%Y"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "uke"; +Calendar._TT["TIME"] = "Tid:"; diff --git a/app/webroot/js/calendar/lang/calendar-pl.js b/app/webroot/js/calendar/lang/calendar-pl.js new file mode 100644 index 0000000..32273d6 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-pl.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, <mihai_bazon@yahoo.com> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Niedziela", + "Poniedziałek", + "Wtorek", + "Środa", + "Czwartek", + "Piątek", + "Sobota", + "Niedziela"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Nie", + "Pon", + "Wto", + "Śro", + "Czw", + "Pią", + "Sob", + "Nie"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("Styczeń", + "Luty", + "Marzec", + "Kwiecień", + "Maj", + "Czerwiec", + "Lipiec", + "Sierpień", + "Wrzesień", + "Październik", + "Listopad", + "Grudzień"); + +// short month names +Calendar._SMN = new Array +("Sty", + "Lut", + "Mar", + "Kwi", + "Maj", + "Cze", + "Lip", + "Sie", + "Wrz", + "Paź", + "Lis", + "Gru"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "O kalendarzu"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"Po ostatnią wersję odwiedź: http://www.dynarch.com/projects/calendar/\n" + +"Rozpowszechniany pod licencją GNU LGPL. Zobacz: http://gnu.org/licenses/lgpl.html z celu zapoznania się ze szczegółami." + +"\n\n" + +"Wybór daty:\n" + +"- Użyj \xab, \xbb przycisków by zaznaczyć rok\n" + +"- Użyj " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " przycisków by zaznaczyć miesiąc\n" + +"- Trzymaj wciśnięty przycisk myszy na każdym z powyższych przycisków by przyśpieszyć zaznaczanie."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Wybór czasu:\n" + +"- Kliknij na każdym przedziale czasu aby go powiększyć\n" + +"- lub kliknij z przyciskiem Shift by go zmniejszyć\n" + +"- lub kliknij i przeciągnij dla szybszego zaznaczenia."; + +Calendar._TT["PREV_YEAR"] = "Poprz. rok (przytrzymaj dla menu)"; +Calendar._TT["PREV_MONTH"] = "Poprz. miesiąc (przytrzymaj dla menu)"; +Calendar._TT["GO_TODAY"] = "Idź do Dzisiaj"; +Calendar._TT["NEXT_MONTH"] = "Następny miesiąc(przytrzymaj dla menu)"; +Calendar._TT["NEXT_YEAR"] = "Następny rok (przytrzymaj dla menu)"; +Calendar._TT["SEL_DATE"] = "Zaznacz datę"; +Calendar._TT["DRAG_TO_MOVE"] = "Przeciągnij by przenieść"; +Calendar._TT["PART_TODAY"] = " (dzisiaj)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Pokaż %s pierwszy"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Zamknij"; +Calendar._TT["TODAY"] = "Dzisiaj"; +Calendar._TT["TIME_PART"] = "(Shift-)Kliknij lub upuść by zmienić wartość"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%R-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "wk"; +Calendar._TT["TIME"] = "Czas:"; diff --git a/app/webroot/js/calendar/lang/calendar-pt-br.js b/app/webroot/js/calendar/lang/calendar-pt-br.js new file mode 100644 index 0000000..bf7734a --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-pt-br.js @@ -0,0 +1,129 @@ +// ** I18N + +// Calendar pt_BR language +// Author: Adalberto Machado, <betosm@terra.com.br> +// Review: Alexandre da Silva, <simpsomboy@gmail.com> +// Encoding: UTF-8 +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Domingo", + "Segunda", + "Terça", + "Quarta", + "Quinta", + "Sexta", + "Sabado", + "Domingo"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Dom", + "Seg", + "Ter", + "Qua", + "Qui", + "Sex", + "Sab", + "Dom"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 0; + +// full month names +Calendar._MN = new Array +("Janeiro", + "Fevereiro", + "Março", + "Abril", + "Maio", + "Junho", + "Julho", + "Agosto", + "Setembro", + "Outubro", + "Novembro", + "Dezembro"); + +// short month names +Calendar._SMN = new Array +("Jan", + "Fev", + "Mar", + "Abr", + "Mai", + "Jun", + "Jul", + "Ago", + "Set", + "Out", + "Nov", + "Dez"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "Sobre o calendário"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"Última versão visite: http://www.dynarch.com/projects/calendar/\n" + +"Distribuído sobre GNU LGPL. Veja http://gnu.org/licenses/lgpl.html para detalhes." + +"\n\n" + +"Seleção de data:\n" + +"- Use os botões \xab, \xbb para selecionar o ano\n" + +"- Use os botões " + String.fromCharCode(0x2039) + ", " + +String.fromCharCode(0x203a) + " para selecionar o mês\n" + +"- Segure o botão do mouse em qualquer um desses botões para seleção rápida."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Seleção de hora:\n" + +"- Clique em qualquer parte da hora para incrementar\n" + +"- ou Shift-click para decrementar\n" + +"- ou clique e segure para seleção rápida."; + +Calendar._TT["PREV_YEAR"] = "Ant. ano (segure para menu)"; +Calendar._TT["PREV_MONTH"] = "Ant. mês (segure para menu)"; +Calendar._TT["GO_TODAY"] = "Hoje"; +Calendar._TT["NEXT_MONTH"] = "Próx. mes (segure para menu)"; +Calendar._TT["NEXT_YEAR"] = "Próx. ano (segure para menu)"; +Calendar._TT["SEL_DATE"] = "Selecione a data"; +Calendar._TT["DRAG_TO_MOVE"] = "Arraste para mover"; +Calendar._TT["PART_TODAY"] = " (hoje)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Mostre %s primeiro"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Fechar"; +Calendar._TT["TODAY"] = "Hoje"; +Calendar._TT["TIME_PART"] = "(Shift-)Click ou arraste para mudar valor"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%d/%m/%Y"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %e %b"; + +Calendar._TT["WK"] = "sm"; +Calendar._TT["TIME"] = "Hora:"; diff --git a/app/webroot/js/calendar/lang/calendar-pt.js b/app/webroot/js/calendar/lang/calendar-pt.js new file mode 100644 index 0000000..1ab5795 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-pt.js @@ -0,0 +1,128 @@ +// ** I18N + +// Calendar pt language +// Author: Adalberto Machado, <betosm@terra.com.br> +// Corrected by: Pedro Araújo <phcrva19@hotmail.com> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Domingo", + "Segunda", + "Terça", + "Quarta", + "Quinta", + "Sexta", + "Sábado", + "Domingo"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Dom", + "Seg", + "Ter", + "Qua", + "Qui", + "Sex", + "Sáb", + "Dom"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("Janeiro", + "Fevereiro", + "Março", + "Abril", + "Maio", + "Junho", + "Julho", + "Agosto", + "Setembro", + "Outubro", + "Novembro", + "Dezembro"); + +// short month names +Calendar._SMN = new Array +("Jan", + "Fev", + "Mar", + "Abr", + "Mai", + "Jun", + "Jul", + "Ago", + "Set", + "Out", + "Nov", + "Dez"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "Sobre o calendário"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"Última versão visite: http://www.dynarch.com/projects/calendar/\n" + +"Distribuído sobre a licença GNU LGPL. Veja http://gnu.org/licenses/lgpl.html para detalhes." + +"\n\n" + +"Selecção de data:\n" + +"- Use os botões \xab, \xbb para seleccionar o ano\n" + +"- Use os botões " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " para seleccionar o mês\n" + +"- Segure o botão do rato em qualquer um desses botões para selecção rápida."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Selecção de hora:\n" + +"- Clique em qualquer parte da hora para incrementar\n" + +"- ou Shift-click para decrementar\n" + +"- ou clique e segure para selecção rápida."; + +Calendar._TT["PREV_YEAR"] = "Ano ant. (segure para menu)"; +Calendar._TT["PREV_MONTH"] = "Mês ant. (segure para menu)"; +Calendar._TT["GO_TODAY"] = "Hoje"; +Calendar._TT["NEXT_MONTH"] = "Prox. mês (segure para menu)"; +Calendar._TT["NEXT_YEAR"] = "Prox. ano (segure para menu)"; +Calendar._TT["SEL_DATE"] = "Seleccione a data"; +Calendar._TT["DRAG_TO_MOVE"] = "Arraste para mover"; +Calendar._TT["PART_TODAY"] = " (hoje)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Mostre %s primeiro"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Fechar"; +Calendar._TT["TODAY"] = "Hoje"; +Calendar._TT["TIME_PART"] = "(Shift-)Click ou arraste para mudar valor"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%d/%m/%Y"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %e %b"; + +Calendar._TT["WK"] = "sm"; +Calendar._TT["TIME"] = "Hora:"; diff --git a/app/webroot/js/calendar/lang/calendar-ro.js b/app/webroot/js/calendar/lang/calendar-ro.js new file mode 100644 index 0000000..fa34ab1 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-ro.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, <mihai_bazon@yahoo.com> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Duminica", + "Luni", + "Marti", + "Miercuri", + "Joi", + "Vineri", + "Sambata", + "Duminica"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Dum", + "Lun", + "Mar", + "Mie", + "Joi", + "Vin", + "Sam", + "Dum"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 0; + +// full month names +Calendar._MN = new Array +("Ianuarie", + "Februarie", + "Martie", + "Aprilie", + "Mai", + "Iunie", + "Iulie", + "August", + "Septembrie", + "Octombrie", + "Noiembrie", + "Decembrie"); + +// short month names +Calendar._SMN = new Array +("Ian", + "Feb", + "Mar", + "Apr", + "Mai", + "Iun", + "Iul", + "Aug", + "Sep", + "Oct", + "Noi", + "Dec"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "Despre calendar"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Selectare data:\n" + +"- Folositi butoanele \xab, \xbb pentru a selecta anul\n" + +"- Folositi butoanele " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " pentru a selecta luna\n" + +"- Lasati apasat butonul pentru o selectie mai rapida."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Selectare timp:\n" + +"- Click pe campul de timp pentru a majora timpul\n" + +"- sau Shift-Click pentru a micsora\n" + +"- sau click si drag pentru manipulare rapida."; + +Calendar._TT["PREV_YEAR"] = "Anul precedent (apasati pentru meniu)"; +Calendar._TT["PREV_MONTH"] = "Luna precedenta (apasati pentru meniu)"; +Calendar._TT["GO_TODAY"] = "Data de azi"; +Calendar._TT["NEXT_MONTH"] = "Luna viitoare (apasati pentru meniu)"; +Calendar._TT["NEXT_YEAR"] = "Anul viitor (apasati pentru meniu)"; +Calendar._TT["SEL_DATE"] = "Selectie data"; +Calendar._TT["DRAG_TO_MOVE"] = "Drag pentru a muta"; +Calendar._TT["PART_TODAY"] = " (azi)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Vizualizeaza %s prima"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "inchide"; +Calendar._TT["TODAY"] = "Azi"; +Calendar._TT["TIME_PART"] = "(Shift-)Click sau drag pentru a schimba valoarea"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%A-%l-%z"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "sapt"; +Calendar._TT["TIME"] = "Ora:"; diff --git a/app/webroot/js/calendar/lang/calendar-ru.js b/app/webroot/js/calendar/lang/calendar-ru.js new file mode 100644 index 0000000..6274cc8 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-ru.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar RU language +// Translation: Sly Golovanov, http://golovanov.net, <sly@golovanov.net> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("воскресенье", + "понедельник", + "вторник", + "среда", + "четверг", + "пятница", + "суббота", + "воскресенье"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("вск", + "пон", + "втр", + "срд", + "чет", + "пят", + "суб", + "вск"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("январь", + "февраль", + "март", + "апрель", + "май", + "июнь", + "июль", + "август", + "сентябрь", + "октябрь", + "ноябрь", + "декабрь"); + +// short month names +Calendar._SMN = new Array +("янв", + "фев", + "мар", + "апр", + "май", + "июн", + "июл", + "авг", + "сен", + "окт", + "ноя", + "дек"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "О календаре..."; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Как выбрать дату:\n" + +"- При помощи кнопок \xab, \xbb можно выбрать год\n" + +"- При помощи кнопок " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " можно выбрать месяц\n" + +"- Подержите эти кнопки нажатыми, чтобы появилось меню быстрого выбора."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Как выбрать время:\n" + +"- При клике на часах или минутах они увеличиваются\n" + +"- при клике с нажатой клавишей Shift они уменьшаются\n" + +"- если нажать и двигать мышкой влево/вправо, они будут меняться быстрее."; + +Calendar._TT["PREV_YEAR"] = "На год назад (удерживать для меню)"; +Calendar._TT["PREV_MONTH"] = "На месяц назад (удерживать для меню)"; +Calendar._TT["GO_TODAY"] = "Сегодня"; +Calendar._TT["NEXT_MONTH"] = "На месяц вперед (удерживать для меню)"; +Calendar._TT["NEXT_YEAR"] = "На год вперед (удерживать для меню)"; +Calendar._TT["SEL_DATE"] = "Выберите дату"; +Calendar._TT["DRAG_TO_MOVE"] = "Перетаскивайте мышкой"; +Calendar._TT["PART_TODAY"] = " (сегодня)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Первый день недели будет %s"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Закрыть"; +Calendar._TT["TODAY"] = "Сегодня"; +Calendar._TT["TIME_PART"] = "(Shift-)клик или нажать и двигать"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%e %b, %a"; + +Calendar._TT["WK"] = "нед"; +Calendar._TT["TIME"] = "Время:"; diff --git a/app/webroot/js/calendar/lang/calendar-sk.js b/app/webroot/js/calendar/lang/calendar-sk.js new file mode 100644 index 0000000..c54d9ac --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-sk.js @@ -0,0 +1,68 @@ +/* + calendar-sk.js + language: Slovak + encoding: UTF-8 + author: Stanislav Pach (stano.pach@seznam.cz) +*/ + +// ** I18N +Calendar._DN = new Array('Nedeľa','Pondelok','Utorok','Streda','Štvrtok','Piatok','Sobota','Nedeľa'); +Calendar._SDN = new Array('Ne','Po','Ut','St','Št','Pi','So','Ne'); +Calendar._MN = new Array('Január','Február','Marec','Apríl','Máj','Jún','Júl','August','September','Október','November','December'); +Calendar._SMN = new Array('Jan','Feb','Mar','Apr','Máj','Jún','Júl','Aug','Sep','Okt','Nov','Dec'); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "O komponente kalendár"; +Calendar._TT["TOGGLE"] = "Zmena prvého dňa v týždni"; +Calendar._TT["PREV_YEAR"] = "Predchádzajúci rok (pridrž pre menu)"; +Calendar._TT["PREV_MONTH"] = "Predchádzajúci mesiac (pridrž pre menu)"; +Calendar._TT["GO_TODAY"] = "Dnešný dátum"; +Calendar._TT["NEXT_MONTH"] = "Ďalší mesiac (pridrž pre menu)"; +Calendar._TT["NEXT_YEAR"] = "Ďalší rok (pridrž pre menu)"; +Calendar._TT["SEL_DATE"] = "Zvoľ dátum"; +Calendar._TT["DRAG_TO_MOVE"] = "Chyť a ťahaj pre presun"; +Calendar._TT["PART_TODAY"] = " (dnes)"; +Calendar._TT["MON_FIRST"] = "Ukáž ako prvný Pondelok"; +//Calendar._TT["SUN_FIRST"] = "Ukaž jako první Neděli"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Výber dátumu:\n" + +"- Použijte tlačítka \xab, \xbb pre voľbu roku\n" + +"- Použijte tlačítka " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " pre výber mesiaca\n" + +"- Podržte tlačítko myši na akomkoľvek z týchto tlačítok pre rýchlejší výber."; + +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Výber času:\n" + +"- Kliknite na akúkoľvek časť z výberu času pre zvýšenie.\n" + +"- alebo Shift-klick pre zníženie\n" + +"- alebo kliknite a ťahajte pre rýchlejší výber."; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Zobraz %s ako prvý"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Zavrieť"; +Calendar._TT["TODAY"] = "Dnes"; +Calendar._TT["TIME_PART"] = "(Shift-)Klikni alebo ťahaj pre zmenu hodnoty"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "d.m.yy"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "týž"; +Calendar._TT["TIME"] = "Čas:"; diff --git a/app/webroot/js/calendar/lang/calendar-sr.js b/app/webroot/js/calendar/lang/calendar-sr.js new file mode 100644 index 0000000..626cbdc --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-sr.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar SR language +// Author: Dragan Matic, <kkid@panforma.co.yu> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Nedelja", + "Ponedeljak", + "Utorak", + "Sreda", + "Četvrtak", + "Petak", + "Subota", + "Nedelja"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Ned", + "Pon", + "Uto", + "Sre", + "Čet", + "Pet", + "Sub", + "Ned"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 0; + +// full month names +Calendar._MN = new Array +("Januar", + "Februar", + "Mart", + "April", + "Maj", + "Jun", + "Jul", + "Avgust", + "Septembar", + "Oktobar", + "Novembar", + "Decembar"); + +// short month names +Calendar._SMN = new Array +("Jan", + "Feb", + "Mar", + "Apr", + "Maj", + "Jun", + "Jul", + "Avg", + "Sep", + "Okt", + "Nov", + "Dec"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "O kalendaru"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Date selection:\n" + +"- Use the \xab, \xbb buttons to select year\n" + +"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" + +"- Hold mouse button on any of the above buttons for faster selection."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Time selection:\n" + +"- Click on any of the time parts to increase it\n" + +"- or Shift-click to decrease it\n" + +"- or click and drag for faster selection."; + +Calendar._TT["PREV_YEAR"] = "Preth. godina (hold for menu)"; +Calendar._TT["PREV_MONTH"] = "Preth. mesec (hold for menu)"; +Calendar._TT["GO_TODAY"] = "Na današnji dan"; +Calendar._TT["NEXT_MONTH"] = "Naredni mesec (hold for menu)"; +Calendar._TT["NEXT_YEAR"] = "Naredna godina (hold for menu)"; +Calendar._TT["SEL_DATE"] = "Izbor datuma"; +Calendar._TT["DRAG_TO_MOVE"] = "Prevucite za izmenu"; +Calendar._TT["PART_TODAY"] = " (danas)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Prikazi %s prvo"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Close"; +Calendar._TT["TODAY"] = "Danas"; +Calendar._TT["TIME_PART"] = "(Shift-)Klik ili prevlačenje za izmenu vrednosti"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%d-%m-%Y"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "wk"; +Calendar._TT["TIME"] = "Vreme:"; diff --git a/app/webroot/js/calendar/lang/calendar-sv.js b/app/webroot/js/calendar/lang/calendar-sv.js new file mode 100644 index 0000000..7e73d79 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-sv.js @@ -0,0 +1,84 @@ +// ** I18N + +// full day names +Calendar._DN = new Array +("Söndag", + "Måndag", + "Tisdag", + "Onsdag", + "Torsdag", + "Fredag", + "Lördag", + "Söndag"); + +Calendar._SDN_len = 3; // short day name length +Calendar._SMN_len = 3; // short month name length + + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 0; + +// full month names +Calendar._MN = new Array +("Januari", + "Februari", + "Mars", + "April", + "Maj", + "Juni", + "Juli", + "Augusti", + "September", + "Oktober", + "November", + "December"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "About the calendar"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Date selection:\n" + +"- Use the \xab, \xbb buttons to select year\n" + +"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" + +"- Hold mouse button on any of the above buttons for faster selection."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Time selection:\n" + +"- Click on any of the time parts to increase it\n" + +"- or Shift-click to decrease it\n" + +"- or click and drag for faster selection."; + +Calendar._TT["PREV_YEAR"] = "Prev. year (hold for menu)"; +Calendar._TT["PREV_MONTH"] = "Prev. month (hold for menu)"; +Calendar._TT["GO_TODAY"] = "Go Today"; +Calendar._TT["NEXT_MONTH"] = "Next month (hold for menu)"; +Calendar._TT["NEXT_YEAR"] = "Next year (hold for menu)"; +Calendar._TT["SEL_DATE"] = "Select date"; +Calendar._TT["DRAG_TO_MOVE"] = "Drag to move"; +Calendar._TT["PART_TODAY"] = " (today)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Display %s first"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Close"; +Calendar._TT["TODAY"] = "Today"; +Calendar._TT["TIME_PART"] = "(Shift-)Click or drag to change value"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "wk"; +Calendar._TT["TIME"] = "Time:"; diff --git a/app/webroot/js/calendar/lang/calendar-th.js b/app/webroot/js/calendar/lang/calendar-th.js new file mode 100644 index 0000000..dc4809e --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-th.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Gampol Thitinilnithi, <gampolt@gmail.com> +// Encoding: UTF-8 +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("อาทิตย์", + "จันทร์", + "อังคาร", + "พุธ", + "พฤหัสบดี", + "ศุกร์", + "เสาร์", + "อาทิตย์"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("อา.", + "จ.", + "อ.", + "พ.", + "พฤ.", + "ศ.", + "ส.", + "อา."); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("มกราคม", + "กุมภาพันธ์", + "มีนาคม", + "เมษายน", + "พฤษภาคม", + "มิถุนายน", + "กรกฎาคม", + "สิงหาคม", + "กันยายน", + "ตุลาคม", + "พฤศจิกายน", + "ธันวาคม"); + +// short month names +Calendar._SMN = new Array +("ม.ค.", + "ก.พ.", + "มี.ค.", + "เม.ย.", + "พ.ค.", + "มิ.ย.", + "ก.ค.", + "ส.ค.", + "ก.ย.", + "ต.ค.", + "พ.ย.", + "ธ.ค."); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "เกี่ยวกับปฏิทิน"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Date selection:\n" + +"- Use the \xab, \xbb buttons to select year\n" + +"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" + +"- Hold mouse button on any of the above buttons for faster selection."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Time selection:\n" + +"- Click on any of the time parts to increase it\n" + +"- or Shift-click to decrease it\n" + +"- or click and drag for faster selection."; + +Calendar._TT["PREV_YEAR"] = "ปีที่แล้ว (ถ้ากดค้างจะมีเมนู)"; +Calendar._TT["PREV_MONTH"] = "เดือนที่แล้ว (ถ้ากดค้างจะมีเมนู)"; +Calendar._TT["GO_TODAY"] = "ไปที่วันนี้"; +Calendar._TT["NEXT_MONTH"] = "เดือนหน้า (ถ้ากดค้างจะมีเมนู)"; +Calendar._TT["NEXT_YEAR"] = "ปีหน้า (ถ้ากดค้างจะมีเมนู)"; +Calendar._TT["SEL_DATE"] = "เลือกวัน"; +Calendar._TT["DRAG_TO_MOVE"] = "กดแล้วลากเพื่อย้าย"; +Calendar._TT["PART_TODAY"] = " (วันนี้)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "แสดง %s เป็นวันแรก"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "ปิด"; +Calendar._TT["TODAY"] = "วันนี้"; +Calendar._TT["TIME_PART"] = "(Shift-)กดหรือกดแล้วลากเพื่อเปลี่ยนค่า"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%a %e %b"; + +Calendar._TT["WK"] = "wk"; +Calendar._TT["TIME"] = "เวลา:"; diff --git a/app/webroot/js/calendar/lang/calendar-tr.js b/app/webroot/js/calendar/lang/calendar-tr.js new file mode 100644 index 0000000..c262d2f --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-tr.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, <mihai_bazon@yahoo.com> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Pazar", + "Pazartesi", + "Salı", + "Çarşamba", + "Perşembe", + "Cuma", + "Cumartesi", + "Pazar"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Paz", + "Pzt", + "Sal", + "Çar", + "Per", + "Cum", + "Cmt", + "Paz"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("Ocak", + "Şubat", + "Mart", + "Nisan", + "Mayıs", + "Haziran", + "Temmuz", + "Ağustos", + "Eylül", + "Ekim", + "Kasım", + "Aralık"); + +// short month names +Calendar._SMN = new Array +("Oca", + "Şub", + "Mar", + "Nis", + "May", + "Haz", + "Tem", + "Ağu", + "Eyl", + "Eki", + "Kas", + "Ara"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "Takvim hakkında"; + +Calendar._TT["ABOUT"] = +"DHTML Tarih/Zaman Seçici\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Tarih Seçimi:\n" + +"- Yıl seçmek için \xab, \xbb tuşlarını kullanın\n" + +"- Ayı seçmek için " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " tuşlarını kullanın\n" + +"- Hızlı seçim için yukardaki butonların üzerinde farenin tuşuna basılı tutun."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Zaman Seçimi:\n" + +"- Arttırmak için herhangi bir zaman bölümüne tıklayın\n" + +"- ya da azaltmak için Shift+tıkla yapın\n" + +"- ya da daha hızlı bir seçim için tıklayın ve sürükleyin."; + +Calendar._TT["PREV_YEAR"] = "Öncki yıl (Menu için basılı tutun)"; +Calendar._TT["PREV_MONTH"] = "Önceki ay (Menu için basılı tutun)"; +Calendar._TT["GO_TODAY"] = "Bugüne Git"; +Calendar._TT["NEXT_MONTH"] = "Sonraki Ay (Menu için basılı tutun)"; +Calendar._TT["NEXT_YEAR"] = "Next year (Menu için basılı tutun)"; +Calendar._TT["SEL_DATE"] = "Tarih seçin"; +Calendar._TT["DRAG_TO_MOVE"] = "Taşımak için sürükleyin"; +Calendar._TT["PART_TODAY"] = " (bugün)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "%s : önce göster"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "1,0"; + +Calendar._TT["CLOSE"] = "Kapat"; +Calendar._TT["TODAY"] = "Bugün"; +Calendar._TT["TIME_PART"] = "Değeri değiştirmek için (Shift-)tıkla veya sürükle"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%d-%m-%Y"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "Hafta"; +Calendar._TT["TIME"] = "Saat:"; diff --git a/app/webroot/js/calendar/lang/calendar-uk.js b/app/webroot/js/calendar/lang/calendar-uk.js new file mode 100644 index 0000000..0dbde79 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-uk.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, <mihai_bazon@yahoo.com> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 0; + +// full month names +Calendar._MN = new Array +("January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December"); + +// short month names +Calendar._SMN = new Array +("Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "About the calendar"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Date selection:\n" + +"- Use the \xab, \xbb buttons to select year\n" + +"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" + +"- Hold mouse button on any of the above buttons for faster selection."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Time selection:\n" + +"- Click on any of the time parts to increase it\n" + +"- or Shift-click to decrease it\n" + +"- or click and drag for faster selection."; + +Calendar._TT["PREV_YEAR"] = "Prev. year (hold for menu)"; +Calendar._TT["PREV_MONTH"] = "Prev. month (hold for menu)"; +Calendar._TT["GO_TODAY"] = "Go Today"; +Calendar._TT["NEXT_MONTH"] = "Next month (hold for menu)"; +Calendar._TT["NEXT_YEAR"] = "Next year (hold for menu)"; +Calendar._TT["SEL_DATE"] = "Select date"; +Calendar._TT["DRAG_TO_MOVE"] = "Drag to move"; +Calendar._TT["PART_TODAY"] = " (today)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Display %s first"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Close"; +Calendar._TT["TODAY"] = "Today"; +Calendar._TT["TIME_PART"] = "(Shift-)Click or drag to change value"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "wk"; +Calendar._TT["TIME"] = "Time:"; diff --git a/app/webroot/js/calendar/lang/calendar-vn.js b/app/webroot/js/calendar/lang/calendar-vn.js new file mode 100644 index 0000000..9172c66 --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-vn.js @@ -0,0 +1,126 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, <mihai_bazon@yahoo.com> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Chủ nhật", + "Thứ Hai", + "Thứ Ba", + "Thứ Tư", + "Thứ Năm", + "Thứ Sáu", + "Thứ Bảy", + "Chủ Nhật"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("C.Nhật", + "Hai", + "Ba", + "Tư", + "Năm", + "Sáu", + "Bảy", + "C.Nhật"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("Tháng Giêng", + "Tháng Hai", + "Tháng Ba", + "Tháng Tư", + "Tháng Năm", + "Tháng Sáu", + "Tháng Bảy", + "Tháng Tám", + "Tháng Chín", + "Tháng Mười", + "Tháng M.Một", + "Tháng Chạp"); + +// short month names +Calendar._SMN = new Array +("Mmột", + "Hai", + "Ba", + "Tư", + "Năm", + "Sáu", + "Bảy", + "Tám", + "Chín", + "Mười", + "MMột", + "Chạp"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "Giới thiệu"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector (c) dynarch.com 2002-2005 / Tác giả: Mihai Bazon. " + // don't translate this this ;-) +"Phiên bản mới nhất có tại: http://www.dynarch.com/projects/calendar/. " + +"Sản phẩm được phân phối theo giấy phép GNU LGPL. Xem chi tiết tại http://gnu.org/licenses/lgpl.html." + +"\n\n" + +"Chọn ngày:\n" + +"- Dùng nút \xab, \xbb để chọn năm\n" + +"- Dùng nút " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " để chọn tháng\n" + +"- Giữ chuột vào các nút trên để có danh sách năm và tháng."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Chọn thời gian:\n" + +"- Click chuột trên từng phần của thời gian để chỉnh sửa\n" + +"- hoặc nhấn Shift + click chuột để tăng giá trị\n" + +"- hoặc click chuột và kéo (drag) để chọn nhanh."; + +Calendar._TT["PREV_YEAR"] = "Năm trước (giữ chuột để có menu)"; +Calendar._TT["PREV_MONTH"] = "Tháng trước (giữ chuột để có menu)"; +Calendar._TT["GO_TODAY"] = "đến Hôm nay"; +Calendar._TT["NEXT_MONTH"] = "Tháng tới (giữ chuột để có menu)"; +Calendar._TT["NEXT_YEAR"] = "Ngày tới (giữ chuột để có menu)"; +Calendar._TT["SEL_DATE"] = "Chọn ngày"; +Calendar._TT["DRAG_TO_MOVE"] = "Kéo (drag) để di chuyển"; +Calendar._TT["PART_TODAY"] = " (hôm nay)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Hiển thị %s trước"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Đóng"; +Calendar._TT["TODAY"] = "Hôm nay"; +Calendar._TT["TIME_PART"] = "Click, shift-click hoặc kéo (drag) để đổi giá trị"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "wk"; +Calendar._TT["TIME"] = "Time:"; diff --git a/app/webroot/js/calendar/lang/calendar-zh-tw.js b/app/webroot/js/calendar/lang/calendar-zh-tw.js new file mode 100644 index 0000000..1e759db --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-zh-tw.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, <mihai_bazon@yahoo.com> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六", + "星期日"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("日", + "一", + "二", + "三", + "四", + "五", + "六", + "日"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 0; + +// full month names +Calendar._MN = new Array +("一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月"); + +// short month names +Calendar._SMN = new Array +("一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "關於 calendar"; + +Calendar._TT["ABOUT"] = +"DHTML 日期/時間 選擇器\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"最新版本取得位址: http://www.dynarch.com/projects/calendar/\n" + +"使用 GNU LGPL 發行. 參考 http://gnu.org/licenses/lgpl.html 以取得更多關於 LGPL 之細節。" + +"\n\n" + +"日期選擇方式:\n" + +"- 使用滑鼠點擊 \xab 、 \xbb 按鈕選擇年份\n" + +"- 使用滑鼠點擊 " + String.fromCharCode(0x2039) + " 、 " + String.fromCharCode(0x203a) + " 按鈕選擇月份\n" + +"- 使用滑鼠點擊上述按鈕並按住不放,可開啟快速選單。"; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"時間選擇方式:\n" + +"- 「單擊」時分秒為遞增\n" + +"- 或 「Shift-單擊」為遞減\n" + +"- 或 「單擊且拖拉」為快速選擇"; + +Calendar._TT["PREV_YEAR"] = "前一年 (按住不放可顯示選單)"; +Calendar._TT["PREV_MONTH"] = "前一個月 (按住不放可顯示選單)"; +Calendar._TT["GO_TODAY"] = "選擇今天"; +Calendar._TT["NEXT_MONTH"] = "後一個月 (按住不放可顯示選單)"; +Calendar._TT["NEXT_YEAR"] = "下一年 (按住不放可顯式選單)"; +Calendar._TT["SEL_DATE"] = "請點選日期"; +Calendar._TT["DRAG_TO_MOVE"] = "按住不放可拖拉視窗"; +Calendar._TT["PART_TODAY"] = " (今天)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "以 %s 做為一週的首日"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "關閉視窗"; +Calendar._TT["TODAY"] = "今天"; +Calendar._TT["TIME_PART"] = "(Shift-)加「單擊」或「拖拉」可變更值"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "星期 %a, %b %e 日"; + +Calendar._TT["WK"] = "週"; +Calendar._TT["TIME"] = "時間:"; diff --git a/app/webroot/js/calendar/lang/calendar-zh.js b/app/webroot/js/calendar/lang/calendar-zh.js new file mode 100644 index 0000000..121653f --- /dev/null +++ b/app/webroot/js/calendar/lang/calendar-zh.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar Chinese language +// Author: Andy Wu, <andywu.zh@gmail.com> +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六", + "星期日"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("日", + "一", + "二", + "三", + "四", + "五", + "六", + "日"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 0; + +// full month names +Calendar._MN = new Array +("1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月"); + +// short month names +Calendar._SMN = new Array +("1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "关于日历"; + +Calendar._TT["ABOUT"] = +"DHTML 日期/时间 选择器\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"最新版本请访问: http://www.dynarch.com/projects/calendar/\n" + +"遵循 GNU LGPL 发布。详情请查阅 http://gnu.org/licenses/lgpl.html " + +"\n\n" + +"日期选择:\n" + +"- 使用 \xab,\xbb 按钮选择年\n" + +"- 使用 " + String.fromCharCode(0x2039) + "," + String.fromCharCode(0x203a) + " 按钮选择月\n" + +"- 在上述按钮上按住不放可以快速选择"; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"时间选择:\n" + +"- 点击时间的任意部分来增加\n" + +"- Shift加点击来减少\n" + +"- 点击后拖动进行快速选择"; + +Calendar._TT["PREV_YEAR"] = "上年(按住不放显示菜单)"; +Calendar._TT["PREV_MONTH"] = "上月(按住不放显示菜单)"; +Calendar._TT["GO_TODAY"] = "回到今天"; +Calendar._TT["NEXT_MONTH"] = "下月(按住不放显示菜单)"; +Calendar._TT["NEXT_YEAR"] = "下年(按住不放显示菜单)"; +Calendar._TT["SEL_DATE"] = "选择日期"; +Calendar._TT["DRAG_TO_MOVE"] = "拖动"; +Calendar._TT["PART_TODAY"] = " (今日)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "一周开始于 %s"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "关闭"; +Calendar._TT["TODAY"] = "今天"; +Calendar._TT["TIME_PART"] = "Shift加点击或者拖动来变更"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "星期%a %b%e日"; + +Calendar._TT["WK"] = "周"; +Calendar._TT["TIME"] = "时间:"; diff --git a/app/webroot/js/context_menu.js b/app/webroot/js/context_menu.js new file mode 100644 index 0000000..955650d --- /dev/null +++ b/app/webroot/js/context_menu.js @@ -0,0 +1,221 @@ +/* redMine - project management software + Copyright (C) 2006-2008 Jean-Philippe Lang */ + +var observingContextMenuClick; + +ContextMenu = Class.create(); +ContextMenu.prototype = { + initialize: function (url) { + this.url = url; + + // prevent selection when using Ctrl/Shit key + var tables = $$('table.issues'); + for (i=0; i<tables.length; i++) { + tables[i].onselectstart = function () { return false; } // ie + tables[i].onmousedown = function () { return false; } // mozilla + } + + if (!observingContextMenuClick) { + Event.observe(document, 'click', this.Click.bindAsEventListener(this)); + Event.observe(document, (window.opera ? 'click' : 'contextmenu'), this.RightClick.bindAsEventListener(this)); + observingContextMenuClick = true; + } + + this.unselectAll(); + this.lastSelected = null; + }, + + RightClick: function(e) { + this.hideMenu(); + // do not show the context menu on links + if (Event.element(e).tagName == 'A') { return; } + // right-click simulated by Alt+Click with Opera + if (window.opera && !e.altKey) { return; } + var tr = Event.findElement(e, 'tr'); + if (tr == document || tr == undefined || !tr.hasClassName('hascontextmenu')) { return; } + Event.stop(e); + if (!this.isSelected(tr)) { + this.unselectAll(); + this.addSelection(tr); + this.lastSelected = tr; + } + this.showMenu(e); + }, + + Click: function(e) { + this.hideMenu(); + if (Event.element(e).tagName == 'A') { return; } + if (window.opera && e.altKey) { return; } + if (Event.isLeftClick(e) || (navigator.appVersion.match(/\bMSIE\b/))) { + var tr = Event.findElement(e, 'tr'); + if (tr!=null && tr!=document && tr.hasClassName('hascontextmenu')) { + // a row was clicked, check if the click was on checkbox + var box = Event.findElement(e, 'input'); + if (box!=document && box!=undefined) { + // a checkbox may be clicked + if (box.checked) { + tr.addClassName('context-menu-selection'); + } else { + tr.removeClassName('context-menu-selection'); + } + } else { + if (e.ctrlKey) { + this.toggleSelection(tr); + } else if (e.shiftKey) { + if (this.lastSelected != null) { + var toggling = false; + var rows = $$('.hascontextmenu'); + for (i=0; i<rows.length; i++) { + if (toggling || rows[i]==tr) { + this.addSelection(rows[i]); + } + if (rows[i]==tr || rows[i]==this.lastSelected) { + toggling = !toggling; + } + } + } else { + this.addSelection(tr); + } + } else { + this.unselectAll(); + this.addSelection(tr); + } + this.lastSelected = tr; + } + } else { + // click is outside the rows + var t = Event.findElement(e, 'a'); + if ((t != document) && (Element.hasClassName(t, 'disabled') || Element.hasClassName(t, 'submenu'))) { + Event.stop(e); + } + } + } + else{ + this.RightClick(e); + } + }, + + showMenu: function(e) { + var mouse_x = Event.pointerX(e); + var mouse_y = Event.pointerY(e); + var render_x = mouse_x; + var render_y = mouse_y; + var dims; + var menu_width; + var menu_height; + var window_width; + var window_height; + var max_width; + var max_height; + + $('context-menu').style['left'] = (render_x + 'px'); + $('context-menu').style['top'] = (render_y + 'px'); + Element.update('context-menu', ''); + + new Ajax.Updater({success:'context-menu'}, this.url, + {asynchronous:true, + evalScripts:true, + parameters:Form.serialize(Event.findElement(e, 'form')), + onComplete:function(request){ + dims = $('context-menu').getDimensions(); + menu_width = dims.width; + menu_height = dims.height; + max_width = mouse_x + 2*menu_width; + max_height = mouse_y + menu_height; + + var ws = window_size(); + window_width = ws.width; + window_height = ws.height; + + /* display the menu above and/or to the left of the click if needed */ + if (max_width > window_width) { + render_x -= menu_width; + $('context-menu').addClassName('reverse-x'); + } else { + $('context-menu').removeClassName('reverse-x'); + } + if (max_height > window_height) { + render_y -= menu_height; + $('context-menu').addClassName('reverse-y'); + } else { + $('context-menu').removeClassName('reverse-y'); + } + if (render_x <= 0) render_x = 1; + if (render_y <= 0) render_y = 1; + $('context-menu').style['left'] = (render_x + 'px'); + $('context-menu').style['top'] = (render_y + 'px'); + + Effect.Appear('context-menu', {duration: 0.20}); + if (window.parseStylesheets) { window.parseStylesheets(); } // IE + }}) + }, + + hideMenu: function() { + Element.hide('context-menu'); + }, + + addSelection: function(tr) { + tr.addClassName('context-menu-selection'); + this.checkSelectionBox(tr, true); + }, + + toggleSelection: function(tr) { + if (this.isSelected(tr)) { + this.removeSelection(tr); + } else { + this.addSelection(tr); + } + }, + + removeSelection: function(tr) { + tr.removeClassName('context-menu-selection'); + this.checkSelectionBox(tr, false); + }, + + unselectAll: function() { + var rows = $$('.hascontextmenu'); + for (i=0; i<rows.length; i++) { + this.removeSelection(rows[i]); + } + }, + + checkSelectionBox: function(tr, checked) { + var inputs = Element.getElementsBySelector(tr, 'input'); + if (inputs.length > 0) { inputs[0].checked = checked; } + }, + + isSelected: function(tr) { + return Element.hasClassName(tr, 'context-menu-selection'); + } +} + +function toggleIssuesSelection(el) { + var boxes = el.getElementsBySelector('input[type=checkbox]'); + var all_checked = true; + for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } } + for (i = 0; i < boxes.length; i++) { + if (all_checked) { + boxes[i].checked = false; + boxes[i].up('tr').removeClassName('context-menu-selection'); + } else if (boxes[i].checked == false) { + boxes[i].checked = true; + boxes[i].up('tr').addClassName('context-menu-selection'); + } + } +} + +function window_size() { + var w; + var h; + if (window.innerWidth) { + w = window.innerWidth; + h = window.innerHeight; + } else if (document.documentElement) { + w = document.documentElement.clientWidth; + h = document.documentElement.clientHeight; + } else { + w = document.body.clientWidth; + h = document.body.clientHeight; + } + return {width: w, height: h}; +} diff --git a/app/webroot/js/controls.js b/app/webroot/js/controls.js new file mode 100644 index 0000000..5aaf0bb --- /dev/null +++ b/app/webroot/js/controls.js @@ -0,0 +1,963 @@ +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2007 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005-2007 Jon Tirsen (http://www.tirsen.com) +// Contributors: +// Richard Livsey +// Rahul Bhargava +// Rob Wills +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +// 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. + +if(typeof Effect == 'undefined') + throw("controls.js requires including script.aculo.us' effects.js library"); + +var Autocompleter = { } +Autocompleter.Base = Class.create({ + baseInitialize: function(element, update, options) { + element = $(element) + this.element = element; + this.update = $(update); + this.hasFocus = false; + this.changed = false; + this.active = false; + this.index = 0; + this.entryCount = 0; + this.oldElementValue = this.element.value; + + 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); + // Force carriage returns as token delimiters anyway + if (!this.options.tokens.include('\n')) + this.options.tokens.push('\n'); + + 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, 'keydown', this.onKeyPress.bindAsEventListener(this)); + }, + + show: function() { + if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); + if(!this.iefix && + (Prototype.Browser.IE) && + (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, {setTop:(!this.update.style.height)}); + 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(); + Event.stop(event); + return; + case Event.KEY_DOWN: + this.markNext(); + this.render(); + Event.stop(event); + return; + } + else + if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || + (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; + + this.changed = true; + this.hasFocus = true; + + if(this.observer) clearTimeout(this.observer); + this.observer = + setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); + }, + + activate: function() { + this.changed = false; + this.hasFocus = true; + this.getUpdatedChoices(); + }, + + 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; + this.getEntry(this.index).scrollIntoView(true); + }, + + markNext: function() { + if(this.index < this.entryCount-1) this.index++ + else this.index = 0; + this.getEntry(this.index).scrollIntoView(false); + }, + + 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 = ''; + if (this.options.select) { + var nodes = $(selectedElement).select('.' + this.options.select) || []; + if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); + } else + value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); + + var bounds = this.getTokenBounds(); + if (bounds[0] != -1) { + var newValue = this.element.value.substr(0, bounds[0]); + var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/); + if (whitespace) + newValue += whitespace[0]; + this.element.value = newValue + value + this.element.value.substr(bounds[1]); + } else { + this.element.value = value; + } + this.oldElementValue = this.element.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.down()); + + if(this.update.firstChild && this.update.down().childNodes) { + this.entryCount = + this.update.down().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; + + if(this.entryCount==1 && this.options.autoSelect) { + this.selectEntry(); + this.hide(); + } else { + 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; + this.tokenBounds = null; + if(this.getToken().length>=this.options.minChars) { + this.getUpdatedChoices(); + } else { + this.active = false; + this.hide(); + } + this.oldElementValue = this.element.value; + }, + + getToken: function() { + var bounds = this.getTokenBounds(); + return this.element.value.substring(bounds[0], bounds[1]).strip(); + }, + + getTokenBounds: function() { + if (null != this.tokenBounds) return this.tokenBounds; + var value = this.element.value; + if (value.strip().empty()) return [-1, 0]; + var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue); + var offset = (diff == this.oldElementValue.length ? 1 : 0); + var prevTokenPos = -1, nextTokenPos = value.length; + var tp; + for (var index = 0, l = this.options.tokens.length; index < l; ++index) { + tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1); + if (tp > prevTokenPos) prevTokenPos = tp; + tp = value.indexOf(this.options.tokens[index], diff + offset); + if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp; + } + return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); + } +}); + +Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) { + var boundary = Math.min(newS.length, oldS.length); + for (var index = 0; index < boundary; ++index) + if (newS[index] != oldS[index]) + return index; + return boundary; +}; + +Ajax.Autocompleter = Class.create(Autocompleter.Base, { + 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() { + this.startIndicator(); + + var 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.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 and collection editor +// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007). + +// 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({ + initialize: function(element, url, options) { + this.url = url; + this.element = element = $(element); + this.prepareOptions(); + this._controls = { }; + arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!! + Object.extend(this.options, options || { }); + if (!this.options.formId && this.element.id) { + this.options.formId = this.element.id + '-inplaceeditor'; + if ($(this.options.formId)) + this.options.formId = ''; + } + if (this.options.externalControl) + this.options.externalControl = $(this.options.externalControl); + if (!this.options.externalControl) + this.options.externalControlOnly = false; + this._originalBackground = this.element.getStyle('background-color') || 'transparent'; + this.element.title = this.options.clickToEditText; + this._boundCancelHandler = this.handleFormCancellation.bind(this); + this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this); + this._boundFailureHandler = this.handleAJAXFailure.bind(this); + this._boundSubmitHandler = this.handleFormSubmission.bind(this); + this._boundWrapperHandler = this.wrapUp.bind(this); + this.registerListeners(); + }, + checkForEscapeOrReturn: function(e) { + if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return; + if (Event.KEY_ESC == e.keyCode) + this.handleFormCancellation(e); + else if (Event.KEY_RETURN == e.keyCode) + this.handleFormSubmission(e); + }, + createControl: function(mode, handler, extraClasses) { + var control = this.options[mode + 'Control']; + var text = this.options[mode + 'Text']; + if ('button' == control) { + var btn = document.createElement('input'); + btn.type = 'submit'; + btn.value = text; + btn.className = 'editor_' + mode + '_button'; + if ('cancel' == mode) + btn.onclick = this._boundCancelHandler; + this._form.appendChild(btn); + this._controls[mode] = btn; + } else if ('link' == control) { + var link = document.createElement('a'); + link.href = '#'; + link.appendChild(document.createTextNode(text)); + link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; + link.className = 'editor_' + mode + '_link'; + if (extraClasses) + link.className += ' ' + extraClasses; + this._form.appendChild(link); + this._controls[mode] = link; + } + }, + createEditField: function() { + var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); + var fld; + if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { + fld = document.createElement('input'); + fld.type = 'text'; + var size = this.options.size || this.options.cols || 0; + if (0 < size) fld.size = size; + } else { + fld = document.createElement('textarea'); + fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows); + fld.cols = this.options.cols || 40; + } + fld.name = this.options.paramName; + fld.value = text; // No HTML breaks conversion anymore + fld.className = 'editor_field'; + if (this.options.submitOnBlur) + fld.onblur = this._boundSubmitHandler; + this._controls.editor = fld; + if (this.options.loadTextURL) + this.loadExternalText(); + this._form.appendChild(this._controls.editor); + }, + createForm: function() { + var ipe = this; + function addText(mode, condition) { + var text = ipe.options['text' + mode + 'Controls']; + if (!text || condition === false) return; + ipe._form.appendChild(document.createTextNode(text)); + }; + this._form = $(document.createElement('form')); + this._form.id = this.options.formId; + this._form.addClassName(this.options.formClassName); + this._form.onsubmit = this._boundSubmitHandler; + this.createEditField(); + if ('textarea' == this._controls.editor.tagName.toLowerCase()) + this._form.appendChild(document.createElement('br')); + if (this.options.onFormCustomization) + this.options.onFormCustomization(this, this._form); + addText('Before', this.options.okControl || this.options.cancelControl); + this.createControl('ok', this._boundSubmitHandler); + addText('Between', this.options.okControl && this.options.cancelControl); + this.createControl('cancel', this._boundCancelHandler, 'editor_cancel'); + addText('After', this.options.okControl || this.options.cancelControl); + }, + destroy: function() { + if (this._oldInnerHTML) + this.element.innerHTML = this._oldInnerHTML; + this.leaveEditMode(); + this.unregisterListeners(); + }, + enterEditMode: function(e) { + if (this._saving || this._editing) return; + this._editing = true; + this.triggerCallback('onEnterEditMode'); + if (this.options.externalControl) + this.options.externalControl.hide(); + this.element.hide(); + this.createForm(); + this.element.parentNode.insertBefore(this._form, this.element); + if (!this.options.loadTextURL) + this.postProcessEditField(); + if (e) Event.stop(e); + }, + enterHover: function(e) { + if (this.options.hoverClassName) + this.element.addClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onEnterHover'); + }, + getText: function() { + return this.element.innerHTML; + }, + handleAJAXFailure: function(transport) { + this.triggerCallback('onFailure', transport); + if (this._oldInnerHTML) { + this.element.innerHTML = this._oldInnerHTML; + this._oldInnerHTML = null; + } + }, + handleFormCancellation: function(e) { + this.wrapUp(); + if (e) Event.stop(e); + }, + handleFormSubmission: function(e) { + var form = this._form; + var value = $F(this._controls.editor); + this.prepareSubmission(); + var params = this.options.callback(form, value) || ''; + if (Object.isString(params)) + params = params.toQueryParams(); + params.editorId = this.element.id; + if (this.options.htmlResponse) { + var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Updater({ success: this.element }, this.url, options); + } else { + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.url, options); + } + if (e) Event.stop(e); + }, + leaveEditMode: function() { + this.element.removeClassName(this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); + if (this.options.externalControl) + this.options.externalControl.show(); + this._saving = false; + this._editing = false; + this._oldInnerHTML = null; + this.triggerCallback('onLeaveEditMode'); + }, + leaveHover: function(e) { + if (this.options.hoverClassName) + this.element.removeClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onLeaveHover'); + }, + loadExternalText: function() { + this._form.addClassName(this.options.loadingClassName); + this._controls.editor.disabled = true; + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._form.removeClassName(this.options.loadingClassName); + var text = transport.responseText; + if (this.options.stripLoadedTextTags) + text = text.stripTags(); + this._controls.editor.value = text; + this._controls.editor.disabled = false; + this.postProcessEditField(); + }.bind(this), + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + postProcessEditField: function() { + var fpc = this.options.fieldPostCreation; + if (fpc) + $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); + }, + prepareOptions: function() { + this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions); + Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks); + [this._extraDefaultOptions].flatten().compact().each(function(defs) { + Object.extend(this.options, defs); + }.bind(this)); + }, + prepareSubmission: function() { + this._saving = true; + this.removeForm(); + this.leaveHover(); + this.showSaving(); + }, + registerListeners: function() { + this._listeners = { }; + var listener; + $H(Ajax.InPlaceEditor.Listeners).each(function(pair) { + listener = this[pair.value].bind(this); + this._listeners[pair.key] = listener; + if (!this.options.externalControlOnly) + this.element.observe(pair.key, listener); + if (this.options.externalControl) + this.options.externalControl.observe(pair.key, listener); + }.bind(this)); + }, + removeForm: function() { + if (!this._form) return; + this._form.remove(); + this._form = null; + this._controls = { }; + }, + showSaving: function() { + this._oldInnerHTML = this.element.innerHTML; + this.element.innerHTML = this.options.savingText; + this.element.addClassName(this.options.savingClassName); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); + }, + triggerCallback: function(cbName, arg) { + if ('function' == typeof this.options[cbName]) { + this.options[cbName](this, arg); + } + }, + unregisterListeners: function() { + $H(this._listeners).each(function(pair) { + if (!this.options.externalControlOnly) + this.element.stopObserving(pair.key, pair.value); + if (this.options.externalControl) + this.options.externalControl.stopObserving(pair.key, pair.value); + }.bind(this)); + }, + wrapUp: function(transport) { + this.leaveEditMode(); + // Can't use triggerCallback due to backward compatibility: requires + // binding + direct element + this._boundComplete(transport, this.element); + } +}); + +Object.extend(Ajax.InPlaceEditor.prototype, { + dispose: Ajax.InPlaceEditor.prototype.destroy +}); + +Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, { + initialize: function($super, element, url, options) { + this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions; + $super(element, url, options); + }, + + createEditField: function() { + var list = document.createElement('select'); + list.name = this.options.paramName; + list.size = 1; + this._controls.editor = list; + this._collection = this.options.collection || []; + if (this.options.loadCollectionURL) + this.loadCollection(); + else + this.checkForExternalText(); + this._form.appendChild(this._controls.editor); + }, + + loadCollection: function() { + this._form.addClassName(this.options.loadingClassName); + this.showLoadingText(this.options.loadingCollectionText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + var js = transport.responseText.strip(); + if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check + throw 'Server returned an invalid collection representation.'; + this._collection = eval(js); + this.checkForExternalText(); + }.bind(this), + onFailure: this.onFailure + }); + new Ajax.Request(this.options.loadCollectionURL, options); + }, + + showLoadingText: function(text) { + this._controls.editor.disabled = true; + var tempOption = this._controls.editor.firstChild; + if (!tempOption) { + tempOption = document.createElement('option'); + tempOption.value = ''; + this._controls.editor.appendChild(tempOption); + tempOption.selected = true; + } + tempOption.update((text || '').stripScripts().stripTags()); + }, + + checkForExternalText: function() { + this._text = this.getText(); + if (this.options.loadTextURL) + this.loadExternalText(); + else + this.buildOptionList(); + }, + + loadExternalText: function() { + this.showLoadingText(this.options.loadingText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._text = transport.responseText.strip(); + this.buildOptionList(); + }.bind(this), + onFailure: this.onFailure + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + + buildOptionList: function() { + this._form.removeClassName(this.options.loadingClassName); + this._collection = this._collection.map(function(entry) { + return 2 === entry.length ? entry : [entry, entry].flatten(); + }); + var marker = ('value' in this.options) ? this.options.value : this._text; + var textFound = this._collection.any(function(entry) { + return entry[0] == marker; + }.bind(this)); + this._controls.editor.update(''); + var option; + this._collection.each(function(entry, index) { + option = document.createElement('option'); + option.value = entry[0]; + option.selected = textFound ? entry[0] == marker : 0 == index; + option.appendChild(document.createTextNode(entry[1])); + this._controls.editor.appendChild(option); + }.bind(this)); + this._controls.editor.disabled = false; + Field.scrollFreeActivate(this._controls.editor); + } +}); + +//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! **** +//**** This only exists for a while, in order to let **** +//**** users adapt to the new API. Read up on the new **** +//**** API and convert your code to it ASAP! **** + +Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) { + if (!options) return; + function fallback(name, expr) { + if (name in options || expr === undefined) return; + options[name] = expr; + }; + fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' : + options.cancelLink == options.cancelButton == false ? false : undefined))); + fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' : + options.okLink == options.okButton == false ? false : undefined))); + fallback('highlightColor', options.highlightcolor); + fallback('highlightEndColor', options.highlightendcolor); +}; + +Object.extend(Ajax.InPlaceEditor, { + DefaultOptions: { + ajaxOptions: { }, + autoRows: 3, // Use when multi-line w/ rows == 1 + cancelControl: 'link', // 'link'|'button'|false + cancelText: 'cancel', + clickToEditText: 'Click to edit', + externalControl: null, // id|elt + externalControlOnly: false, + fieldPostCreation: 'activate', // 'activate'|'focus'|false + formClassName: 'inplaceeditor-form', + formId: null, // id|elt + highlightColor: '#ffff99', + highlightEndColor: '#ffffff', + hoverClassName: '', + htmlResponse: true, + loadingClassName: 'inplaceeditor-loading', + loadingText: 'Loading...', + okControl: 'button', // 'link'|'button'|false + okText: 'ok', + paramName: 'value', + rows: 1, // If 1 and multi-line, uses autoRows + savingClassName: 'inplaceeditor-saving', + savingText: 'Saving...', + size: 0, + stripLoadedTextTags: false, + submitOnBlur: false, + textAfterControls: '', + textBeforeControls: '', + textBetweenControls: '' + }, + DefaultCallbacks: { + callback: function(form) { + return Form.serialize(form); + }, + onComplete: function(transport, element) { + // For backward compatibility, this one is bound to the IPE, and passes + // the element directly. It was too often customized, so we don't break it. + new Effect.Highlight(element, { + startcolor: this.options.highlightColor, keepBackgroundImage: true }); + }, + onEnterEditMode: null, + onEnterHover: function(ipe) { + ipe.element.style.backgroundColor = ipe.options.highlightColor; + if (ipe._effect) + ipe._effect.cancel(); + }, + onFailure: function(transport, ipe) { + alert('Error communication with the server: ' + transport.responseText.stripTags()); + }, + onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls. + onLeaveEditMode: null, + onLeaveHover: function(ipe) { + ipe._effect = new Effect.Highlight(ipe.element, { + startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor, + restorecolor: ipe._originalBackground, keepBackgroundImage: true + }); + } + }, + Listeners: { + click: 'enterEditMode', + keydown: 'checkForEscapeOrReturn', + mouseover: 'enterHover', + mouseout: 'leaveHover' + } +}); + +Ajax.InPlaceCollectionEditor.DefaultOptions = { + loadingCollectionText: 'Loading options...' +}; + +// Delayed observer, like Form.Element.Observer, +// but waits for delay after last key input +// Ideal for live-search fields + +Form.Element.DelayedObserver = Class.create({ + 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)); + } +}); diff --git a/app/webroot/js/dragdrop.js b/app/webroot/js/dragdrop.js new file mode 100644 index 0000000..bf5cfea --- /dev/null +++ b/app/webroot/js/dragdrop.js @@ -0,0 +1,972 @@ +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2007 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +if(Object.isUndefined(Effect)) + throw("dragdrop.js requires including script.aculo.us' effects.js library"); + +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, + tree: false + }, arguments[1] || { }); + + // cache containers + if(options.containment) { + options._containers = []; + var containment = options.containment; + if(Object.isArray(containment)) { + 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); + }, + + findDeepestChild: function(drops) { + deepest = drops[0]; + + for (i = 1; i < drops.length; ++i) + if (Element.isParent(drops[i].element, deepest.element)) + deepest = drops[i]; + + return deepest; + }, + + isContained: function(element, drop) { + var containmentNode; + if(drop.tree) { + containmentNode = element.treeNode; + } else { + containmentNode = element.parentNode; + } + return drop._containers.detect(function(c) { return containmentNode == 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; + var drop, affected = []; + + this.drops.each( function(drop) { + if(Droppables.isAffected(point, element, drop)) + affected.push(drop); + }); + + if(affected.length>0) + drop = Droppables.findDeepestChild(affected); + + if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); + if (drop) { + Position.within(drop.element, point[0], point[1]); + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + + if (drop != this.last_active) Droppables.activate(drop); + } + }, + + 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); + return true; + } + }, + + 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) { + if(draggable.options.delay) { + this._timeout = setTimeout(function() { + Draggables._timeout = null; + window.focus(); + Draggables.activeDraggable = draggable; + }.bind(this), draggable.options.delay); + } else { + window.focus(); // allows keypress events if window isn't currently focused, fails for Safari + this.activeDraggable = draggable; + } + }, + + deactivate: function() { + 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._timeout) { + clearTimeout(this._timeout); + this._timeout = null; + } + if(!this.activeDraggable) return; + this._lastPointer = null; + this.activeDraggable.endDrag(event); + this.activeDraggable = null; + }, + + 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); + }); + if(draggable.options[eventName]) draggable.options[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({ + initialize: function(element) { + var defaults = { + handle: false, + reverteffect: function(element, top_offset, left_offset) { + var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; + new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, + queue: {scope:'_draggable', position:'end'} + }); + }, + endeffect: function(element) { + var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; + new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, + queue: {scope:'_draggable', position:'end'}, + afterFinish: function(){ + Draggable._dragging[element] = false + } + }); + }, + zindex: 1000, + revert: false, + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } + delay: 0 + }; + + if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) + Object.extend(defaults, { + starteffect: function(element) { + element._opacity = Element.getOpacity(element); + Draggable._dragging[element] = true; + new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); + } + }); + + var options = Object.extend(defaults, arguments[1] || { }); + + this.element = $(element); + + if(options.handle && Object.isString(options.handle)) + this.handle = this.element.down('.'+options.handle, 0); + + if(!this.handle) this.handle = $(options.handle); + if(!this.handle) this.handle = this.element; + + if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { + options.scroll = $(options.scroll); + this._isScrollChild = Element.childOf(this.element, options.scroll); + } + + Element.makePositioned(this.element); // fix IE + + 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(Element.getStyle(this.element,'left') || '0'), + parseInt(Element.getStyle(this.element,'top') || '0')]); + }, + + initDrag: function(event) { + if(!Object.isUndefined(Draggable._dragging[this.element]) && + Draggable._dragging[this.element]) return; + if(Event.isLeftClick(event)) { + // abort on form elements, fixes a Firefox issue + var src = Event.element(event); + if((tag_name = src.tagName.toUpperCase()) && ( + tag_name=='INPUT' || + tag_name=='SELECT' || + tag_name=='OPTION' || + tag_name=='BUTTON' || + tag_name=='TEXTAREA')) return; + + 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.delta) + this.delta = this.currentDelta(); + + 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); + this.element._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); + if (!this.element._originallyAbsolute) + Position.absolutize(this.element); + this.element.parentNode.insertBefore(this._clone, this.element); + } + + if(this.options.scroll) { + if (this.options.scroll == window) { + var where = this._getWindowScroll(this.options.scroll); + this.originalScrollLeft = where.left; + this.originalScrollTop = where.top; + } else { + this.originalScrollLeft = this.options.scroll.scrollLeft; + this.originalScrollTop = this.options.scroll.scrollTop; + } + } + + Draggables.notify('onStart', this, event); + + if(this.options.starteffect) this.options.starteffect(this.element); + }, + + updateDrag: function(event, pointer) { + if(!this.dragging) this.startDrag(event); + + if(!this.options.quiet){ + Position.prepare(); + Droppables.show(pointer, this.element); + } + + Draggables.notify('onDrag', this, event); + + this.draw(pointer); + if(this.options.change) this.options.change(this); + + if(this.options.scroll) { + this.stopScrolling(); + + var p; + if (this.options.scroll == window) { + with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } + } else { + p = Position.page(this.options.scroll); + p[0] += this.options.scroll.scrollLeft + Position.deltaX; + p[1] += this.options.scroll.scrollTop + Position.deltaY; + p.push(p[0]+this.options.scroll.offsetWidth); + p.push(p[1]+this.options.scroll.offsetHeight); + } + var speed = [0,0]; + if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); + if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); + if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); + if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); + this.startScrolling(speed); + } + + // fix AppleWebKit rendering + if(Prototype.Browser.WebKit) window.scrollBy(0,0); + + Event.stop(event); + }, + + finishDrag: function(event, success) { + this.dragging = false; + + if(this.options.quiet){ + Position.prepare(); + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + Droppables.show(pointer, this.element); + } + + if(this.options.ghosting) { + if (!this.element._originallyAbsolute) + Position.relativize(this.element); + delete this.element._originallyAbsolute; + Element.remove(this._clone); + this._clone = null; + } + + var dropped = false; + if(success) { + dropped = Droppables.fire(event, this.element); + if (!dropped) dropped = false; + } + if(dropped && this.options.onDropped) this.options.onDropped(this.element); + Draggables.notify('onEnd', this, event); + + var revert = this.options.revert; + if(revert && Object.isFunction(revert)) revert = revert(this.element); + + var d = this.currentDelta(); + if(revert && this.options.reverteffect) { + if (dropped == 0 || revert != 'failure') + 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.stopScrolling(); + this.finishDrag(event, true); + Event.stop(event); + }, + + draw: function(point) { + var pos = Position.cumulativeOffset(this.element); + if(this.options.ghosting) { + var r = Position.realOffset(this.element); + pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; + } + + var d = this.currentDelta(); + pos[0] -= d[0]; pos[1] -= d[1]; + + if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { + pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; + pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; + } + + var p = [0,1].map(function(i){ + return (point[i]-pos[i]-this.offset[i]) + }.bind(this)); + + if(this.options.snap) { + if(Object.isFunction(this.options.snap)) { + p = this.options.snap(p[0],p[1],this); + } else { + if(Object.isArray(this.options.snap)) { + p = p.map( function(v, i) { + return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)) + } else { + p = p.map( function(v) { + return (v/this.options.snap).round()*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 + }, + + stopScrolling: function() { + if(this.scrollInterval) { + clearInterval(this.scrollInterval); + this.scrollInterval = null; + Draggables._lastScrollPointer = null; + } + }, + + startScrolling: function(speed) { + if(!(speed[0] || speed[1])) return; + this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; + this.lastScrolled = new Date(); + this.scrollInterval = setInterval(this.scroll.bind(this), 10); + }, + + scroll: function() { + var current = new Date(); + var delta = current - this.lastScrolled; + this.lastScrolled = current; + if(this.options.scroll == window) { + with (this._getWindowScroll(this.options.scroll)) { + if (this.scrollSpeed[0] || this.scrollSpeed[1]) { + var d = delta / 1000; + this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); + } + } + } else { + this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; + this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; + } + + Position.prepare(); + Droppables.show(Draggables._lastPointer, this.element); + Draggables.notify('onDrag', this); + if (this._isScrollChild) { + Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); + Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; + Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; + if (Draggables._lastScrollPointer[0] < 0) + Draggables._lastScrollPointer[0] = 0; + if (Draggables._lastScrollPointer[1] < 0) + Draggables._lastScrollPointer[1] = 0; + this.draw(Draggables._lastScrollPointer); + } + + if(this.options.change) this.options.change(this); + }, + + _getWindowScroll: function(w) { + var T, L, W, H; + with (w.document) { + if (w.document.documentElement && documentElement.scrollTop) { + T = documentElement.scrollTop; + L = documentElement.scrollLeft; + } else if (w.document.body) { + T = body.scrollTop; + L = body.scrollLeft; + } + if (w.innerWidth) { + W = w.innerWidth; + H = w.innerHeight; + } else if (w.document.documentElement && documentElement.clientWidth) { + W = documentElement.clientWidth; + H = documentElement.clientHeight; + } else { + W = body.offsetWidth; + H = body.offsetHeight + } + } + return { top: T, left: L, width: W, height: H }; + } +}); + +Draggable._dragging = { }; + +/*--------------------------------------------------------------------------*/ + +var SortableObserver = Class.create({ + 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 = { + SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, + + sortables: { }, + + _findRootElement: function(element) { + while (element.tagName.toUpperCase() != "BODY") { + if(element.id && Sortable.sortables[element.id]) return element; + element = element.parentNode; + } + }, + + options: function(element) { + element = Sortable._findRootElement($(element)); + if(!element) return; + return Sortable.sortables[element.id]; + }, + + destroy: function(element){ + var s = Sortable.options(element); + + if(s) { + Draggables.removeObserver(s.element); + s.droppables.each(function(d){ Droppables.remove(d) }); + s.draggables.invoke('destroy'); + + delete Sortable.sortables[s.element.id]; + } + }, + + create: function(element) { + element = $(element); + var options = Object.extend({ + element: element, + tag: 'li', // assumes li children, override with tag: 'tagname' + dropOnEmpty: false, + tree: false, + treeTag: 'ul', + 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, + delay: 0, + hoverclass: null, + ghosting: false, + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + format: this.SERIALIZE_RULE, + + // these take arrays of elements or ids and can be + // used for better initialization performance + elements: false, + handles: false, + + 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, + quiet: options.quiet, + scroll: options.scroll, + scrollSpeed: options.scrollSpeed, + scrollSensitivity: options.scrollSensitivity, + delay: options.delay, + 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, + tree: options.tree, + hoverclass: options.hoverclass, + onHover: Sortable.onHover + } + + var options_for_tree = { + onHover: Sortable.onEmptyHover, + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass + } + + // fix for gecko engine + Element.cleanWhitespace(element); + + options.draggables = []; + options.droppables = []; + + // drop on empty handling + if(options.dropOnEmpty || options.tree) { + Droppables.add(element, options_for_tree); + options.droppables.push(element); + } + + (options.elements || this.findElements(element, options) || []).each( function(e,i) { + var handle = options.handles ? $(options.handles[i]) : + (options.handle ? $(e).select('.' + options.handle)[0] : e); + options.draggables.push( + new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); + Droppables.add(e, options_for_droppable); + if(options.tree) e.treeNode = element; + options.droppables.push(e); + }); + + if(options.tree) { + (Sortable.findTreeElements(element, options) || []).each( function(e) { + Droppables.add(e, options_for_tree); + e.treeNode = element; + options.droppables.push(e); + }); + } + + // keep reference + this.sortables[element.id] = options; + + // for onupdate + Draggables.addObserver(new SortableObserver(element, options.onUpdate)); + + }, + + // return all suitable-for-sortable elements in a guaranteed order + findElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.tag); + }, + + findTreeElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.treeTag); + }, + + onHover: function(element, dropon, overlap) { + if(Element.isParent(dropon, element)) return; + + if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { + return; + } else 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, overlap) { + var oldParentNode = element.parentNode; + var droponOptions = Sortable.options(dropon); + + if(!Element.isParent(dropon, element)) { + var index; + + var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); + var child = null; + + if(children) { + var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); + + for (index = 0; index < children.length; index += 1) { + if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { + offset -= Element.offsetSize (children[index], droponOptions.overlap); + } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { + child = index + 1 < children.length ? children[index + 1] : null; + break; + } else { + child = children[index]; + break; + } + } + } + + dropon.insertBefore(element, child); + + Sortable.options(oldParentNode).onChange(element); + droponOptions.onChange(element); + } + }, + + unmark: function() { + if(Sortable._marker) Sortable._marker.hide(); + }, + + 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') || Element.extend(document.createElement('DIV'))). + hide().addClassName('dropmarker').setStyle({position:'absolute'}); + document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); + } + var offsets = Position.cumulativeOffset(dropon); + Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); + + if(position=='after') + if(sortable.overlap == 'horizontal') + Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); + else + Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); + + Sortable._marker.show(); + }, + + _tree: function(element, options, parent) { + var children = Sortable.findElements(element, options) || []; + + for (var i = 0; i < children.length; ++i) { + var match = children[i].id.match(options.format); + + if (!match) continue; + + var child = { + id: encodeURIComponent(match ? match[1] : null), + element: element, + parent: parent, + children: [], + position: parent.children.length, + container: $(children[i]).down(options.treeTag) + } + + /* Get the element containing the children and recurse over it */ + if (child.container) + this._tree(child.container, options, child) + + parent.children.push (child); + } + + return parent; + }, + + tree: function(element) { + element = $(element); + var sortableOptions = this.options(element); + var options = Object.extend({ + tag: sortableOptions.tag, + treeTag: sortableOptions.treeTag, + only: sortableOptions.only, + name: element.id, + format: sortableOptions.format + }, arguments[1] || { }); + + var root = { + id: null, + parent: null, + children: [], + container: element, + position: 0 + } + + return Sortable._tree(element, options, root); + }, + + /* Construct a [i] index for a particular node */ + _constructIndex: function(node) { + var index = ''; + do { + if (node.id) index = '[' + node.position + ']' + index; + } while ((node = node.parent) != null); + return index; + }, + + sequence: function(element) { + element = $(element); + var options = Object.extend(this.options(element), arguments[1] || { }); + + return $(this.findElements(element, options) || []).map( function(item) { + return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; + }); + }, + + setSequence: function(element, new_sequence) { + element = $(element); + var options = Object.extend(this.options(element), arguments[2] || { }); + + var nodeMap = { }; + this.findElements(element, options).each( function(n) { + if (n.id.match(options.format)) + nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; + n.parentNode.removeChild(n); + }); + + new_sequence.each(function(ident) { + var n = nodeMap[ident]; + if (n) { + n[1].appendChild(n[0]); + delete nodeMap[ident]; + } + }); + }, + + serialize: function(element) { + element = $(element); + var options = Object.extend(Sortable.options(element), arguments[1] || { }); + var name = encodeURIComponent( + (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); + + if (options.tree) { + return Sortable.tree(element, arguments[1]).children.map( function (item) { + return [name + Sortable._constructIndex(item) + "[id]=" + + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); + }).flatten().join('&'); + } else { + return Sortable.sequence(element, arguments[1]).map( function(item) { + return name + "[]=" + encodeURIComponent(item); + }).join('&'); + } + } +} + +// Returns true if child is contained within element +Element.isParent = function(child, element) { + if (!child.parentNode || child == element) return false; + if (child.parentNode == element) return true; + return Element.isParent(child.parentNode, element); +} + +Element.findChildren = function(element, only, recursive, tagName) { + if(!element.hasChildNodes()) return null; + tagName = tagName.toUpperCase(); + if(only) only = [only].flatten(); + var elements = []; + $A(element.childNodes).each( function(e) { + if(e.tagName && e.tagName.toUpperCase()==tagName && + (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) + elements.push(e); + if(recursive) { + var grandchildren = Element.findChildren(e, only, recursive, tagName); + if(grandchildren) elements.push(grandchildren); + } + }); + + return (elements.length>0 ? elements.flatten() : []); +} + +Element.offsetSize = function (element, type) { + return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; +} diff --git a/app/webroot/js/effects.js b/app/webroot/js/effects.js new file mode 100644 index 0000000..f030b5d --- /dev/null +++ b/app/webroot/js/effects.js @@ -0,0 +1,1120 @@ +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Contributors: +// Justin Palmer (http://encytemedia.com/) +// Mark Pilgrim (http://diveintomark.org/) +// Martin Bialasinki +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +// 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.collectTextNodes = function(element) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); + }).flatten().join(''); +}; + +Element.collectTextNodesIgnoreClass = function(element, className) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? + Element.collectTextNodesIgnoreClass(node, className) : '')); + }).flatten().join(''); +}; + +Element.setContentZoom = function(element, percent) { + element = $(element); + element.setStyle({fontSize: (percent/100) + 'em'}); + if (Prototype.Browser.WebKit) window.scrollBy(0,0); + return element; +}; + +Element.getInlineOpacity = function(element){ + return $(element).style.opacity || ''; +}; + +Element.forceRerendering = function(element) { + try { + element = $(element); + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch(e) { } +}; + +/*--------------------------------------------------------------------------*/ + +var Effect = { + _elementDoesNotExistError: { + name: 'ElementDoesNotExistError', + message: 'The specified DOM element does not exist, but is required for this effect to operate' + }, + Transitions: { + linear: Prototype.K, + sinoidal: function(pos) { + return (-Math.cos(pos*Math.PI)/2) + 0.5; + }, + reverse: function(pos) { + return 1-pos; + }, + flicker: function(pos) { + var pos = ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; + return pos > 1 ? 1 : pos; + }, + wobble: function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; + }, + pulse: function(pos, pulses) { + pulses = pulses || 5; + return ( + ((pos % (1/pulses)) * pulses).round() == 0 ? + ((pos * pulses * 2) - (pos * pulses * 2).floor()) : + 1 - ((pos * pulses * 2) - (pos * pulses * 2).floor()) + ); + }, + spring: function(pos) { + return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); + }, + none: function(pos) { + return 0; + }, + full: function(pos) { + return 1; + } + }, + DefaultOptions: { + duration: 1.0, // seconds + fps: 100, // 100= assume 66fps max. + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' + }, + tagifyText: function(element) { + var tagifyStyle = 'position:relative'; + if (Prototype.Browser.IE) tagifyStyle += ';zoom:1'; + + element = $(element); + $A(element.childNodes).each( function(child) { + if (child.nodeType==3) { + child.nodeValue.toArray().each( function(character) { + element.insertBefore( + new Element('span', {style: tagifyStyle}).update( + character == ' ' ? String.fromCharCode(160) : character), + child); + }); + Element.remove(child); + } + }); + }, + multiple: function(element, effect) { + var elements; + if (((typeof element == 'object') || + Object.isFunction(element)) && + (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 })); + }); + }, + PAIRS: { + 'slide': ['SlideDown','SlideUp'], + 'blind': ['BlindDown','BlindUp'], + 'appear': ['Appear','Fade'] + }, + toggle: function(element, effect) { + element = $(element); + effect = (effect || 'appear').toLowerCase(); + var options = Object.extend({ + queue: { position:'end', scope:(element.id || 'global'), limit: 1 } + }, arguments[2] || { }); + Effect[element.visible() ? + Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); + } +}; + +Effect.DefaultOptions.transition = Effect.Transitions.sinoidal; + +/* ------------- core effects ------------- */ + +Effect.ScopedQueue = Class.create(Enumerable, { + initialize: function() { + this.effects = []; + this.interval = null; + }, + _each: function(iterator) { + this.effects._each(iterator); + }, + add: function(effect) { + var timestamp = new Date().getTime(); + + var position = Object.isString(effect.options.queue) ? + effect.options.queue : effect.options.queue.position; + + switch(position) { + 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 'with-last': + timestamp = this.effects.pluck('startOn').max() || timestamp; + 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; + + if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) + this.effects.push(effect); + + if (!this.interval) + this.interval = setInterval(this.loop.bind(this), 15); + }, + 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(); + for(var i=0, len=this.effects.length;i<len;i++) + this.effects[i] && this.effects[i].loop(timePos); + } +}); + +Effect.Queues = { + instances: $H(), + get: function(queueName) { + if (!Object.isString(queueName)) return queueName; + + return this.instances.get(queueName) || + this.instances.set(queueName, new Effect.ScopedQueue()); + } +}; +Effect.Queue = Effect.Queues.get('global'); + +Effect.Base = Class.create({ + position: null, + start: function(options) { + function codeForEvent(options,eventName){ + return ( + (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') + + (options[eventName] ? 'this.options.'+eventName+'(this);' : '') + ); + } + if (options && options.transition === false) options.transition = Effect.Transitions.linear; + this.options = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { }); + this.currentFrame = 0; + this.state = 'idle'; + this.startOn = this.options.delay*1000; + this.finishOn = this.startOn+(this.options.duration*1000); + this.fromToDelta = this.options.to-this.options.from; + this.totalTime = this.finishOn-this.startOn; + this.totalFrames = this.options.fps*this.options.duration; + + eval('this.render = function(pos){ '+ + 'if (this.state=="idle"){this.state="running";'+ + codeForEvent(this.options,'beforeSetup')+ + (this.setup ? 'this.setup();':'')+ + codeForEvent(this.options,'afterSetup')+ + '};if (this.state=="running"){'+ + 'pos=this.options.transition(pos)*'+this.fromToDelta+'+'+this.options.from+';'+ + 'this.position=pos;'+ + codeForEvent(this.options,'beforeUpdate')+ + (this.update ? 'this.update(pos);':'')+ + codeForEvent(this.options,'afterUpdate')+ + '}}'); + + this.event('beforeStart'); + if (!this.options.sync) + Effect.Queues.get(Object.isString(this.options.queue) ? + 'global' : this.options.queue.scope).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.totalTime, + frame = (pos * this.totalFrames).round(); + if (frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + } + }, + cancel: function() { + if (!this.options.sync) + Effect.Queues.get(Object.isString(this.options.queue) ? + 'global' : this.options.queue.scope).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() { + var data = $H(); + for(property in this) + if (!Object.isFunction(this[property])) data.set(property, this[property]); + return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>'; + } +}); + +Effect.Parallel = Class.create(Effect.Base, { + 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.Tween = Class.create(Effect.Base, { + initialize: function(object, from, to) { + object = Object.isString(object) ? $(object) : object; + var args = $A(arguments), method = args.last(), + options = args.length == 5 ? args[3] : null; + this.method = Object.isFunction(method) ? method.bind(object) : + Object.isFunction(object[method]) ? object[method].bind(object) : + function(value) { object[method] = value }; + this.start(Object.extend({ from: from, to: to }, options || { })); + }, + update: function(position) { + this.method(position); + } +}); + +Effect.Event = Class.create(Effect.Base, { + initialize: function() { + this.start(Object.extend({ duration: 0 }, arguments[0] || { })); + }, + update: Prototype.emptyFunction +}); + +Effect.Opacity = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + // make this work on IE on elements without 'layout' + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + var options = Object.extend({ + from: this.element.getOpacity() || 0.0, + to: 1.0 + }, arguments[1] || { }); + this.start(options); + }, + update: function(position) { + this.element.setOpacity(position); + } +}); + +Effect.Move = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + x: 0, + y: 0, + mode: 'relative' + }, arguments[1] || { }); + this.start(options); + }, + setup: function() { + this.element.makePositioned(); + this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); + this.originalTop = parseFloat(this.element.getStyle('top') || '0'); + if (this.options.mode == 'absolute') { + this.options.x = this.options.x - this.originalLeft; + this.options.y = this.options.y - this.originalTop; + } + }, + update: function(position) { + this.element.setStyle({ + left: (this.options.x * position + this.originalLeft).round() + 'px', + top: (this.options.y * position + this.originalTop).round() + 'px' + }); + } +}); + +// for backwards compatibility +Effect.MoveBy = function(element, toTop, toLeft) { + return new Effect.Move(element, + Object.extend({ x: toLeft, y: toTop }, arguments[3] || { })); +}; + +Effect.Scale = Class.create(Effect.Base, { + initialize: function(element, percent) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + 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 = this.element.getStyle('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 = this.element.getStyle('font-size') || '100%'; + ['em','px','%','pt'].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) + this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); + this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); + }, + finish: function(position) { + if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); + }, + setDimensions: function(height, width) { + var d = { }; + if (this.options.scaleX) d.width = width.round() + 'px'; + if (this.options.scaleY) d.height = height.round() + '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'; + } + } + this.element.setStyle(d); + } +}); + +Effect.Highlight = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { }); + this.start(options); + }, + setup: function() { + // Prevent executing on elements not in the layout flow + if (this.element.getStyle('display')=='none') { this.cancel(); return; } + // Disable background image during the effect + this.oldStyle = { }; + if (!this.options.keepBackgroundImage) { + this.oldStyle.backgroundImage = this.element.getStyle('background-image'); + this.element.setStyle({backgroundImage: 'none'}); + } + if (!this.options.endcolor) + this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); + if (!this.options.restorecolor) + this.options.restorecolor = this.element.getStyle('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) { + this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ + return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) }); + }, + finish: function() { + this.element.setStyle(Object.extend(this.oldStyle, { + backgroundColor: this.options.restorecolor + })); + } +}); + +Effect.ScrollTo = function(element) { + var options = arguments[1] || { }, + scrollOffsets = document.viewport.getScrollOffsets(), + elementOffsets = $(element).cumulativeOffset(), + max = (window.height || document.body.scrollHeight) - document.viewport.getHeight(); + + if (options.offset) elementOffsets[1] += options.offset; + + return new Effect.Tween(null, + scrollOffsets.top, + elementOffsets[1] > max ? max : elementOffsets[1], + options, + function(p){ scrollTo(scrollOffsets.left, p.round()) } + ); +}; + +/* ------------- combination effects ------------- */ + +Effect.Fade = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + var options = Object.extend({ + from: element.getOpacity() || 1.0, + to: 0.0, + afterFinishInternal: function(effect) { + if (effect.options.to!=0) return; + effect.element.hide().setStyle({opacity: oldOpacity}); + } + }, arguments[1] || { }); + return new Effect.Opacity(element,options); +}; + +Effect.Appear = function(element) { + element = $(element); + var options = Object.extend({ + from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), + to: 1.0, + // force Safari to render floated elements properly + afterFinishInternal: function(effect) { + effect.element.forceRerendering(); + }, + beforeSetup: function(effect) { + effect.element.setOpacity(effect.options.from).show(); + }}, arguments[1] || { }); + return new Effect.Opacity(element,options); +}; + +Effect.Puff = function(element) { + element = $(element); + var oldStyle = { + opacity: element.getInlineOpacity(), + position: element.getStyle('position'), + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height + }; + 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) { + Position.absolutize(effect.effects[0].element) + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().setStyle(oldStyle); } + }, arguments[1] || { }) + ); +}; + +Effect.BlindUp = function(element) { + element = $(element); + element.makeClipping(); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + restoreAfterFinish: true, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }, arguments[1] || { }) + ); +}; + +Effect.BlindDown = function(element) { + element = $(element); + var elementDimensions = element.getDimensions(); + 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) { + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + } + }, arguments[1] || { })); +}; + +Effect.SwitchOff = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + return new Effect.Appear(element, Object.extend({ + 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) { + effect.element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); + } + }) + } + }, arguments[1] || { })); +}; + +Effect.DropOut = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left'), + opacity: element.getInlineOpacity() }; + return new Effect.Parallel( + [ new Effect.Move(element, {x: 0, y: 100, sync: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 }) ], + Object.extend( + { duration: 0.5, + beforeSetup: function(effect) { + effect.effects[0].element.makePositioned(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); + } + }, arguments[1] || { })); +}; + +Effect.Shake = function(element) { + element = $(element); + var options = Object.extend({ + distance: 20, + duration: 0.5 + }, arguments[1] || {}); + var distance = parseFloat(options.distance); + var split = parseFloat(options.duration) / 10.0; + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left') }; + return new Effect.Move(element, + { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) { + effect.element.undoPositioned().setStyle(oldStyle); + }}) }}) }}) }}) }}) }}); +}; + +Effect.SlideDown = function(element) { + element = $(element).cleanWhitespace(); + // SlideDown need to have the content of the element wrapped in a container element with fixed height! + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: window.opera ? 0 : 1, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if (window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || { }) + ); +}; + +Effect.SlideUp = function(element) { + element = $(element).cleanWhitespace(); + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, window.opera ? 0 : 1, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if (window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({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) { + effect.element.makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }); +}; + +Effect.Grow = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: 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() }; + + var dims = element.getDimensions(); + 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.Move(element, { + x: initialMoveX, + y: initialMoveY, + duration: 0.01, + beforeSetup: function(effect) { + effect.element.hide().makeClipping().makePositioned(); + }, + afterFinishInternal: function(effect) { + new Effect.Parallel( + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), + new Effect.Move(effect.element, { x: moveX, y: moveY, 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) { + effect.effects[0].element.setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); + } + }, options) + ) + } + }); +}; + +Effect.Shrink = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: 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() }; + + var dims = element.getDimensions(); + 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.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { + effect.effects[0].element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } + }, options) + ); +}; + +Effect.Pulsate = function(element) { + element = $(element); + var options = arguments[1] || { }; + var oldOpacity = element.getInlineOpacity(); + var transition = options.transition || Effect.Transitions.sinoidal; + var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) }; + reverser.bind(transition); + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 2.0, from: 0, + afterFinishInternal: function(effect) { effect.element.setStyle({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(); + 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) { + effect.element.hide().undoClipping().setStyle(oldStyle); + } }); + }}, arguments[1] || { })); +}; + +Effect.Morph = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + style: { } + }, arguments[1] || { }); + + if (!Object.isString(options.style)) this.style = $H(options.style); + else { + if (options.style.include(':')) + this.style = options.style.parseStyle(); + else { + this.element.addClassName(options.style); + this.style = $H(this.element.getStyles()); + this.element.removeClassName(options.style); + var css = this.element.getStyles(); + this.style = this.style.reject(function(style) { + return style.value == css[style.key]; + }); + options.afterFinishInternal = function(effect) { + effect.element.addClassName(effect.options.style); + effect.transforms.each(function(transform) { + effect.element.style[transform.style] = ''; + }); + } + } + } + this.start(options); + }, + + setup: function(){ + function parseColor(color){ + if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; + color = color.parseColor(); + return $R(0,2).map(function(i){ + return parseInt( color.slice(i*2+1,i*2+3), 16 ) + }); + } + this.transforms = this.style.map(function(pair){ + var property = pair[0], value = pair[1], unit = null; + + if (value.parseColor('#zzzzzz') != '#zzzzzz') { + value = value.parseColor(); + unit = 'color'; + } else if (property == 'opacity') { + value = parseFloat(value); + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + } else if (Element.CSS_LENGTH.test(value)) { + var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); + value = parseFloat(components[1]); + unit = (components.length == 3) ? components[2] : null; + } + + var originalValue = this.element.getStyle(property); + return { + style: property.camelize(), + originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), + targetValue: unit=='color' ? parseColor(value) : value, + unit: unit + }; + }.bind(this)).reject(function(transform){ + return ( + (transform.originalValue == transform.targetValue) || + ( + transform.unit != 'color' && + (isNaN(transform.originalValue) || isNaN(transform.targetValue)) + ) + ) + }); + }, + update: function(position) { + var style = { }, transform, i = this.transforms.length; + while(i--) + style[(transform = this.transforms[i]).style] = + transform.unit=='color' ? '#'+ + (Math.round(transform.originalValue[0]+ + (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + + (Math.round(transform.originalValue[1]+ + (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + + (Math.round(transform.originalValue[2]+ + (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : + (transform.originalValue + + (transform.targetValue - transform.originalValue) * position).toFixed(3) + + (transform.unit === null ? '' : transform.unit); + this.element.setStyle(style, true); + } +}); + +Effect.Transform = Class.create({ + initialize: function(tracks){ + this.tracks = []; + this.options = arguments[1] || { }; + this.addTracks(tracks); + }, + addTracks: function(tracks){ + tracks.each(function(track){ + track = $H(track); + var data = track.values().first(); + this.tracks.push($H({ + ids: track.keys().first(), + effect: Effect.Morph, + options: { style: data } + })); + }.bind(this)); + return this; + }, + play: function(){ + return new Effect.Parallel( + this.tracks.map(function(track){ + var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options'); + var elements = [$(ids) || $$(ids)].flatten(); + return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) }); + }).flatten(), + this.options + ); + } +}); + +Element.CSS_PROPERTIES = $w( + 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + + 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + + 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + + 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + + 'fontSize fontWeight height left letterSpacing lineHeight ' + + 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ + 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + + 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + + 'right textIndent top width wordSpacing zIndex'); + +Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; + +String.__parseStyleElement = document.createElement('div'); +String.prototype.parseStyle = function(){ + var style, styleRules = $H(); + if (Prototype.Browser.WebKit) + style = new Element('div',{style:this}).style; + else { + String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>'; + style = String.__parseStyleElement.childNodes[0].style; + } + + Element.CSS_PROPERTIES.each(function(property){ + if (style[property]) styleRules.set(property, style[property]); + }); + + if (Prototype.Browser.IE && this.include('opacity')) + styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]); + + return styleRules; +}; + +if (document.defaultView && document.defaultView.getComputedStyle) { + Element.getStyles = function(element) { + var css = document.defaultView.getComputedStyle($(element), null); + return Element.CSS_PROPERTIES.inject({ }, function(styles, property) { + styles[property] = css[property]; + return styles; + }); + }; +} else { + Element.getStyles = function(element) { + element = $(element); + var css = element.currentStyle, styles; + styles = Element.CSS_PROPERTIES.inject({ }, function(hash, property) { + hash.set(property, css[property]); + return hash; + }); + if (!styles.opacity) styles.set('opacity', element.getOpacity()); + return styles; + }; +}; + +Effect.Methods = { + morph: function(element, style) { + element = $(element); + new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { })); + return element; + }, + visualEffect: function(element, effect, options) { + element = $(element) + var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1); + new Effect[klass](element, options); + return element; + }, + highlight: function(element, options) { + element = $(element); + new Effect.Highlight(element, options); + return element; + } +}; + +$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+ + 'pulsate shake puff squish switchOff dropOut').each( + function(effect) { + Effect.Methods[effect] = function(element, options){ + element = $(element); + Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options); + return element; + } + } +); + +$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( + function(f) { Effect.Methods[f] = Element[f]; } +); + +Element.addMethods(Effect.Methods); diff --git a/app/webroot/js/jstoolbar/jstoolbar.js b/app/webroot/js/jstoolbar/jstoolbar.js new file mode 100644 index 0000000..66669ce --- /dev/null +++ b/app/webroot/js/jstoolbar/jstoolbar.js @@ -0,0 +1,380 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * This file is part of DotClear. + * Copyright (c) 2005 Nicolas Martin & Olivier Meunier and contributors. All + * rights reserved. + * + * DotClear 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. + * + * DotClear 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 DotClear; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * ***** END LICENSE BLOCK ***** +*/ + +/* Modified by JP LANG for textile formatting */ + +function jsToolBar(textarea) { + if (!document.createElement) { return; } + + if (!textarea) { return; } + + if ((typeof(document["selection"]) == "undefined") + && (typeof(textarea["setSelectionRange"]) == "undefined")) { + return; + } + + this.textarea = textarea; + + this.editor = document.createElement('div'); + this.editor.className = 'jstEditor'; + + this.textarea.parentNode.insertBefore(this.editor,this.textarea); + this.editor.appendChild(this.textarea); + + this.toolbar = document.createElement("div"); + this.toolbar.className = 'jstElements'; + this.editor.parentNode.insertBefore(this.toolbar,this.editor); + + // Dragable resizing (only for gecko) + if (this.editor.addEventListener) + { + this.handle = document.createElement('div'); + this.handle.className = 'jstHandle'; + var dragStart = this.resizeDragStart; + var This = this; + this.handle.addEventListener('mousedown',function(event) { dragStart.call(This,event); },false); + // fix memory leak in Firefox (bug #241518) + window.addEventListener('unload',function() { + var del = This.handle.parentNode.removeChild(This.handle); + delete(This.handle); + },false); + + this.editor.parentNode.insertBefore(this.handle,this.editor.nextSibling); + } + + this.context = null; + this.toolNodes = {}; // lorsque la toolbar est dessinée , cet objet est garni + // de raccourcis vers les éléments DOM correspondants aux outils. +} + +function jsButton(title, fn, scope, className) { + if(typeof jsToolBar.strings == 'undefined') { + this.title = title || null; + } else { + this.title = jsToolBar.strings[title] || title || null; + } + this.fn = fn || function(){}; + this.scope = scope || null; + this.className = className || null; +} +jsButton.prototype.draw = function() { + if (!this.scope) return null; + + var button = document.createElement('button'); + button.setAttribute('type','button'); + button.tabIndex = 200; + if (this.className) button.className = this.className; + button.title = this.title; + var span = document.createElement('span'); + span.appendChild(document.createTextNode(this.title)); + button.appendChild(span); + + if (this.icon != undefined) { + button.style.backgroundImage = 'url('+this.icon+')'; + } + if (typeof(this.fn) == 'function') { + var This = this; + button.onclick = function() { try { This.fn.apply(This.scope, arguments) } catch (e) {} return false; }; + } + return button; +} + +function jsSpace(id) { + this.id = id || null; + this.width = null; +} +jsSpace.prototype.draw = function() { + var span = document.createElement('span'); + if (this.id) span.id = this.id; + span.appendChild(document.createTextNode(String.fromCharCode(160))); + span.className = 'jstSpacer'; + if (this.width) span.style.marginRight = this.width+'px'; + + return span; +} + +function jsCombo(title, options, scope, fn, className) { + this.title = title || null; + this.options = options || null; + this.scope = scope || null; + this.fn = fn || function(){}; + this.className = className || null; +} +jsCombo.prototype.draw = function() { + if (!this.scope || !this.options) return null; + + var select = document.createElement('select'); + if (this.className) select.className = className; + select.title = this.title; + + for (var o in this.options) { + //var opt = this.options[o]; + var option = document.createElement('option'); + option.value = o; + option.appendChild(document.createTextNode(this.options[o])); + select.appendChild(option); + } + + var This = this; + select.onchange = function() { + try { + This.fn.call(This.scope, this.value); + } catch (e) { alert(e); } + + return false; + } + + return select; +} + + +jsToolBar.prototype = { + base_url: '', + mode: 'wiki', + elements: {}, + help_link: '', + + getMode: function() { + return this.mode; + }, + + setMode: function(mode) { + this.mode = mode || 'wiki'; + }, + + switchMode: function(mode) { + mode = mode || 'wiki'; + this.draw(mode); + }, + + setHelpLink: function(link) { + this.help_link = link; + }, + + button: function(toolName) { + var tool = this.elements[toolName]; + if (typeof tool.fn[this.mode] != 'function') return null; + var b = new jsButton(tool.title, tool.fn[this.mode], this, 'jstb_'+toolName); + if (tool.icon != undefined) b.icon = tool.icon; + return b; + }, + space: function(toolName) { + var tool = new jsSpace(toolName) + if (this.elements[toolName].width !== undefined) + tool.width = this.elements[toolName].width; + return tool; + }, + combo: function(toolName) { + var tool = this.elements[toolName]; + var length = tool[this.mode].list.length; + + if (typeof tool[this.mode].fn != 'function' || length == 0) { + return null; + } else { + var options = {}; + for (var i=0; i < length; i++) { + var opt = tool[this.mode].list[i]; + options[opt] = tool.options[opt]; + } + return new jsCombo(tool.title, options, this, tool[this.mode].fn); + } + }, + draw: function(mode) { + this.setMode(mode); + + // Empty toolbar + while (this.toolbar.hasChildNodes()) { + this.toolbar.removeChild(this.toolbar.firstChild) + } + this.toolNodes = {}; // vide les raccourcis DOM/**/ + + var h = document.createElement('div'); + h.className = 'help' + h.innerHTML = this.help_link; + '<a href="/help/wiki_syntax.html" onclick="window.open(\'/help/wiki_syntax.html\', \'\', \'resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\'); return false;">Aide</a>'; + this.toolbar.appendChild(h); + + // Draw toolbar elements + var b, tool, newTool; + + for (var i in this.elements) { + b = this.elements[i]; + + var disabled = + b.type == undefined || b.type == '' + || (b.disabled != undefined && b.disabled) + || (b.context != undefined && b.context != null && b.context != this.context); + + if (!disabled && typeof this[b.type] == 'function') { + tool = this[b.type](i); + if (tool) newTool = tool.draw(); + if (newTool) { + this.toolNodes[i] = newTool; //mémorise l'accès DOM pour usage éventuel ultérieur + this.toolbar.appendChild(newTool); + } + } + } + }, + + singleTag: function(stag,etag) { + stag = stag || null; + etag = etag || stag; + + if (!stag || !etag) { return; } + + this.encloseSelection(stag,etag); + }, + + encloseLineSelection: function(prefix, suffix, fn) { + this.textarea.focus(); + + prefix = prefix || ''; + suffix = suffix || ''; + + var start, end, sel, scrollPos, subst, res; + + if (typeof(document["selection"]) != "undefined") { + sel = document.selection.createRange().text; + } else if (typeof(this.textarea["setSelectionRange"]) != "undefined") { + start = this.textarea.selectionStart; + end = this.textarea.selectionEnd; + scrollPos = this.textarea.scrollTop; + // go to the start of the line + start = this.textarea.value.substring(0, start).replace(/[^\r\n]*$/g,'').length; + // go to the end of the line + end = this.textarea.value.length - this.textarea.value.substring(end, this.textarea.value.length).replace(/^[^\r\n]*/, '').length; + sel = this.textarea.value.substring(start, end); + } + + if (sel.match(/ $/)) { // exclude ending space char, if any + sel = sel.substring(0, sel.length - 1); + suffix = suffix + " "; + } + + if (typeof(fn) == 'function') { + res = (sel) ? fn.call(this,sel) : fn(''); + } else { + res = (sel) ? sel : ''; + } + + subst = prefix + res + suffix; + + if (typeof(document["selection"]) != "undefined") { + document.selection.createRange().text = subst; + var range = this.textarea.createTextRange(); + range.collapse(false); + range.move('character', -suffix.length); + range.select(); + } else if (typeof(this.textarea["setSelectionRange"]) != "undefined") { + this.textarea.value = this.textarea.value.substring(0, start) + subst + + this.textarea.value.substring(end); + if (sel) { + this.textarea.setSelectionRange(start + subst.length, start + subst.length); + } else { + this.textarea.setSelectionRange(start + prefix.length, start + prefix.length); + } + this.textarea.scrollTop = scrollPos; + } + }, + + encloseSelection: function(prefix, suffix, fn) { + this.textarea.focus(); + + prefix = prefix || ''; + suffix = suffix || ''; + + var start, end, sel, scrollPos, subst, res; + + if (typeof(document["selection"]) != "undefined") { + sel = document.selection.createRange().text; + } else if (typeof(this.textarea["setSelectionRange"]) != "undefined") { + start = this.textarea.selectionStart; + end = this.textarea.selectionEnd; + scrollPos = this.textarea.scrollTop; + sel = this.textarea.value.substring(start, end); + } + + if (sel.match(/ $/)) { // exclude ending space char, if any + sel = sel.substring(0, sel.length - 1); + suffix = suffix + " "; + } + + if (typeof(fn) == 'function') { + res = (sel) ? fn.call(this,sel) : fn(''); + } else { + res = (sel) ? sel : ''; + } + + subst = prefix + res + suffix; + + if (typeof(document["selection"]) != "undefined") { + document.selection.createRange().text = subst; + var range = this.textarea.createTextRange(); + range.collapse(false); + range.move('character', -suffix.length); + range.select(); +// this.textarea.caretPos -= suffix.length; + } else if (typeof(this.textarea["setSelectionRange"]) != "undefined") { + this.textarea.value = this.textarea.value.substring(0, start) + subst + + this.textarea.value.substring(end); + if (sel) { + this.textarea.setSelectionRange(start + subst.length, start + subst.length); + } else { + this.textarea.setSelectionRange(start + prefix.length, start + prefix.length); + } + this.textarea.scrollTop = scrollPos; + } + }, + + stripBaseURL: function(url) { + if (this.base_url != '') { + var pos = url.indexOf(this.base_url); + if (pos == 0) { + url = url.substr(this.base_url.length); + } + } + + return url; + } +}; + +/** Resizer +-------------------------------------------------------- */ +jsToolBar.prototype.resizeSetStartH = function() { + this.dragStartH = this.textarea.offsetHeight + 0; +}; +jsToolBar.prototype.resizeDragStart = function(event) { + var This = this; + this.dragStartY = event.clientY; + this.resizeSetStartH(); + document.addEventListener('mousemove', this.dragMoveHdlr=function(event){This.resizeDragMove(event);}, false); + document.addEventListener('mouseup', this.dragStopHdlr=function(event){This.resizeDragStop(event);}, false); +}; + +jsToolBar.prototype.resizeDragMove = function(event) { + this.textarea.style.height = (this.dragStartH+event.clientY-this.dragStartY)+'px'; +}; + +jsToolBar.prototype.resizeDragStop = function(event) { + document.removeEventListener('mousemove', this.dragMoveHdlr, false); + document.removeEventListener('mouseup', this.dragStopHdlr, false); +}; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-bg.js b/app/webroot/js/jstoolbar/lang/jstoolbar-bg.js new file mode 100644 index 0000000..2d68498 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-bg.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Strong'; +jsToolBar.strings['Italic'] = 'Italic'; +jsToolBar.strings['Underline'] = 'Underline'; +jsToolBar.strings['Deleted'] = 'Deleted'; +jsToolBar.strings['Code'] = 'Inline Code'; +jsToolBar.strings['Heading 1'] = 'Heading 1'; +jsToolBar.strings['Heading 2'] = 'Heading 2'; +jsToolBar.strings['Heading 3'] = 'Heading 3'; +jsToolBar.strings['Unordered list'] = 'Unordered list'; +jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Preformatted text'; +jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-ca.js b/app/webroot/js/jstoolbar/lang/jstoolbar-ca.js new file mode 100644 index 0000000..3d652a4 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-ca.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Negreta'; +jsToolBar.strings['Italic'] = 'Cursiva'; +jsToolBar.strings['Underline'] = 'Subratllat'; +jsToolBar.strings['Deleted'] = 'Barrat'; +jsToolBar.strings['Code'] = 'Codi en línia'; +jsToolBar.strings['Heading 1'] = 'Encapçalament 1'; +jsToolBar.strings['Heading 2'] = 'Encapçalament 2'; +jsToolBar.strings['Heading 3'] = 'Encapçalament 3'; +jsToolBar.strings['Unordered list'] = 'Llista sense ordre'; +jsToolBar.strings['Ordered list'] = 'Llista ordenada'; +jsToolBar.strings['Quote'] = 'Cometes'; +jsToolBar.strings['Unquote'] = 'Sense cometes'; +jsToolBar.strings['Preformatted text'] = 'Text formatat'; +jsToolBar.strings['Wiki link'] = 'Enllaça a una pàgina Wiki'; +jsToolBar.strings['Image'] = 'Imatge'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-cs.js b/app/webroot/js/jstoolbar/lang/jstoolbar-cs.js new file mode 100644 index 0000000..f2c0dbf --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-cs.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Tučné'; +jsToolBar.strings['Italic'] = 'Kurzíva'; +jsToolBar.strings['Underline'] = 'Podtržené'; +jsToolBar.strings['Deleted'] = 'Přeškrtnuté '; +jsToolBar.strings['Code'] = 'Zobrazení kódu'; +jsToolBar.strings['Heading 1'] = 'Záhlaví 1'; +jsToolBar.strings['Heading 2'] = 'Záhlaví 2'; +jsToolBar.strings['Heading 3'] = 'Záhlaví 3'; +jsToolBar.strings['Unordered list'] = 'Seznam'; +jsToolBar.strings['Ordered list'] = 'Uspořádaný seznam'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Předformátovaný text'; +jsToolBar.strings['Wiki link'] = 'Vložit odkaz na Wiki stránku'; +jsToolBar.strings['Image'] = 'Vložit obrázek'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-da.js b/app/webroot/js/jstoolbar/lang/jstoolbar-da.js new file mode 100644 index 0000000..6ccc8ea --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-da.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Fed'; +jsToolBar.strings['Italic'] = 'Kursiv'; +jsToolBar.strings['Underline'] = 'Underskrevet'; +jsToolBar.strings['Deleted'] = 'Slettet'; +jsToolBar.strings['Code'] = 'Inline Kode'; +jsToolBar.strings['Heading 1'] = 'Overskrift 1'; +jsToolBar.strings['Heading 2'] = 'Overskrift 2'; +jsToolBar.strings['Heading 3'] = 'Overskrift 3'; +jsToolBar.strings['Unordered list'] = 'Unummereret list'; +jsToolBar.strings['Ordered list'] = 'Nummereret list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Preformatteret tekst'; +jsToolBar.strings['Wiki link'] = 'Link til en Wiki side'; +jsToolBar.strings['Image'] = 'Billede'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-de.js b/app/webroot/js/jstoolbar/lang/jstoolbar-de.js new file mode 100644 index 0000000..ce68686 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-de.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Fett'; +jsToolBar.strings['Italic'] = 'Kursiv'; +jsToolBar.strings['Underline'] = 'Unterstrichen'; +jsToolBar.strings['Deleted'] = 'Durchgestrichen'; +jsToolBar.strings['Code'] = 'Quelltext'; +jsToolBar.strings['Heading 1'] = 'Überschrift 1. Ordnung'; +jsToolBar.strings['Heading 2'] = 'Überschrift 2. Ordnung'; +jsToolBar.strings['Heading 3'] = 'Überschrift 3. Ordnung'; +jsToolBar.strings['Unordered list'] = 'Aufzählungsliste'; +jsToolBar.strings['Ordered list'] = 'Nummerierte Liste'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Präformatierter Text'; +jsToolBar.strings['Wiki link'] = 'Verweis (Link) zu einer Wiki-Seite'; +jsToolBar.strings['Image'] = 'Grafik'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-en.js b/app/webroot/js/jstoolbar/lang/jstoolbar-en.js new file mode 100644 index 0000000..2d68498 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-en.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Strong'; +jsToolBar.strings['Italic'] = 'Italic'; +jsToolBar.strings['Underline'] = 'Underline'; +jsToolBar.strings['Deleted'] = 'Deleted'; +jsToolBar.strings['Code'] = 'Inline Code'; +jsToolBar.strings['Heading 1'] = 'Heading 1'; +jsToolBar.strings['Heading 2'] = 'Heading 2'; +jsToolBar.strings['Heading 3'] = 'Heading 3'; +jsToolBar.strings['Unordered list'] = 'Unordered list'; +jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Preformatted text'; +jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-es.js b/app/webroot/js/jstoolbar/lang/jstoolbar-es.js new file mode 100644 index 0000000..878489f --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-es.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Negrita'; +jsToolBar.strings['Italic'] = 'Itálica'; +jsToolBar.strings['Underline'] = 'Subrayado'; +jsToolBar.strings['Deleted'] = 'Tachado'; +jsToolBar.strings['Code'] = 'Código fuente'; +jsToolBar.strings['Heading 1'] = 'Encabezado 1'; +jsToolBar.strings['Heading 2'] = 'Encabezado 2'; +jsToolBar.strings['Heading 3'] = 'Encabezado 3'; +jsToolBar.strings['Unordered list'] = 'Lista sin ordenar'; +jsToolBar.strings['Ordered list'] = 'Lista ordenada'; +jsToolBar.strings['Quote'] = 'Citar'; +jsToolBar.strings['Unquote'] = 'Quitar cita'; +jsToolBar.strings['Preformatted text'] = 'Texto con formato'; +jsToolBar.strings['Wiki link'] = 'Enlace a página Wiki'; +jsToolBar.strings['Image'] = 'Imagen'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-fi.js b/app/webroot/js/jstoolbar/lang/jstoolbar-fi.js new file mode 100644 index 0000000..c2229b2 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-fi.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Lihavoitu'; +jsToolBar.strings['Italic'] = 'Kursivoitu'; +jsToolBar.strings['Underline'] = 'Alleviivattu'; +jsToolBar.strings['Deleted'] = 'Yliviivattu'; +jsToolBar.strings['Code'] = 'Koodi näkymä'; +jsToolBar.strings['Heading 1'] = 'Otsikko 1'; +jsToolBar.strings['Heading 2'] = 'Otsikko 2'; +jsToolBar.strings['Heading 3'] = 'Otsikko 3'; +jsToolBar.strings['Unordered list'] = 'Järjestämätön lista'; +jsToolBar.strings['Ordered list'] = 'Järjestetty lista'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Ennaltamuotoiltu teksti'; +jsToolBar.strings['Wiki link'] = 'Linkki Wiki sivulle'; +jsToolBar.strings['Image'] = 'Kuva'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-fr.js b/app/webroot/js/jstoolbar/lang/jstoolbar-fr.js new file mode 100644 index 0000000..c52a783 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-fr.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Gras'; +jsToolBar.strings['Italic'] = 'Italique'; +jsToolBar.strings['Underline'] = 'Souligné'; +jsToolBar.strings['Deleted'] = 'Rayé'; +jsToolBar.strings['Code'] = 'Code en ligne'; +jsToolBar.strings['Heading 1'] = 'Titre niveau 1'; +jsToolBar.strings['Heading 2'] = 'Titre niveau 2'; +jsToolBar.strings['Heading 3'] = 'Titre niveau 3'; +jsToolBar.strings['Unordered list'] = 'Liste à puces'; +jsToolBar.strings['Ordered list'] = 'Liste numérotée'; +jsToolBar.strings['Quote'] = 'Citer'; +jsToolBar.strings['Unquote'] = 'Supprimer citation'; +jsToolBar.strings['Preformatted text'] = 'Texte préformaté'; +jsToolBar.strings['Wiki link'] = 'Lien vers une page Wiki'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-he.js b/app/webroot/js/jstoolbar/lang/jstoolbar-he.js new file mode 100644 index 0000000..2d68498 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-he.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Strong'; +jsToolBar.strings['Italic'] = 'Italic'; +jsToolBar.strings['Underline'] = 'Underline'; +jsToolBar.strings['Deleted'] = 'Deleted'; +jsToolBar.strings['Code'] = 'Inline Code'; +jsToolBar.strings['Heading 1'] = 'Heading 1'; +jsToolBar.strings['Heading 2'] = 'Heading 2'; +jsToolBar.strings['Heading 3'] = 'Heading 3'; +jsToolBar.strings['Unordered list'] = 'Unordered list'; +jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Preformatted text'; +jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-hu.js b/app/webroot/js/jstoolbar/lang/jstoolbar-hu.js new file mode 100644 index 0000000..c31ba00 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-hu.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Félkövér'; +jsToolBar.strings['Italic'] = 'Dőlt'; +jsToolBar.strings['Underline'] = 'Aláhúzott'; +jsToolBar.strings['Deleted'] = 'Törölt'; +jsToolBar.strings['Code'] = 'Kód sorok'; +jsToolBar.strings['Heading 1'] = 'Fejléc 1'; +jsToolBar.strings['Heading 2'] = 'Fejléc 2'; +jsToolBar.strings['Heading 3'] = 'Fejléc 3'; +jsToolBar.strings['Unordered list'] = 'Felsorolás'; +jsToolBar.strings['Ordered list'] = 'Számozott lista'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Előreformázott szöveg'; +jsToolBar.strings['Wiki link'] = 'Link egy Wiki oldalra'; +jsToolBar.strings['Image'] = 'Kép'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-it.js b/app/webroot/js/jstoolbar/lang/jstoolbar-it.js new file mode 100644 index 0000000..bf7fcef --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-it.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Grassetto'; +jsToolBar.strings['Italic'] = 'Corsivo'; +jsToolBar.strings['Underline'] = 'Sottolineato'; +jsToolBar.strings['Deleted'] = 'Barrato'; +jsToolBar.strings['Code'] = 'Codice sorgente'; +jsToolBar.strings['Heading 1'] = 'Titolo 1'; +jsToolBar.strings['Heading 2'] = 'Titolo 2'; +jsToolBar.strings['Heading 3'] = 'Titolo 3'; +jsToolBar.strings['Unordered list'] = 'Elenco puntato'; +jsToolBar.strings['Ordered list'] = 'Numerazione'; +jsToolBar.strings['Quote'] = 'Aumenta rientro'; +jsToolBar.strings['Unquote'] = 'Riduci rientro'; +jsToolBar.strings['Preformatted text'] = 'Testo preformattato'; +jsToolBar.strings['Wiki link'] = 'Collegamento a pagina Wiki'; +jsToolBar.strings['Image'] = 'Immagine'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-ja.js b/app/webroot/js/jstoolbar/lang/jstoolbar-ja.js new file mode 100644 index 0000000..c9413da --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-ja.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = '強調'; +jsToolBar.strings['Italic'] = '斜体'; +jsToolBar.strings['Underline'] = '下線'; +jsToolBar.strings['Deleted'] = '取り消し線'; +jsToolBar.strings['Code'] = 'コード'; +jsToolBar.strings['Heading 1'] = '見出し 1'; +jsToolBar.strings['Heading 2'] = '見出し 2'; +jsToolBar.strings['Heading 3'] = '見出し 3'; +jsToolBar.strings['Unordered list'] = '順不同リスト'; +jsToolBar.strings['Ordered list'] = '番号つきリスト'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = '整形済みテキスト'; +jsToolBar.strings['Wiki link'] = 'Wiki ページへのリンク'; +jsToolBar.strings['Image'] = '画像'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-ko.js b/app/webroot/js/jstoolbar/lang/jstoolbar-ko.js new file mode 100644 index 0000000..1c437ef --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-ko.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = '굵게'; +jsToolBar.strings['Italic'] = '기울임'; +jsToolBar.strings['Underline'] = '밑줄'; +jsToolBar.strings['Deleted'] = '취소선'; +jsToolBar.strings['Code'] = '코드'; +jsToolBar.strings['Heading 1'] = '제목 1'; +jsToolBar.strings['Heading 2'] = '제목 2'; +jsToolBar.strings['Heading 3'] = '제목 3'; +jsToolBar.strings['Unordered list'] = '글머리 기호'; +jsToolBar.strings['Ordered list'] = '번호 매기기'; +jsToolBar.strings['Quote'] = '인용'; +jsToolBar.strings['Unquote'] = '인용 취소'; +jsToolBar.strings['Preformatted text'] = '있는 그대로 표현 (Preformatted text)'; +jsToolBar.strings['Wiki link'] = 'Wiki 페이지에 연결'; +jsToolBar.strings['Image'] = '그림'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-lt.js b/app/webroot/js/jstoolbar/lang/jstoolbar-lt.js new file mode 100644 index 0000000..8af364c --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-lt.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Pastorinti'; +jsToolBar.strings['Italic'] = 'Italic'; +jsToolBar.strings['Underline'] = 'Pabraukti'; +jsToolBar.strings['Deleted'] = 'Užbraukti'; +jsToolBar.strings['Code'] = 'Kodas'; +jsToolBar.strings['Heading 1'] = 'Heading 1'; +jsToolBar.strings['Heading 2'] = 'Heading 2'; +jsToolBar.strings['Heading 3'] = 'Heading 3'; +jsToolBar.strings['Unordered list'] = 'Nenumeruotas sąrašas'; +jsToolBar.strings['Ordered list'] = 'Numeruotas sąrašas'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Preformatuotas tekstas'; +jsToolBar.strings['Wiki link'] = 'Nuoroda į Wiki puslapį'; +jsToolBar.strings['Image'] = 'Paveikslas'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-nl.js b/app/webroot/js/jstoolbar/lang/jstoolbar-nl.js new file mode 100644 index 0000000..2d68498 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-nl.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Strong'; +jsToolBar.strings['Italic'] = 'Italic'; +jsToolBar.strings['Underline'] = 'Underline'; +jsToolBar.strings['Deleted'] = 'Deleted'; +jsToolBar.strings['Code'] = 'Inline Code'; +jsToolBar.strings['Heading 1'] = 'Heading 1'; +jsToolBar.strings['Heading 2'] = 'Heading 2'; +jsToolBar.strings['Heading 3'] = 'Heading 3'; +jsToolBar.strings['Unordered list'] = 'Unordered list'; +jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Preformatted text'; +jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-no.js b/app/webroot/js/jstoolbar/lang/jstoolbar-no.js new file mode 100644 index 0000000..7995973 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-no.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Fet'; +jsToolBar.strings['Italic'] = 'Kursiv'; +jsToolBar.strings['Underline'] = 'Understreking'; +jsToolBar.strings['Deleted'] = 'Slettet'; +jsToolBar.strings['Code'] = 'Kode'; +jsToolBar.strings['Heading 1'] = 'Overskrift 1'; +jsToolBar.strings['Heading 2'] = 'Overskrift 2'; +jsToolBar.strings['Heading 3'] = 'Overskrift 3'; +jsToolBar.strings['Unordered list'] = 'Punktliste'; +jsToolBar.strings['Ordered list'] = 'Nummerert liste'; +jsToolBar.strings['Quote'] = 'Sitat'; +jsToolBar.strings['Unquote'] = 'Avslutt sitat'; +jsToolBar.strings['Preformatted text'] = 'Preformatert tekst'; +jsToolBar.strings['Wiki link'] = 'Lenke til Wiki-side'; +jsToolBar.strings['Image'] = 'Bilde'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-pl.js b/app/webroot/js/jstoolbar/lang/jstoolbar-pl.js new file mode 100644 index 0000000..0e7a38c --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-pl.js @@ -0,0 +1,17 @@ +// Keep this line in order to avoid problems with Windows Notepad UTF-8 EF-BB-BF idea... +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Pogrubienie'; +jsToolBar.strings['Italic'] = 'Kursywa'; +jsToolBar.strings['Underline'] = 'Podkreślenie'; +jsToolBar.strings['Deleted'] = 'Usunięte'; +jsToolBar.strings['Code'] = 'Wstawka kodu'; +jsToolBar.strings['Heading 1'] = 'Nagłowek 1'; +jsToolBar.strings['Heading 2'] = 'Nagłówek 2'; +jsToolBar.strings['Heading 3'] = 'Nagłówek 3'; +jsToolBar.strings['Unordered list'] = 'Nieposortowana lista'; +jsToolBar.strings['Ordered list'] = 'Posortowana lista'; +jsToolBar.strings['Quote'] = 'Cytat'; +jsToolBar.strings['Unquote'] = 'Usuń cytat'; +jsToolBar.strings['Preformatted text'] = 'Sformatowany tekst'; +jsToolBar.strings['Wiki link'] = 'Odnośnik do strony Wiki'; +jsToolBar.strings['Image'] = 'Obraz'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-pt-br.js b/app/webroot/js/jstoolbar/lang/jstoolbar-pt-br.js new file mode 100644 index 0000000..5035524 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-pt-br.js @@ -0,0 +1,18 @@ +// Translated by: Alexandre da Silva <simpsomboy@gmail.com> + +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Negrito'; +jsToolBar.strings['Italic'] = 'Itálico'; +jsToolBar.strings['Underline'] = 'Sublinhado'; +jsToolBar.strings['Deleted'] = 'Excluído'; +jsToolBar.strings['Code'] = 'Código Inline'; +jsToolBar.strings['Heading 1'] = 'Cabeçalho 1'; +jsToolBar.strings['Heading 2'] = 'Cabeçalho 2'; +jsToolBar.strings['Heading 3'] = 'Cabeçalho 3'; +jsToolBar.strings['Unordered list'] = 'Lista não ordenada'; +jsToolBar.strings['Ordered list'] = 'Lista ordenada'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Texto pré-formatado'; +jsToolBar.strings['Wiki link'] = 'Link para uma página Wiki'; +jsToolBar.strings['Image'] = 'Imagem'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-pt.js b/app/webroot/js/jstoolbar/lang/jstoolbar-pt.js new file mode 100644 index 0000000..137d795 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-pt.js @@ -0,0 +1,17 @@ +// Translated by: Pedro Araújo <phcrva19@hotmail.com> +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Negrito'; +jsToolBar.strings['Italic'] = 'Itálico'; +jsToolBar.strings['Underline'] = 'Sublinhado'; +jsToolBar.strings['Deleted'] = 'Apagado'; +jsToolBar.strings['Code'] = 'Código Inline'; +jsToolBar.strings['Heading 1'] = 'Cabeçalho 1'; +jsToolBar.strings['Heading 2'] = 'Cabeçalho 2'; +jsToolBar.strings['Heading 3'] = 'Cabeçalho 3'; +jsToolBar.strings['Unordered list'] = 'Lista não ordenada'; +jsToolBar.strings['Ordered list'] = 'Lista ordenada'; +jsToolBar.strings['Quote'] = 'Citação'; +jsToolBar.strings['Unquote'] = 'Remover citação'; +jsToolBar.strings['Preformatted text'] = 'Texto pré-formatado'; +jsToolBar.strings['Wiki link'] = 'Link para uma página da Wiki'; +jsToolBar.strings['Image'] = 'Imagem'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-ro.js b/app/webroot/js/jstoolbar/lang/jstoolbar-ro.js new file mode 100644 index 0000000..2d68498 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-ro.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Strong'; +jsToolBar.strings['Italic'] = 'Italic'; +jsToolBar.strings['Underline'] = 'Underline'; +jsToolBar.strings['Deleted'] = 'Deleted'; +jsToolBar.strings['Code'] = 'Inline Code'; +jsToolBar.strings['Heading 1'] = 'Heading 1'; +jsToolBar.strings['Heading 2'] = 'Heading 2'; +jsToolBar.strings['Heading 3'] = 'Heading 3'; +jsToolBar.strings['Unordered list'] = 'Unordered list'; +jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Preformatted text'; +jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-ru.js b/app/webroot/js/jstoolbar/lang/jstoolbar-ru.js new file mode 100644 index 0000000..a6d8c4f --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-ru.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Жирный'; +jsToolBar.strings['Italic'] = 'Курсив'; +jsToolBar.strings['Underline'] = 'Подчеркнутый'; +jsToolBar.strings['Deleted'] = 'Зачеркнутый'; +jsToolBar.strings['Code'] = 'Вставка кода'; +jsToolBar.strings['Heading 1'] = 'Заголовок 1'; +jsToolBar.strings['Heading 2'] = 'Заголовок 2'; +jsToolBar.strings['Heading 3'] = 'Заголовок 3'; +jsToolBar.strings['Unordered list'] = 'Маркированный список'; +jsToolBar.strings['Ordered list'] = 'Нумерованный список'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Заранее форматированный текст'; +jsToolBar.strings['Wiki link'] = 'Ссылка на страницу в Wiki'; +jsToolBar.strings['Image'] = 'Вставка изображения'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-sk.js b/app/webroot/js/jstoolbar/lang/jstoolbar-sk.js new file mode 100644 index 0000000..0d47cd5 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-sk.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Tučné'; +jsToolBar.strings['Italic'] = 'Kurzíva'; +jsToolBar.strings['Underline'] = 'Podčiarknuté'; +jsToolBar.strings['Deleted'] = 'Preškrtnuté'; +jsToolBar.strings['Code'] = 'Zobrazenie kódu'; +jsToolBar.strings['Heading 1'] = 'Záhlavie 1'; +jsToolBar.strings['Heading 2'] = 'Záhlavie 2'; +jsToolBar.strings['Heading 3'] = 'Záhlavie 3'; +jsToolBar.strings['Unordered list'] = 'Zoznam'; +jsToolBar.strings['Ordered list'] = 'Zoradený zoznam'; +jsToolBar.strings['Quote'] = 'Citácia'; +jsToolBar.strings['Unquote'] = 'Odstránenie citácie'; +jsToolBar.strings['Preformatted text'] = 'Predformátovaný text'; +jsToolBar.strings['Wiki link'] = 'Link na Wiki stránku'; +jsToolBar.strings['Image'] = 'Obrázok'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-sr.js b/app/webroot/js/jstoolbar/lang/jstoolbar-sr.js new file mode 100644 index 0000000..2d68498 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-sr.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Strong'; +jsToolBar.strings['Italic'] = 'Italic'; +jsToolBar.strings['Underline'] = 'Underline'; +jsToolBar.strings['Deleted'] = 'Deleted'; +jsToolBar.strings['Code'] = 'Inline Code'; +jsToolBar.strings['Heading 1'] = 'Heading 1'; +jsToolBar.strings['Heading 2'] = 'Heading 2'; +jsToolBar.strings['Heading 3'] = 'Heading 3'; +jsToolBar.strings['Unordered list'] = 'Unordered list'; +jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Preformatted text'; +jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-sv.js b/app/webroot/js/jstoolbar/lang/jstoolbar-sv.js new file mode 100644 index 0000000..2d68498 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-sv.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Strong'; +jsToolBar.strings['Italic'] = 'Italic'; +jsToolBar.strings['Underline'] = 'Underline'; +jsToolBar.strings['Deleted'] = 'Deleted'; +jsToolBar.strings['Code'] = 'Inline Code'; +jsToolBar.strings['Heading 1'] = 'Heading 1'; +jsToolBar.strings['Heading 2'] = 'Heading 2'; +jsToolBar.strings['Heading 3'] = 'Heading 3'; +jsToolBar.strings['Unordered list'] = 'Unordered list'; +jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Preformatted text'; +jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-th.js b/app/webroot/js/jstoolbar/lang/jstoolbar-th.js new file mode 100644 index 0000000..d871642 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-th.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'หนา'; +jsToolBar.strings['Italic'] = 'เอียง'; +jsToolBar.strings['Underline'] = 'ขีดเส้นใต้'; +jsToolBar.strings['Deleted'] = 'ขีดฆ่า'; +jsToolBar.strings['Code'] = 'โค๊ดโปรแกรม'; +jsToolBar.strings['Heading 1'] = 'หัวข้อ 1'; +jsToolBar.strings['Heading 2'] = 'หัวข้อ 2'; +jsToolBar.strings['Heading 3'] = 'หัวข้อ 3'; +jsToolBar.strings['Unordered list'] = 'รายการ'; +jsToolBar.strings['Ordered list'] = 'ลำดับเลข'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'รูปแบบข้อความคงที่'; +jsToolBar.strings['Wiki link'] = 'เชื่อมโยงไปหน้า Wiki อื่น'; +jsToolBar.strings['Image'] = 'รูปภาพ'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-tr.js b/app/webroot/js/jstoolbar/lang/jstoolbar-tr.js new file mode 100644 index 0000000..31705d7 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-tr.js @@ -0,0 +1,14 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Kalın'; +jsToolBar.strings['Italic'] = 'İtalik'; +jsToolBar.strings['Underline'] = 'Altı çizgili'; +jsToolBar.strings['Deleted'] = 'Silinmiş'; +jsToolBar.strings['Code'] = 'Satır içi kod'; +jsToolBar.strings['Heading 1'] = 'Başlık 1'; +jsToolBar.strings['Heading 2'] = 'Başlık 2'; +jsToolBar.strings['Heading 3'] = 'Başlık 3'; +jsToolBar.strings['Unordered list'] = 'Sırasız liste'; +jsToolBar.strings['Ordered list'] = 'Sıralı liste'; +jsToolBar.strings['Preformatted text'] = 'Preformatted text'; +jsToolBar.strings['Wiki link'] = 'Wiki sayfasına bağlantı'; +jsToolBar.strings['Image'] = 'Resim'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-uk.js b/app/webroot/js/jstoolbar/lang/jstoolbar-uk.js new file mode 100644 index 0000000..2d68498 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-uk.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Strong'; +jsToolBar.strings['Italic'] = 'Italic'; +jsToolBar.strings['Underline'] = 'Underline'; +jsToolBar.strings['Deleted'] = 'Deleted'; +jsToolBar.strings['Code'] = 'Inline Code'; +jsToolBar.strings['Heading 1'] = 'Heading 1'; +jsToolBar.strings['Heading 2'] = 'Heading 2'; +jsToolBar.strings['Heading 3'] = 'Heading 3'; +jsToolBar.strings['Unordered list'] = 'Unordered list'; +jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Preformatted text'; +jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-vn.js b/app/webroot/js/jstoolbar/lang/jstoolbar-vn.js new file mode 100644 index 0000000..f598bfe --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-vn.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Đậm'; +jsToolBar.strings['Italic'] = 'Nghiêng'; +jsToolBar.strings['Underline'] = 'Gạch chân'; +jsToolBar.strings['Deleted'] = 'Xóa'; +jsToolBar.strings['Code'] = 'Mã chung dòng'; +jsToolBar.strings['Heading 1'] = 'Tiêu đề 1'; +jsToolBar.strings['Heading 2'] = 'Tiêu đề 2'; +jsToolBar.strings['Heading 3'] = 'Tiêu đề 3'; +jsToolBar.strings['Unordered list'] = 'Danh sách không thứ tự'; +jsToolBar.strings['Ordered list'] = 'Danh sách có thứ tự'; +jsToolBar.strings['Quote'] = 'Trích dẫn'; +jsToolBar.strings['Unquote'] = 'Bỏ trích dẫn'; +jsToolBar.strings['Preformatted text'] = 'Mã nguồn'; +jsToolBar.strings['Wiki link'] = 'Liên kết đến trang wiki'; +jsToolBar.strings['Image'] = 'Ảnh'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-zh-tw.js b/app/webroot/js/jstoolbar/lang/jstoolbar-zh-tw.js new file mode 100644 index 0000000..86599c5 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-zh-tw.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = '粗體'; +jsToolBar.strings['Italic'] = '斜體'; +jsToolBar.strings['Underline'] = '底線'; +jsToolBar.strings['Deleted'] = '刪除線'; +jsToolBar.strings['Code'] = '程式碼'; +jsToolBar.strings['Heading 1'] = '標題 1'; +jsToolBar.strings['Heading 2'] = '標題 2'; +jsToolBar.strings['Heading 3'] = '標題 3'; +jsToolBar.strings['Unordered list'] = '項目清單'; +jsToolBar.strings['Ordered list'] = '編號清單'; +jsToolBar.strings['Quote'] = '引文'; +jsToolBar.strings['Unquote'] = '取消引文'; +jsToolBar.strings['Preformatted text'] = '已格式文字'; +jsToolBar.strings['Wiki link'] = '連結至 Wiki 頁面'; +jsToolBar.strings['Image'] = '圖片'; diff --git a/app/webroot/js/jstoolbar/lang/jstoolbar-zh.js b/app/webroot/js/jstoolbar/lang/jstoolbar-zh.js new file mode 100644 index 0000000..a9b6ba2 --- /dev/null +++ b/app/webroot/js/jstoolbar/lang/jstoolbar-zh.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = '粗体'; +jsToolBar.strings['Italic'] = '斜体'; +jsToolBar.strings['Underline'] = '下划线'; +jsToolBar.strings['Deleted'] = '删除线'; +jsToolBar.strings['Code'] = '程序代码'; +jsToolBar.strings['Heading 1'] = '标题 1'; +jsToolBar.strings['Heading 2'] = '标题 2'; +jsToolBar.strings['Heading 3'] = '标题 3'; +jsToolBar.strings['Unordered list'] = '无序列表'; +jsToolBar.strings['Ordered list'] = '排序列表'; +jsToolBar.strings['Quote'] = '引用'; +jsToolBar.strings['Unquote'] = '删除引用'; +jsToolBar.strings['Preformatted text'] = '格式化文本'; +jsToolBar.strings['Wiki link'] = '连接到 Wiki 页面'; +jsToolBar.strings['Image'] = '图片'; diff --git a/app/webroot/js/jstoolbar/textile.js b/app/webroot/js/jstoolbar/textile.js new file mode 100644 index 0000000..c461b9d --- /dev/null +++ b/app/webroot/js/jstoolbar/textile.js @@ -0,0 +1,200 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * This file is part of DotClear. + * Copyright (c) 2005 Nicolas Martin & Olivier Meunier and contributors. All + * rights reserved. + * + * DotClear 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. + * + * DotClear 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 DotClear; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * ***** END LICENSE BLOCK ***** +*/ + +/* Modified by JP LANG for textile formatting */ + +// strong +jsToolBar.prototype.elements.strong = { + type: 'button', + title: 'Strong', + fn: { + wiki: function() { this.singleTag('*') } + } +} + +// em +jsToolBar.prototype.elements.em = { + type: 'button', + title: 'Italic', + fn: { + wiki: function() { this.singleTag("_") } + } +} + +// ins +jsToolBar.prototype.elements.ins = { + type: 'button', + title: 'Underline', + fn: { + wiki: function() { this.singleTag('+') } + } +} + +// del +jsToolBar.prototype.elements.del = { + type: 'button', + title: 'Deleted', + fn: { + wiki: function() { this.singleTag('-') } + } +} + +// code +jsToolBar.prototype.elements.code = { + type: 'button', + title: 'Code', + fn: { + wiki: function() { this.singleTag('@') } + } +} + +// spacer +jsToolBar.prototype.elements.space1 = {type: 'space'} + +// headings +jsToolBar.prototype.elements.h1 = { + type: 'button', + title: 'Heading 1', + fn: { + wiki: function() { + this.encloseLineSelection('h1. ', '',function(str) { + str = str.replace(/^h\d+\.\s+/, '') + return str; + }); + } + } +} +jsToolBar.prototype.elements.h2 = { + type: 'button', + title: 'Heading 2', + fn: { + wiki: function() { + this.encloseLineSelection('h2. ', '',function(str) { + str = str.replace(/^h\d+\.\s+/, '') + return str; + }); + } + } +} +jsToolBar.prototype.elements.h3 = { + type: 'button', + title: 'Heading 3', + fn: { + wiki: function() { + this.encloseLineSelection('h3. ', '',function(str) { + str = str.replace(/^h\d+\.\s+/, '') + return str; + }); + } + } +} + +// spacer +jsToolBar.prototype.elements.space2 = {type: 'space'} + +// ul +jsToolBar.prototype.elements.ul = { + type: 'button', + title: 'Unordered list', + fn: { + wiki: function() { + this.encloseLineSelection('','',function(str) { + str = str.replace(/\r/g,''); + return str.replace(/(\n|^)[#-]?\s*/g,"$1* "); + }); + } + } +} + +// ol +jsToolBar.prototype.elements.ol = { + type: 'button', + title: 'Ordered list', + fn: { + wiki: function() { + this.encloseLineSelection('','',function(str) { + str = str.replace(/\r/g,''); + return str.replace(/(\n|^)[*-]?\s*/g,"$1# "); + }); + } + } +} + +// spacer +jsToolBar.prototype.elements.space3 = {type: 'space'} + +// bq +jsToolBar.prototype.elements.bq = { + type: 'button', + title: 'Quote', + fn: { + wiki: function() { + this.encloseLineSelection('','',function(str) { + str = str.replace(/\r/g,''); + return str.replace(/(\n|^) *([^\n]*)/g,"$1> $2"); + }); + } + } +} + +// unbq +jsToolBar.prototype.elements.unbq = { + type: 'button', + title: 'Unquote', + fn: { + wiki: function() { + this.encloseLineSelection('','',function(str) { + str = str.replace(/\r/g,''); + return str.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2"); + }); + } + } +} + +// pre +jsToolBar.prototype.elements.pre = { + type: 'button', + title: 'Preformatted text', + fn: { + wiki: function() { this.encloseLineSelection('<pre>\n', '\n</pre>') } + } +} + +// spacer +jsToolBar.prototype.elements.space4 = {type: 'space'} + +// wiki page +jsToolBar.prototype.elements.link = { + type: 'button', + title: 'Wiki link', + fn: { + wiki: function() { this.encloseSelection("[[", "]]") } + } +} +// image +jsToolBar.prototype.elements.img = { + type: 'button', + title: 'Image', + fn: { + wiki: function() { this.encloseSelection("!", "!") } + } +} diff --git a/app/webroot/js/prototype.js b/app/webroot/js/prototype.js new file mode 100644 index 0000000..546f9fe --- /dev/null +++ b/app/webroot/js/prototype.js @@ -0,0 +1,4225 @@ +/* Prototype JavaScript framework, version 1.6.0.1 + * (c) 2005-2007 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://www.prototypejs.org/ + * + *--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.6.0.1', + + Browser: { + IE: !!(window.attachEvent && !window.opera), + Opera: !!window.opera, + WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1, + Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1, + MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/) + }, + + BrowserFeatures: { + XPath: !!document.evaluate, + ElementExtensions: !!window.HTMLElement, + SpecificElementExtensions: + document.createElement('div').__proto__ && + document.createElement('div').__proto__ !== + document.createElement('form').__proto__ + }, + + ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>', + JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, + + emptyFunction: function() { }, + K: function(x) { return x } +}; + +if (Prototype.Browser.MobileSafari) + Prototype.BrowserFeatures.SpecificElementExtensions = false; + + +/* Based on Alex Arnell's inheritance implementation. */ +var Class = { + create: function() { + var parent = null, properties = $A(arguments); + if (Object.isFunction(properties[0])) + parent = properties.shift(); + + function klass() { + this.initialize.apply(this, arguments); + } + + Object.extend(klass, Class.Methods); + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + var subclass = function() { }; + subclass.prototype = parent.prototype; + klass.prototype = new subclass; + parent.subclasses.push(klass); + } + + for (var i = 0; i < properties.length; i++) + klass.addMethods(properties[i]); + + if (!klass.prototype.initialize) + klass.prototype.initialize = Prototype.emptyFunction; + + klass.prototype.constructor = klass; + + return klass; + } +}; + +Class.Methods = { + addMethods: function(source) { + var ancestor = this.superclass && this.superclass.prototype; + var properties = Object.keys(source); + + if (!Object.keys({ toString: true }).length) + properties.push("toString", "valueOf"); + + for (var i = 0, length = properties.length; i < length; i++) { + var property = properties[i], value = source[property]; + if (ancestor && Object.isFunction(value) && + value.argumentNames().first() == "$super") { + var method = value, value = Object.extend((function(m) { + return function() { return ancestor[m].apply(this, arguments) }; + })(property).wrap(method), { + valueOf: function() { return method }, + toString: function() { return method.toString() } + }); + } + this.prototype[property] = value; + } + + return this; + } +}; + +var Abstract = { }; + +Object.extend = function(destination, source) { + for (var property in source) + destination[property] = source[property]; + return destination; +}; + +Object.extend(Object, { + inspect: function(object) { + try { + if (Object.isUndefined(object)) return 'undefined'; + if (object === null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } + }, + + toJSON: function(object) { + var type = typeof object; + switch (type) { + case 'undefined': + case 'function': + case 'unknown': return; + case 'boolean': return object.toString(); + } + + if (object === null) return 'null'; + if (object.toJSON) return object.toJSON(); + if (Object.isElement(object)) return; + + var results = []; + for (var property in object) { + var value = Object.toJSON(object[property]); + if (!Object.isUndefined(value)) + results.push(property.toJSON() + ': ' + value); + } + + return '{' + results.join(', ') + '}'; + }, + + toQueryString: function(object) { + return $H(object).toQueryString(); + }, + + toHTML: function(object) { + return object && object.toHTML ? object.toHTML() : String.interpret(object); + }, + + keys: function(object) { + var keys = []; + for (var property in object) + keys.push(property); + return keys; + }, + + values: function(object) { + var values = []; + for (var property in object) + values.push(object[property]); + return values; + }, + + clone: function(object) { + return Object.extend({ }, object); + }, + + isElement: function(object) { + return object && object.nodeType == 1; + }, + + isArray: function(object) { + return object && object.constructor === Array; + }, + + isHash: function(object) { + return object instanceof Hash; + }, + + isFunction: function(object) { + return typeof object == "function"; + }, + + isString: function(object) { + return typeof object == "string"; + }, + + isNumber: function(object) { + return typeof object == "number"; + }, + + isUndefined: function(object) { + return typeof object == "undefined"; + } +}); + +Object.extend(Function.prototype, { + argumentNames: function() { + var names = this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip"); + return names.length == 1 && !names[0] ? [] : names; + }, + + bind: function() { + if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } + }, + + bindAsEventListener: function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function(event) { + return __method.apply(object, [event || window.event].concat(args)); + } + }, + + curry: function() { + if (!arguments.length) return this; + var __method = this, args = $A(arguments); + return function() { + return __method.apply(this, args.concat($A(arguments))); + } + }, + + delay: function() { + var __method = this, args = $A(arguments), timeout = args.shift() * 1000; + return window.setTimeout(function() { + return __method.apply(__method, args); + }, timeout); + }, + + wrap: function(wrapper) { + var __method = this; + return function() { + return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); + } + }, + + methodize: function() { + if (this._methodized) return this._methodized; + var __method = this; + return this._methodized = function() { + return __method.apply(null, [this].concat($A(arguments))); + }; + } +}); + +Function.prototype.defer = Function.prototype.delay.curry(0.01); + +Date.prototype.toJSON = function() { + return '"' + this.getUTCFullYear() + '-' + + (this.getUTCMonth() + 1).toPaddedString(2) + '-' + + this.getUTCDate().toPaddedString(2) + 'T' + + this.getUTCHours().toPaddedString(2) + ':' + + this.getUTCMinutes().toPaddedString(2) + ':' + + this.getUTCSeconds().toPaddedString(2) + 'Z"'; +}; + +var Try = { + these: function() { + var returnValue; + + for (var i = 0, length = arguments.length; i < length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) { } + } + + return returnValue; + } +}; + +RegExp.prototype.match = RegExp.prototype.test; + +RegExp.escape = function(str) { + return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); +}; + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create({ + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + execute: function() { + this.callback(this); + }, + + stop: function() { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = null; + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.execute(); + } finally { + this.currentlyExecuting = false; + } + } + } +}); +Object.extend(String, { + interpret: function(value) { + return value == null ? '' : String(value); + }, + specialChar: { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '\\': '\\\\' + } +}); + +Object.extend(String.prototype, { + gsub: function(pattern, replacement) { + var result = '', source = this, match; + replacement = arguments.callee.prepareReplacement(replacement); + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += String.interpret(replacement(match)); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + }, + + sub: function(pattern, replacement, count) { + replacement = this.gsub.prepareReplacement(replacement); + count = Object.isUndefined(count) ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + }, + + scan: function(pattern, iterator) { + this.gsub(pattern, iterator); + return String(this); + }, + + truncate: function(length, truncation) { + length = length || 30; + truncation = Object.isUndefined(truncation) ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : String(this); + }, + + strip: function() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + }, + + 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(function(script) { return eval(script) }); + }, + + escapeHTML: function() { + var self = arguments.callee; + self.text.data = this; + return self.div.innerHTML; + }, + + unescapeHTML: function() { + var div = new Element('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? (div.childNodes.length > 1 ? + $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) : + div.childNodes[0].nodeValue) : ''; + }, + + toQueryParams: function(separator) { + var match = this.strip().match(/([^?#]*)(#.*)?$/); + if (!match) return { }; + + return match[1].split(separator || '&').inject({ }, function(hash, pair) { + if ((pair = pair.split('='))[0]) { + var key = decodeURIComponent(pair.shift()); + var value = pair.length > 1 ? pair.join('=') : pair[0]; + if (value != undefined) value = decodeURIComponent(value); + + if (key in hash) { + if (!Object.isArray(hash[key])) hash[key] = [hash[key]]; + hash[key].push(value); + } + else hash[key] = value; + } + return hash; + }); + }, + + toArray: function() { + return this.split(''); + }, + + succ: function() { + return this.slice(0, this.length - 1) + + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); + }, + + times: function(count) { + return count < 1 ? '' : new Array(count + 1).join(this); + }, + + camelize: function() { + var parts = this.split('-'), len = parts.length; + if (len == 1) return parts[0]; + + var camelized = this.charAt(0) == '-' + ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) + : parts[0]; + + for (var i = 1; i < len; i++) + camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); + + return camelized; + }, + + capitalize: function() { + return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); + }, + + underscore: function() { + return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase(); + }, + + dasherize: function() { + return this.gsub(/_/,'-'); + }, + + inspect: function(useDoubleQuotes) { + var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) { + var character = String.specialChar[match[0]]; + return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); + }); + if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; + return "'" + escapedString.replace(/'/g, '\\\'') + "'"; + }, + + toJSON: function() { + return this.inspect(true); + }, + + unfilterJSON: function(filter) { + return this.sub(filter || Prototype.JSONFilter, '#{1}'); + }, + + isJSON: function() { + var str = this; + if (str.blank()) return false; + str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); + return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); + }, + + evalJSON: function(sanitize) { + var json = this.unfilterJSON(); + try { + if (!sanitize || json.isJSON()) return eval('(' + json + ')'); + } catch (e) { } + throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); + }, + + include: function(pattern) { + return this.indexOf(pattern) > -1; + }, + + startsWith: function(pattern) { + return this.indexOf(pattern) === 0; + }, + + endsWith: function(pattern) { + var d = this.length - pattern.length; + return d >= 0 && this.lastIndexOf(pattern) === d; + }, + + empty: function() { + return this == ''; + }, + + blank: function() { + return /^\s*$/.test(this); + }, + + interpolate: function(object, pattern) { + return new Template(this, pattern).evaluate(object); + } +}); + +if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, { + escapeHTML: function() { + return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); + }, + unescapeHTML: function() { + return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>'); + } +}); + +String.prototype.gsub.prepareReplacement = function(replacement) { + if (Object.isFunction(replacement)) return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; +}; + +String.prototype.parseQuery = String.prototype.toQueryParams; + +Object.extend(String.prototype.escapeHTML, { + div: document.createElement('div'), + text: document.createTextNode('') +}); + +with (String.prototype.escapeHTML) div.appendChild(text); + +var Template = Class.create({ + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + if (Object.isFunction(object.toTemplateReplacements)) + object = object.toTemplateReplacements(); + + return this.template.gsub(this.pattern, function(match) { + if (object == null) return ''; + + var before = match[1] || ''; + if (before == '\\') return match[2]; + + var ctx = object, expr = match[3]; + var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; + match = pattern.exec(expr); + if (match == null) return before; + + while (match != null) { + var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1]; + ctx = ctx[comp]; + if (null == ctx || '' == match[3]) break; + expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); + match = pattern.exec(expr); + } + + return before + String.interpret(ctx); + }.bind(this)); + } +}); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; + +var $break = { }; + +var Enumerable = { + each: function(iterator, context) { + var index = 0; + iterator = iterator.bind(context); + try { + this._each(function(value) { + iterator(value, index++); + }); + } catch (e) { + if (e != $break) throw e; + } + return this; + }, + + eachSlice: function(number, iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var index = -number, slices = [], array = this.toArray(); + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.collect(iterator, context); + }, + + all: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result = true; + this.each(function(value, index) { + result = result && !!iterator(value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result = false; + this.each(function(value, index) { + if (result = !!iterator(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var results = []; + this.each(function(value, index) { + results.push(iterator(value, index)); + }); + return results; + }, + + detect: function(iterator, context) { + iterator = iterator.bind(context); + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator, context) { + iterator = iterator.bind(context); + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(filter, iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var results = []; + + if (Object.isString(filter)) + filter = new RegExp(filter); + + this.each(function(value, index) { + if (filter.match(value)) + results.push(iterator(value, index)); + }); + return results; + }, + + include: function(object) { + if (Object.isFunction(this.indexOf)) + if (this.indexOf(object) != -1) return true; + + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inGroupsOf: function(number, fillWith) { + fillWith = Object.isUndefined(fillWith) ? null : fillWith; + return this.eachSlice(number, function(slice) { + while(slice.length < number) slice.push(fillWith); + return slice; + }); + }, + + inject: function(memo, iterator, context) { + iterator = iterator.bind(context); + this.each(function(value, index) { + memo = iterator(memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.map(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result; + this.each(function(value, index) { + value = iterator(value, index); + if (result == null || value >= result) + result = value; + }); + return result; + }, + + min: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result; + this.each(function(value, index) { + value = iterator(value, index); + if (result == null || value < result) + result = value; + }); + return result; + }, + + partition: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var trues = [], falses = []; + this.each(function(value, index) { + (iterator(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator, context) { + iterator = iterator.bind(context); + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator, context) { + iterator = iterator.bind(context); + return this.map(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.map(); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (Object.isFunction(args.last())) + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + return iterator(collections.pluck(index)); + }); + }, + + size: function() { + return this.toArray().length; + }, + + inspect: function() { + return '#<Enumerable:' + this.toArray().inspect() + '>'; + } +}; + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + filter: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray, + every: Enumerable.all, + some: Enumerable.any +}); +function $A(iterable) { + if (!iterable) return []; + if (iterable.toArray) return iterable.toArray(); + var length = iterable.length, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; +} + +if (Prototype.Browser.WebKit) { + function $A(iterable) { + if (!iterable) return []; + if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') && + iterable.toArray) return iterable.toArray(); + var length = iterable.length, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; + } +} + +Array.from = $A; + +Object.extend(Array.prototype, Enumerable); + +if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0, length = this.length; i < 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 != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(Object.isArray(value) ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + reduce: function() { + return this.length > 1 ? this : this[0]; + }, + + uniq: function(sorted) { + return this.inject([], function(array, value, index) { + if (0 == index || (sorted ? array.last() != value : !array.include(value))) + array.push(value); + return array; + }); + }, + + intersect: function(array) { + return this.uniq().findAll(function(item) { + return array.detect(function(value) { return item === value }); + }); + }, + + clone: function() { + return [].concat(this); + }, + + size: function() { + return this.length; + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + }, + + toJSON: function() { + var results = []; + this.each(function(object) { + var value = Object.toJSON(object); + if (!Object.isUndefined(value)) results.push(value); + }); + return '[' + results.join(', ') + ']'; + } +}); + +// use native browser JS 1.6 implementation if available +if (Object.isFunction(Array.prototype.forEach)) + Array.prototype._each = Array.prototype.forEach; + +if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) { + i || (i = 0); + var length = this.length; + if (i < 0) i = length + i; + for (; i < length; i++) + if (this[i] === item) return i; + return -1; +}; + +if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) { + i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; + var n = this.slice(0, i).reverse().indexOf(item); + return (n < 0) ? n : i - n - 1; +}; + +Array.prototype.toArray = Array.prototype.clone; + +function $w(string) { + if (!Object.isString(string)) return []; + string = string.strip(); + return string ? string.split(/\s+/) : []; +} + +if (Prototype.Browser.Opera){ + Array.prototype.concat = function() { + var array = []; + for (var i = 0, length = this.length; i < length; i++) array.push(this[i]); + for (var i = 0, length = arguments.length; i < length; i++) { + if (Object.isArray(arguments[i])) { + for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) + array.push(arguments[i][j]); + } else { + array.push(arguments[i]); + } + } + return array; + }; +} +Object.extend(Number.prototype, { + toColorPart: function() { + return this.toPaddedString(2, 16); + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + }, + + toPaddedString: function(length, radix) { + var string = this.toString(radix || 10); + return '0'.times(length - string.length) + string; + }, + + toJSON: function() { + return isFinite(this) ? this.toString() : 'null'; + } +}); + +$w('abs round ceil floor').each(function(method){ + Number.prototype[method] = Math[method].methodize(); +}); +function $H(object) { + return new Hash(object); +}; + +var Hash = Class.create(Enumerable, (function() { + + function toQueryPair(key, value) { + if (Object.isUndefined(value)) return key; + return key + '=' + encodeURIComponent(String.interpret(value)); + } + + return { + initialize: function(object) { + this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); + }, + + _each: function(iterator) { + for (var key in this._object) { + var value = this._object[key], pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + set: function(key, value) { + return this._object[key] = value; + }, + + get: function(key) { + return this._object[key]; + }, + + unset: function(key) { + var value = this._object[key]; + delete this._object[key]; + return value; + }, + + toObject: function() { + return Object.clone(this._object); + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + index: function(value) { + var match = this.detect(function(pair) { + return pair.value === value; + }); + return match && match.key; + }, + + merge: function(object) { + return this.clone().update(object); + }, + + update: function(object) { + return new Hash(object).inject(this, function(result, pair) { + result.set(pair.key, pair.value); + return result; + }); + }, + + toQueryString: function() { + return this.map(function(pair) { + var key = encodeURIComponent(pair.key), values = pair.value; + + if (values && typeof values == 'object') { + if (Object.isArray(values)) + return values.map(toQueryPair.curry(key)).join('&'); + } + return toQueryPair(key, values); + }).join('&'); + }, + + inspect: function() { + return '#<Hash:{' + this.map(function(pair) { + return pair.map(Object.inspect).join(': '); + }).join(', ') + '}>'; + }, + + toJSON: function() { + return Object.toJSON(this.toObject()); + }, + + clone: function() { + return new Hash(this); + } + } +})()); + +Hash.prototype.toTemplateReplacements = Hash.prototype.toObject; +Hash.from = $H; +var ObjectRange = Class.create(Enumerable, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + while (this.include(value)) { + iterator(value); + value = value.succ(); + } + }, + + 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 XMLHttpRequest()}, + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')} + ) || false; + }, + + activeRequestCount: 0 +}; + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responder) { + if (!this.include(responder)) + this.responders.push(responder); + }, + + unregister: function(responder) { + this.responders = this.responders.without(responder); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (Object.isFunction(responder[callback])) { + 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 = Class.create({ + initialize: function(options) { + this.options = { + method: 'post', + asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + encoding: 'UTF-8', + parameters: '', + evalJSON: true, + evalJS: true + }; + Object.extend(this.options, options || { }); + + this.options.method = this.options.method.toLowerCase(); + + if (Object.isString(this.options.parameters)) + this.options.parameters = this.options.parameters.toQueryParams(); + else if (Object.isHash(this.options.parameters)) + this.options.parameters = this.options.parameters.toObject(); + } +}); + +Ajax.Request = Class.create(Ajax.Base, { + _complete: false, + + initialize: function($super, url, options) { + $super(options); + this.transport = Ajax.getTransport(); + this.request(url); + }, + + request: function(url) { + this.url = url; + this.method = this.options.method; + var params = Object.clone(this.options.parameters); + + if (!['get', 'post'].include(this.method)) { + // simulate other verbs over post + params['_method'] = this.method; + this.method = 'post'; + } + + this.parameters = params; + + if (params = Object.toQueryString(params)) { + // when GET, append parameters to URL + if (this.method == 'get') + this.url += (this.url.include('?') ? '&' : '?') + params; + else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) + params += '&_='; + } + + try { + var response = new Ajax.Response(this); + if (this.options.onCreate) this.options.onCreate(response); + Ajax.Responders.dispatch('onCreate', this, response); + + this.transport.open(this.method.toUpperCase(), this.url, + this.options.asynchronous); + + if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); + + this.transport.onreadystatechange = this.onStateChange.bind(this); + this.setRequestHeaders(); + + this.body = this.method == 'post' ? (this.options.postBody || params) : null; + this.transport.send(this.body); + + /* Force Firefox to handle ready state 4 for synchronous requests */ + if (!this.options.asynchronous && this.transport.overrideMimeType) + this.onStateChange(); + + } + catch (e) { + this.dispatchException(e); + } + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState > 1 && !((readyState == 4) && this._complete)) + this.respondToReadyState(this.transport.readyState); + }, + + setRequestHeaders: function() { + var headers = { + 'X-Requested-With': 'XMLHttpRequest', + 'X-Prototype-Version': Prototype.Version, + 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' + }; + + if (this.method == 'post') { + headers['Content-type'] = this.options.contentType + + (this.options.encoding ? '; charset=' + this.options.encoding : ''); + + /* Force "Connection: close" for older Mozilla browsers to work + * around a bug where XMLHttpRequest sends an incorrect + * Content-length header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType && + (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) + headers['Connection'] = 'close'; + } + + // user-defined headers + if (typeof this.options.requestHeaders == 'object') { + var extras = this.options.requestHeaders; + + if (Object.isFunction(extras.push)) + for (var i = 0, length = extras.length; i < length; i += 2) + headers[extras[i]] = extras[i+1]; + else + $H(extras).each(function(pair) { headers[pair.key] = pair.value }); + } + + for (var name in headers) + this.transport.setRequestHeader(name, headers[name]); + }, + + success: function() { + var status = this.getStatus(); + return !status || (status >= 200 && status < 300); + }, + + getStatus: function() { + try { + return this.transport.status || 0; + } catch (e) { return 0 } + }, + + respondToReadyState: function(readyState) { + var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); + + if (state == 'Complete') { + try { + this._complete = true; + (this.options['on' + response.status] + || this.options['on' + (this.success() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + var contentType = response.getHeader('Content-type'); + if (this.options.evalJS == 'force' + || (this.options.evalJS && contentType + && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) + this.evalResponse(); + } + + try { + (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); + Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + if (state == 'Complete') { + // avoid memory leak in MSIE: clean up + this.transport.onreadystatechange = Prototype.emptyFunction; + } + }, + + getHeader: function(name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) { return null } + }, + + evalResponse: function() { + try { + return eval((this.transport.responseText || '').unfilterJSON()); + } catch (e) { + this.dispatchException(e); + } + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Response = Class.create({ + initialize: function(request){ + this.request = request; + var transport = this.transport = request.transport, + readyState = this.readyState = transport.readyState; + + if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { + this.status = this.getStatus(); + this.statusText = this.getStatusText(); + this.responseText = String.interpret(transport.responseText); + this.headerJSON = this._getHeaderJSON(); + } + + if(readyState == 4) { + var xml = transport.responseXML; + this.responseXML = Object.isUndefined(xml) ? null : xml; + this.responseJSON = this._getResponseJSON(); + } + }, + + status: 0, + statusText: '', + + getStatus: Ajax.Request.prototype.getStatus, + + getStatusText: function() { + try { + return this.transport.statusText || ''; + } catch (e) { return '' } + }, + + getHeader: Ajax.Request.prototype.getHeader, + + getAllHeaders: function() { + try { + return this.getAllResponseHeaders(); + } catch (e) { return null } + }, + + getResponseHeader: function(name) { + return this.transport.getResponseHeader(name); + }, + + getAllResponseHeaders: function() { + return this.transport.getAllResponseHeaders(); + }, + + _getHeaderJSON: function() { + var json = this.getHeader('X-JSON'); + if (!json) return null; + json = decodeURIComponent(escape(json)); + try { + return json.evalJSON(this.request.options.sanitizeJSON); + } catch (e) { + this.request.dispatchException(e); + } + }, + + _getResponseJSON: function() { + var options = this.request.options; + if (!options.evalJSON || (options.evalJSON != 'force' && + !(this.getHeader('Content-type') || '').include('application/json')) || + this.responseText.blank()) + return null; + try { + return this.responseText.evalJSON(options.sanitizeJSON); + } catch (e) { + this.request.dispatchException(e); + } + } +}); + +Ajax.Updater = Class.create(Ajax.Request, { + initialize: function($super, container, url, options) { + this.container = { + success: (container.success || container), + failure: (container.failure || (container.success ? null : container)) + }; + + options = Object.clone(options); + var onComplete = options.onComplete; + options.onComplete = (function(response, json) { + this.updateContent(response.responseText); + if (Object.isFunction(onComplete)) onComplete(response, json); + }).bind(this); + + $super(url, options); + }, + + updateContent: function(responseText) { + var receiver = this.container[this.success() ? 'success' : 'failure'], + options = this.options; + + if (!options.evalScripts) responseText = responseText.stripScripts(); + + if (receiver = $(receiver)) { + if (options.insertion) { + if (Object.isString(options.insertion)) { + var insertion = { }; insertion[options.insertion] = responseText; + receiver.insert(insertion); + } + else options.insertion(receiver, responseText); + } + else receiver.update(responseText); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { + initialize: function($super, container, url, options) { + $super(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.options.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(response) { + if (this.options.decay) { + this.decay = (response.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = response.responseText; + } + this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +function $(element) { + if (arguments.length > 1) { + for (var i = 0, elements = [], length = arguments.length; i < length; i++) + elements.push($(arguments[i])); + return elements; + } + if (Object.isString(element)) + element = document.getElementById(element); + return Element.extend(element); +} + +if (Prototype.BrowserFeatures.XPath) { + document._getElementsByXPath = function(expression, parentElement) { + var results = []; + var query = document.evaluate(expression, $(parentElement) || document, + null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + for (var i = 0, length = query.snapshotLength; i < length; i++) + results.push(Element.extend(query.snapshotItem(i))); + return results; + }; +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Node) var Node = { }; + +if (!Node.ELEMENT_NODE) { + // DOM level 2 ECMAScript Language Binding + Object.extend(Node, { + ELEMENT_NODE: 1, + ATTRIBUTE_NODE: 2, + TEXT_NODE: 3, + CDATA_SECTION_NODE: 4, + ENTITY_REFERENCE_NODE: 5, + ENTITY_NODE: 6, + PROCESSING_INSTRUCTION_NODE: 7, + COMMENT_NODE: 8, + DOCUMENT_NODE: 9, + DOCUMENT_TYPE_NODE: 10, + DOCUMENT_FRAGMENT_NODE: 11, + NOTATION_NODE: 12 + }); +} + +(function() { + var element = this.Element; + this.Element = function(tagName, attributes) { + attributes = attributes || { }; + tagName = tagName.toLowerCase(); + var cache = Element.cache; + if (Prototype.Browser.IE && attributes.name) { + tagName = '<' + tagName + ' name="' + attributes.name + '">'; + delete attributes.name; + return Element.writeAttribute(document.createElement(tagName), attributes); + } + if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); + return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); + }; + Object.extend(this.Element, element || { }); +}).call(window); + +Element.cache = { }; + +Element.Methods = { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function(element) { + element = $(element); + Element[Element.visible(element) ? 'hide' : 'show'](element); + return element; + }, + + hide: function(element) { + $(element).style.display = 'none'; + return element; + }, + + show: function(element) { + $(element).style.display = ''; + return element; + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + return element; + }, + + update: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + content = Object.toHTML(content); + element.innerHTML = content.stripScripts(); + content.evalScripts.bind(content).defer(); + return element; + }, + + replace: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + else if (!Object.isElement(content)) { + content = Object.toHTML(content); + var range = element.ownerDocument.createRange(); + range.selectNode(element); + content.evalScripts.bind(content).defer(); + content = range.createContextualFragment(content.stripScripts()); + } + element.parentNode.replaceChild(content, element); + return element; + }, + + insert: function(element, insertions) { + element = $(element); + + if (Object.isString(insertions) || Object.isNumber(insertions) || + Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) + insertions = {bottom:insertions}; + + var content, t, range; + + for (position in insertions) { + content = insertions[position]; + position = position.toLowerCase(); + t = Element._insertionTranslations[position]; + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + t.insert(element, content); + continue; + } + + content = Object.toHTML(content); + + range = element.ownerDocument.createRange(); + t.initializeRange(element, range); + t.insert(element, range.createContextualFragment(content.stripScripts())); + + content.evalScripts.bind(content).defer(); + } + + return element; + }, + + wrap: function(element, wrapper, attributes) { + element = $(element); + if (Object.isElement(wrapper)) + $(wrapper).writeAttribute(attributes || { }); + else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes); + else wrapper = new Element('div', wrapper); + if (element.parentNode) + element.parentNode.replaceChild(wrapper, element); + wrapper.appendChild(element); + return wrapper; + }, + + inspect: function(element) { + element = $(element); + var result = '<' + element.tagName.toLowerCase(); + $H({'id': 'id', 'className': 'class'}).each(function(pair) { + var property = pair.first(), attribute = pair.last(); + var value = (element[property] || '').toString(); + if (value) result += ' ' + attribute + '=' + value.inspect(true); + }); + return result + '>'; + }, + + recursivelyCollect: function(element, property) { + element = $(element); + var elements = []; + while (element = element[property]) + if (element.nodeType == 1) + elements.push(Element.extend(element)); + return elements; + }, + + ancestors: function(element) { + return $(element).recursivelyCollect('parentNode'); + }, + + descendants: function(element) { + return $(element).getElementsBySelector("*"); + }, + + firstDescendant: function(element) { + element = $(element).firstChild; + while (element && element.nodeType != 1) element = element.nextSibling; + return $(element); + }, + + immediateDescendants: function(element) { + if (!(element = $(element).firstChild)) return []; + while (element && element.nodeType != 1) element = element.nextSibling; + if (element) return [element].concat($(element).nextSiblings()); + return []; + }, + + previousSiblings: function(element) { + return $(element).recursivelyCollect('previousSibling'); + }, + + nextSiblings: function(element) { + return $(element).recursivelyCollect('nextSibling'); + }, + + siblings: function(element) { + element = $(element); + return element.previousSiblings().reverse().concat(element.nextSiblings()); + }, + + match: function(element, selector) { + if (Object.isString(selector)) + selector = new Selector(selector); + return selector.match($(element)); + }, + + up: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(element.parentNode); + var ancestors = element.ancestors(); + return expression ? Selector.findElement(ancestors, expression, index) : + ancestors[index || 0]; + }, + + down: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return element.firstDescendant(); + var descendants = element.descendants(); + return expression ? Selector.findElement(descendants, expression, index) : + descendants[index || 0]; + }, + + previous: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); + var previousSiblings = element.previousSiblings(); + return expression ? Selector.findElement(previousSiblings, expression, index) : + previousSiblings[index || 0]; + }, + + next: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); + var nextSiblings = element.nextSiblings(); + return expression ? Selector.findElement(nextSiblings, expression, index) : + nextSiblings[index || 0]; + }, + + select: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element, args); + }, + + adjacent: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element.parentNode, args).without(element); + }, + + identify: function(element) { + element = $(element); + var id = element.readAttribute('id'), self = arguments.callee; + if (id) return id; + do { id = 'anonymous_element_' + self.counter++ } while ($(id)); + element.writeAttribute('id', id); + return id; + }, + + readAttribute: function(element, name) { + element = $(element); + if (Prototype.Browser.IE) { + var t = Element._attributeTranslations.read; + if (t.values[name]) return t.values[name](element, name); + if (t.names[name]) name = t.names[name]; + if (name.include(':')) { + return (!element.attributes || !element.attributes[name]) ? null : + element.attributes[name].value; + } + } + return element.getAttribute(name); + }, + + writeAttribute: function(element, name, value) { + element = $(element); + var attributes = { }, t = Element._attributeTranslations.write; + + if (typeof name == 'object') attributes = name; + else attributes[name] = Object.isUndefined(value) ? true : value; + + for (var attr in attributes) { + name = t.names[attr] || attr; + value = attributes[attr]; + if (t.values[attr]) name = t.values[attr](element, value); + if (value === false || value === null) + element.removeAttribute(name); + else if (value === true) + element.setAttribute(name, name); + else element.setAttribute(name, value); + } + return element; + }, + + getHeight: function(element) { + return $(element).getDimensions().height; + }, + + getWidth: function(element) { + return $(element).getDimensions().width; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + var elementClassName = element.className; + return (elementClassName.length > 0 && (elementClassName == className || + new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + if (!element.hasClassName(className)) + element.className += (element.className ? ' ' : '') + className; + return element; + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + element.className = element.className.replace( + new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); + return element; + }, + + toggleClassName: function(element, className) { + if (!(element = $(element))) return; + return element[element.hasClassName(className) ? + 'removeClassName' : 'addClassName'](className); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + var node = element.firstChild; + while (node) { + var nextNode = node.nextSibling; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + element.removeChild(node); + node = nextNode; + } + return element; + }, + + empty: function(element) { + return $(element).innerHTML.blank(); + }, + + descendantOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + var originalAncestor = ancestor; + + if (element.compareDocumentPosition) + return (element.compareDocumentPosition(ancestor) & 8) === 8; + + if (element.sourceIndex && !Prototype.Browser.Opera) { + var e = element.sourceIndex, a = ancestor.sourceIndex, + nextAncestor = ancestor.nextSibling; + if (!nextAncestor) { + do { ancestor = ancestor.parentNode; } + while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode); + } + if (nextAncestor) return (e > a && e < nextAncestor.sourceIndex); + } + + while (element = element.parentNode) + if (element == originalAncestor) return true; + return false; + }, + + scrollTo: function(element) { + element = $(element); + var pos = element.cumulativeOffset(); + window.scrollTo(pos[0], pos[1]); + return element; + }, + + getStyle: function(element, style) { + element = $(element); + style = style == 'float' ? 'cssFloat' : style.camelize(); + var value = element.style[style]; + if (!value) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css[style] : null; + } + if (style == 'opacity') return value ? parseFloat(value) : 1.0; + return value == 'auto' ? null : value; + }, + + getOpacity: function(element) { + return $(element).getStyle('opacity'); + }, + + setStyle: function(element, styles) { + element = $(element); + var elementStyle = element.style, match; + if (Object.isString(styles)) { + element.style.cssText += ';' + styles; + return styles.include('opacity') ? + element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; + } + for (var property in styles) + if (property == 'opacity') element.setOpacity(styles[property]); + else + elementStyle[(property == 'float' || property == 'cssFloat') ? + (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') : + property] = styles[property]; + + return element; + }, + + setOpacity: function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + return element; + }, + + getDimensions: function(element) { + element = $(element); + var display = $(element).getStyle('display'); + if (display != 'none' && display != null) // Safari bug + 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; + var originalDisplay = els.display; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = 'block'; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = originalDisplay; + 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; + } + } + return element; + }, + + 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 = ''; + } + return element; + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return element; + element._overflow = Element.getStyle(element, 'overflow') || 'auto'; + if (element._overflow !== 'hidden') + element.style.overflow = 'hidden'; + return element; + }, + + undoClipping: function(element) { + element = $(element); + if (!element._overflow) return element; + element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; + element._overflow = null; + return element; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if (element.tagName == 'BODY') break; + var p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + absolutize: function(element) { + element = $(element); + if (element.getStyle('position') == 'absolute') return; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + var offsets = element.positionedOffset(); + 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'; + return element; + }, + + relativize: function(element) { + element = $(element); + if (element.getStyle('position') == 'relative') return; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + 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; + return element; + }, + + cumulativeScrollOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + getOffsetParent: 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); + }, + + viewportOffset: 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 && + Element.getStyle(element, 'position') == 'absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + if (!Prototype.Browser.Opera || element.tagName == 'BODY') { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } + } while (element = element.parentNode); + + return Element._returnOffset(valueL, valueT); + }, + + clonePosition: function(element, source) { + 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 = source.viewportOffset(); + + // find coordinate system to use + element = $(element); + 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(element, 'position') == 'absolute') { + parent = element.getOffsetParent(); + delta = parent.viewportOffset(); + } + + // 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) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if (options.setWidth) element.style.width = source.offsetWidth + 'px'; + if (options.setHeight) element.style.height = source.offsetHeight + 'px'; + return element; + } +}; + +Element.Methods.identify.counter = 1; + +Object.extend(Element.Methods, { + getElementsBySelector: Element.Methods.select, + childElements: Element.Methods.immediateDescendants +}); + +Element._attributeTranslations = { + write: { + names: { + className: 'class', + htmlFor: 'for' + }, + values: { } + } +}; + + +if (!document.createRange || Prototype.Browser.Opera) { + Element.Methods.insert = function(element, insertions) { + element = $(element); + + if (Object.isString(insertions) || Object.isNumber(insertions) || + Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) + insertions = { bottom: insertions }; + + var t = Element._insertionTranslations, content, position, pos, tagName; + + for (position in insertions) { + content = insertions[position]; + position = position.toLowerCase(); + pos = t[position]; + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + pos.insert(element, content); + continue; + } + + content = Object.toHTML(content); + tagName = ((position == 'before' || position == 'after') + ? element.parentNode : element).tagName.toUpperCase(); + + if (t.tags[tagName]) { + var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + if (position == 'top' || position == 'after') fragments.reverse(); + fragments.each(pos.insert.curry(element)); + } + else element.insertAdjacentHTML(pos.adjacency, content.stripScripts()); + + content.evalScripts.bind(content).defer(); + } + + return element; + }; +} + +if (Prototype.Browser.Opera) { + Element.Methods.getStyle = Element.Methods.getStyle.wrap( + function(proceed, element, style) { + switch (style) { + case 'left': case 'top': case 'right': case 'bottom': + if (proceed(element, 'position') === 'static') return null; + case 'height': case 'width': + // returns '0px' for hidden elements; we want it to return null + if (!Element.visible(element)) return null; + + // returns the border-box dimensions rather than the content-box + // dimensions, so we subtract padding and borders from the value + var dim = parseInt(proceed(element, style), 10); + + if (dim !== element['offset' + style.capitalize()]) + return dim + 'px'; + + var properties; + if (style === 'height') { + properties = ['border-top-width', 'padding-top', + 'padding-bottom', 'border-bottom-width']; + } + else { + properties = ['border-left-width', 'padding-left', + 'padding-right', 'border-right-width']; + } + return properties.inject(dim, function(memo, property) { + var val = proceed(element, property); + return val === null ? memo : memo - parseInt(val, 10); + }) + 'px'; + default: return proceed(element, style); + } + } + ); + + Element.Methods.readAttribute = Element.Methods.readAttribute.wrap( + function(proceed, element, attribute) { + if (attribute === 'title') return element.title; + return proceed(element, attribute); + } + ); +} + +else if (Prototype.Browser.IE) { + $w('positionedOffset getOffsetParent viewportOffset').each(function(method) { + Element.Methods[method] = Element.Methods[method].wrap( + function(proceed, element) { + element = $(element); + var position = element.getStyle('position'); + if (position != 'static') return proceed(element); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + }); + + Element.Methods.getStyle = function(element, style) { + element = $(element); + style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); + var value = element.style[style]; + if (!value && element.currentStyle) value = element.currentStyle[style]; + + if (style == 'opacity') { + if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if (value[1]) return parseFloat(value[1]) / 100; + return 1.0; + } + + if (value == 'auto') { + if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none')) + return element['offset' + style.capitalize()] + 'px'; + return null; + } + return value; + }; + + Element.Methods.setOpacity = function(element, value) { + function stripAlpha(filter){ + return filter.replace(/alpha\([^\)]*\)/gi,''); + } + element = $(element); + var currentStyle = element.currentStyle; + if ((currentStyle && !currentStyle.hasLayout) || + (!currentStyle && element.style.zoom == 'normal')) + element.style.zoom = 1; + + var filter = element.getStyle('filter'), style = element.style; + if (value == 1 || value === '') { + (filter = stripAlpha(filter)) ? + style.filter = filter : style.removeAttribute('filter'); + return element; + } else if (value < 0.00001) value = 0; + style.filter = stripAlpha(filter) + + 'alpha(opacity=' + (value * 100) + ')'; + return element; + }; + + Element._attributeTranslations = { + read: { + names: { + 'class': 'className', + 'for': 'htmlFor' + }, + values: { + _getAttr: function(element, attribute) { + return element.getAttribute(attribute, 2); + }, + _getAttrNode: function(element, attribute) { + var node = element.getAttributeNode(attribute); + return node ? node.value : ""; + }, + _getEv: function(element, attribute) { + attribute = element.getAttribute(attribute); + return attribute ? attribute.toString().slice(23, -2) : null; + }, + _flag: function(element, attribute) { + return $(element).hasAttribute(attribute) ? attribute : null; + }, + style: function(element) { + return element.style.cssText.toLowerCase(); + }, + title: function(element) { + return element.title; + } + } + } + }; + + Element._attributeTranslations.write = { + names: Object.clone(Element._attributeTranslations.read.names), + values: { + checked: function(element, value) { + element.checked = !!value; + }, + + style: function(element, value) { + element.style.cssText = value ? value : ''; + } + } + }; + + Element._attributeTranslations.has = {}; + + $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' + + 'encType maxLength readOnly longDesc').each(function(attr) { + Element._attributeTranslations.write.names[attr.toLowerCase()] = attr; + Element._attributeTranslations.has[attr.toLowerCase()] = attr; + }); + + (function(v) { + Object.extend(v, { + href: v._getAttr, + src: v._getAttr, + type: v._getAttr, + action: v._getAttrNode, + disabled: v._flag, + checked: v._flag, + readonly: v._flag, + multiple: v._flag, + onload: v._getEv, + onunload: v._getEv, + onclick: v._getEv, + ondblclick: v._getEv, + onmousedown: v._getEv, + onmouseup: v._getEv, + onmouseover: v._getEv, + onmousemove: v._getEv, + onmouseout: v._getEv, + onfocus: v._getEv, + onblur: v._getEv, + onkeypress: v._getEv, + onkeydown: v._getEv, + onkeyup: v._getEv, + onsubmit: v._getEv, + onreset: v._getEv, + onselect: v._getEv, + onchange: v._getEv + }); + })(Element._attributeTranslations.read.values); +} + +else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1) ? 0.999999 : + (value === '') ? '' : (value < 0.00001) ? 0 : value; + return element; + }; +} + +else if (Prototype.Browser.WebKit) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + + if (value == 1) + if(element.tagName == 'IMG' && element.width) { + element.width++; element.width--; + } else try { + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch (e) { } + + return element; + }; + + // Safari returns margins on body which is incorrect if the child is absolutely + // positioned. For performance reasons, redefine Element#cumulativeOffset for + // KHTML/WebKit only. + Element.Methods.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 Element._returnOffset(valueL, valueT); + }; +} + +if (Prototype.Browser.IE || Prototype.Browser.Opera) { + // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements + Element.Methods.update = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + + content = Object.toHTML(content); + var tagName = element.tagName.toUpperCase(); + + if (tagName in Element._insertionTranslations.tags) { + $A(element.childNodes).each(function(node) { element.removeChild(node) }); + Element._getContentFromAnonymousElement(tagName, content.stripScripts()) + .each(function(node) { element.appendChild(node) }); + } + else element.innerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +if (document.createElement('div').outerHTML) { + Element.Methods.replace = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + element.parentNode.replaceChild(content, element); + return element; + } + + content = Object.toHTML(content); + var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); + + if (Element._insertionTranslations.tags[tagName]) { + var nextSibling = element.next(); + var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + parent.removeChild(element); + if (nextSibling) + fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); + else + fragments.each(function(node) { parent.appendChild(node) }); + } + else element.outerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +Element._returnOffset = function(l, t) { + var result = [l, t]; + result.left = l; + result.top = t; + return result; +}; + +Element._getContentFromAnonymousElement = function(tagName, html) { + var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; + div.innerHTML = t[0] + html + t[1]; + t[2].times(function() { div = div.firstChild }); + return $A(div.childNodes); +}; + +Element._insertionTranslations = { + before: { + adjacency: 'beforeBegin', + insert: function(element, node) { + element.parentNode.insertBefore(node, element); + }, + initializeRange: function(element, range) { + range.setStartBefore(element); + } + }, + top: { + adjacency: 'afterBegin', + insert: function(element, node) { + element.insertBefore(node, element.firstChild); + }, + initializeRange: function(element, range) { + range.selectNodeContents(element); + range.collapse(true); + } + }, + bottom: { + adjacency: 'beforeEnd', + insert: function(element, node) { + element.appendChild(node); + } + }, + after: { + adjacency: 'afterEnd', + insert: function(element, node) { + element.parentNode.insertBefore(node, element.nextSibling); + }, + initializeRange: function(element, range) { + range.setStartAfter(element); + } + }, + tags: { + TABLE: ['<table>', '</table>', 1], + TBODY: ['<table><tbody>', '</tbody></table>', 2], + TR: ['<table><tbody><tr>', '</tr></tbody></table>', 3], + TD: ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4], + SELECT: ['<select>', '</select>', 1] + } +}; + +(function() { + this.bottom.initializeRange = this.top.initializeRange; + Object.extend(this.tags, { + THEAD: this.tags.TBODY, + TFOOT: this.tags.TBODY, + TH: this.tags.TD + }); +}).call(Element._insertionTranslations); + +Element.Methods.Simulated = { + hasAttribute: function(element, attribute) { + attribute = Element._attributeTranslations.has[attribute] || attribute; + var node = $(element).getAttributeNode(attribute); + return node && node.specified; + } +}; + +Element.Methods.ByTag = { }; + +Object.extend(Element, Element.Methods); + +if (!Prototype.BrowserFeatures.ElementExtensions && + document.createElement('div').__proto__) { + window.HTMLElement = { }; + window.HTMLElement.prototype = document.createElement('div').__proto__; + Prototype.BrowserFeatures.ElementExtensions = true; +} + +Element.extend = (function() { + if (Prototype.BrowserFeatures.SpecificElementExtensions) + return Prototype.K; + + var Methods = { }, ByTag = Element.Methods.ByTag; + + var extend = Object.extend(function(element) { + if (!element || element._extendedByPrototype || + element.nodeType != 1 || element == window) return element; + + var methods = Object.clone(Methods), + tagName = element.tagName, property, value; + + // extend methods for specific tags + if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]); + + for (property in methods) { + value = methods[property]; + if (Object.isFunction(value) && !(property in element)) + element[property] = value.methodize(); + } + + element._extendedByPrototype = Prototype.emptyFunction; + return element; + + }, { + refresh: function() { + // extend methods for all tags (Safari doesn't need this) + if (!Prototype.BrowserFeatures.ElementExtensions) { + Object.extend(Methods, Element.Methods); + Object.extend(Methods, Element.Methods.Simulated); + } + } + }); + + extend.refresh(); + return extend; +})(); + +Element.hasAttribute = function(element, attribute) { + if (element.hasAttribute) return element.hasAttribute(attribute); + return Element.Methods.Simulated.hasAttribute(element, attribute); +}; + +Element.addMethods = function(methods) { + var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; + + if (!methods) { + Object.extend(Form, Form.Methods); + Object.extend(Form.Element, Form.Element.Methods); + Object.extend(Element.Methods.ByTag, { + "FORM": Object.clone(Form.Methods), + "INPUT": Object.clone(Form.Element.Methods), + "SELECT": Object.clone(Form.Element.Methods), + "TEXTAREA": Object.clone(Form.Element.Methods) + }); + } + + if (arguments.length == 2) { + var tagName = methods; + methods = arguments[1]; + } + + if (!tagName) Object.extend(Element.Methods, methods || { }); + else { + if (Object.isArray(tagName)) tagName.each(extend); + else extend(tagName); + } + + function extend(tagName) { + tagName = tagName.toUpperCase(); + if (!Element.Methods.ByTag[tagName]) + Element.Methods.ByTag[tagName] = { }; + Object.extend(Element.Methods.ByTag[tagName], methods); + } + + function copy(methods, destination, onlyIfAbsent) { + onlyIfAbsent = onlyIfAbsent || false; + for (var property in methods) { + var value = methods[property]; + if (!Object.isFunction(value)) continue; + if (!onlyIfAbsent || !(property in destination)) + destination[property] = value.methodize(); + } + } + + function findDOMClass(tagName) { + var klass; + var trans = { + "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", + "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", + "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", + "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", + "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": + "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": + "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": + "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": + "FrameSet", "IFRAME": "IFrame" + }; + if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName.capitalize() + 'Element'; + if (window[klass]) return window[klass]; + + window[klass] = { }; + window[klass].prototype = document.createElement(tagName).__proto__; + return window[klass]; + } + + if (F.ElementExtensions) { + copy(Element.Methods, HTMLElement.prototype); + copy(Element.Methods.Simulated, HTMLElement.prototype, true); + } + + if (F.SpecificElementExtensions) { + for (var tag in Element.Methods.ByTag) { + var klass = findDOMClass(tag); + if (Object.isUndefined(klass)) continue; + copy(T[tag], klass.prototype); + } + } + + Object.extend(Element, Element.Methods); + delete Element.ByTag; + + if (Element.extend.refresh) Element.extend.refresh(); + Element.cache = { }; +}; + +document.viewport = { + getDimensions: function() { + var dimensions = { }; + var B = Prototype.Browser; + $w('width height').each(function(d) { + var D = d.capitalize(); + dimensions[d] = (B.WebKit && !document.evaluate) ? self['inner' + D] : + (B.Opera) ? document.body['client' + D] : document.documentElement['client' + D]; + }); + return dimensions; + }, + + getWidth: function() { + return this.getDimensions().width; + }, + + getHeight: function() { + return this.getDimensions().height; + }, + + getScrollOffsets: function() { + return Element._returnOffset( + window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, + window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); + } +}; +/* Portions of the Selector class are derived from Jack Slocum’s DomQuery, + * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style + * license. Please see http://www.yui-ext.com/ for more information. */ + +var Selector = Class.create({ + initialize: function(expression) { + this.expression = expression.strip(); + this.compileMatcher(); + }, + + shouldUseXPath: function() { + if (!Prototype.BrowserFeatures.XPath) return false; + + var e = this.expression; + + // Safari 3 chokes on :*-of-type and :empty + if (Prototype.Browser.WebKit && + (e.include("-of-type") || e.include(":empty"))) + return false; + + // XPath can't do namespaced attributes, nor can it read + // the "checked" property from DOM nodes + if ((/(\[[\w-]*?:|:checked)/).test(this.expression)) + return false; + + return true; + }, + + compileMatcher: function() { + if (this.shouldUseXPath()) + return this.compileXPathMatcher(); + + var e = this.expression, ps = Selector.patterns, h = Selector.handlers, + c = Selector.criteria, le, p, m; + + if (Selector._cache[e]) { + this.matcher = Selector._cache[e]; + return; + } + + this.matcher = ["this.matcher = function(root) {", + "var r = root, h = Selector.handlers, c = false, n;"]; + + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + this.matcher.push(Object.isFunction(c[i]) ? c[i](m) : + new Template(c[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.matcher.push("return h.unique(n);\n}"); + eval(this.matcher.join('\n')); + Selector._cache[this.expression] = this.matcher; + }, + + compileXPathMatcher: function() { + var e = this.expression, ps = Selector.patterns, + x = Selector.xpath, le, m; + + if (Selector._cache[e]) { + this.xpath = Selector._cache[e]; return; + } + + this.matcher = ['.//*']; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + if (m = e.match(ps[i])) { + this.matcher.push(Object.isFunction(x[i]) ? x[i](m) : + new Template(x[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.xpath = this.matcher.join(''); + Selector._cache[this.expression] = this.xpath; + }, + + findElements: function(root) { + root = root || document; + if (this.xpath) return document._getElementsByXPath(this.xpath, root); + return this.matcher(root); + }, + + match: function(element) { + this.tokens = []; + + var e = this.expression, ps = Selector.patterns, as = Selector.assertions; + var le, p, m; + + while (e && le !== e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + // use the Selector.assertions methods unless the selector + // is too complex. + if (as[i]) { + this.tokens.push([i, Object.clone(m)]); + e = e.replace(m[0], ''); + } else { + // reluctantly do a document-wide search + // and look for a match in the array + return this.findElements(document).include(element); + } + } + } + } + + var match = true, name, matches; + for (var i = 0, token; token = this.tokens[i]; i++) { + name = token[0], matches = token[1]; + if (!Selector.assertions[name](element, matches)) { + match = false; break; + } + } + + return match; + }, + + toString: function() { + return this.expression; + }, + + inspect: function() { + return "#<Selector:" + this.expression.inspect() + ">"; + } +}); + +Object.extend(Selector, { + _cache: { }, + + xpath: { + descendant: "//*", + child: "/*", + adjacent: "/following-sibling::*[1]", + laterSibling: '/following-sibling::*', + tagName: function(m) { + if (m[1] == '*') return ''; + return "[local-name()='" + m[1].toLowerCase() + + "' or local-name()='" + m[1].toUpperCase() + "']"; + }, + className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", + id: "[@id='#{1}']", + attrPresence: function(m) { + m[1] = m[1].toLowerCase(); + return new Template("[@#{1}]").evaluate(m); + }, + attr: function(m) { + m[1] = m[1].toLowerCase(); + m[3] = m[5] || m[6]; + return new Template(Selector.xpath.operators[m[2]]).evaluate(m); + }, + pseudo: function(m) { + var h = Selector.xpath.pseudos[m[1]]; + if (!h) return ''; + if (Object.isFunction(h)) return h(m); + return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); + }, + operators: { + '=': "[@#{1}='#{3}']", + '!=': "[@#{1}!='#{3}']", + '^=': "[starts-with(@#{1}, '#{3}')]", + '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", + '*=': "[contains(@#{1}, '#{3}')]", + '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", + '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" + }, + pseudos: { + 'first-child': '[not(preceding-sibling::*)]', + 'last-child': '[not(following-sibling::*)]', + 'only-child': '[not(preceding-sibling::* or following-sibling::*)]', + 'empty': "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]", + 'checked': "[@checked]", + 'disabled': "[@disabled]", + 'enabled': "[not(@disabled)]", + 'not': function(m) { + var e = m[6], p = Selector.patterns, + x = Selector.xpath, le, v; + + var exclusion = []; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in p) { + if (m = e.match(p[i])) { + v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m); + exclusion.push("(" + v.substring(1, v.length - 1) + ")"); + e = e.replace(m[0], ''); + break; + } + } + } + return "[not(" + exclusion.join(" and ") + ")]"; + }, + 'nth-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m); + }, + 'nth-last-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m); + }, + 'nth-of-type': function(m) { + return Selector.xpath.pseudos.nth("position() ", m); + }, + 'nth-last-of-type': function(m) { + return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m); + }, + 'first-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m); + }, + 'last-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m); + }, + 'only-of-type': function(m) { + var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m); + }, + nth: function(fragment, m) { + var mm, formula = m[6], predicate; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + if (mm = formula.match(/^(\d+)$/)) // digit only + return '[' + fragment + "= " + mm[1] + ']'; + if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (mm[1] == "-") mm[1] = -1; + var a = mm[1] ? Number(mm[1]) : 1; + var b = mm[2] ? Number(mm[2]) : 0; + predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " + + "((#{fragment} - #{b}) div #{a} >= 0)]"; + return new Template(predicate).evaluate({ + fragment: fragment, a: a, b: b }); + } + } + } + }, + + criteria: { + tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', + className: 'n = h.className(n, r, "#{1}", c); c = false;', + id: 'n = h.id(n, r, "#{1}", c); c = false;', + attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;', + attr: function(m) { + m[3] = (m[5] || m[6]); + return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m); + }, + pseudo: function(m) { + if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); + return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m); + }, + descendant: 'c = "descendant";', + child: 'c = "child";', + adjacent: 'c = "adjacent";', + laterSibling: 'c = "laterSibling";' + }, + + patterns: { + // combinators must be listed first + // (and descendant needs to be last combinator) + laterSibling: /^\s*~\s*/, + child: /^\s*>\s*/, + adjacent: /^\s*\+\s*/, + descendant: /^\s/, + + // selectors follow + tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, + id: /^#([\w\-\*]+)(\b|$)/, + className: /^\.([\w\-\*]+)(\b|$)/, + pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s)|(?=:))/, + attrPresence: /^\[([\w]+)\]/, + attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ + }, + + // for Selector.match and Element#match + assertions: { + tagName: function(element, matches) { + return matches[1].toUpperCase() == element.tagName.toUpperCase(); + }, + + className: function(element, matches) { + return Element.hasClassName(element, matches[1]); + }, + + id: function(element, matches) { + return element.id === matches[1]; + }, + + attrPresence: function(element, matches) { + return Element.hasAttribute(element, matches[1]); + }, + + attr: function(element, matches) { + var nodeValue = Element.readAttribute(element, matches[1]); + return Selector.operators[matches[2]](nodeValue, matches[3]); + } + }, + + handlers: { + // UTILITY FUNCTIONS + // joins two collections + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + a.push(node); + return a; + }, + + // marks an array of nodes for counting + mark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._counted = true; + return nodes; + }, + + unmark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._counted = undefined; + return nodes; + }, + + // mark each child node with its position (for nth calls) + // "ofType" flag indicates whether we're indexing for nth-of-type + // rather than nth-child + index: function(parentNode, reverse, ofType) { + parentNode._counted = true; + if (reverse) { + for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { + var node = nodes[i]; + if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; + } + } else { + for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) + if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; + } + }, + + // filters out duplicates and extends all nodes + unique: function(nodes) { + if (nodes.length == 0) return nodes; + var results = [], n; + for (var i = 0, l = nodes.length; i < l; i++) + if (!(n = nodes[i])._counted) { + n._counted = true; + results.push(Element.extend(n)); + } + return Selector.handlers.unmark(results); + }, + + // COMBINATOR FUNCTIONS + descendant: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName('*')); + return results; + }, + + child: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) { + for (var j = 0, child; child = node.childNodes[j]; j++) + if (child.nodeType == 1 && child.tagName != '!') results.push(child); + } + return results; + }, + + adjacent: function(nodes) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + var next = this.nextElementSibling(node); + if (next) results.push(next); + } + return results; + }, + + laterSibling: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, Element.nextSiblings(node)); + return results; + }, + + nextElementSibling: function(node) { + while (node = node.nextSibling) + if (node.nodeType == 1) return node; + return null; + }, + + previousElementSibling: function(node) { + while (node = node.previousSibling) + if (node.nodeType == 1) return node; + return null; + }, + + // TOKEN FUNCTIONS + tagName: function(nodes, root, tagName, combinator) { + tagName = tagName.toUpperCase(); + var results = [], h = Selector.handlers; + if (nodes) { + if (combinator) { + // fastlane for ordinary descendant combinators + if (combinator == "descendant") { + for (var i = 0, node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName(tagName)); + return results; + } else nodes = this[combinator](nodes); + if (tagName == "*") return nodes; + } + for (var i = 0, node; node = nodes[i]; i++) + if (node.tagName.toUpperCase() == tagName) results.push(node); + return results; + } else return root.getElementsByTagName(tagName); + }, + + id: function(nodes, root, id, combinator) { + var targetNode = $(id), h = Selector.handlers; + if (!targetNode) return []; + if (!nodes && root == document) return [targetNode]; + if (nodes) { + if (combinator) { + if (combinator == 'child') { + for (var i = 0, node; node = nodes[i]; i++) + if (targetNode.parentNode == node) return [targetNode]; + } else if (combinator == 'descendant') { + for (var i = 0, node; node = nodes[i]; i++) + if (Element.descendantOf(targetNode, node)) return [targetNode]; + } else if (combinator == 'adjacent') { + for (var i = 0, node; node = nodes[i]; i++) + if (Selector.handlers.previousElementSibling(targetNode) == node) + return [targetNode]; + } else nodes = h[combinator](nodes); + } + for (var i = 0, node; node = nodes[i]; i++) + if (node == targetNode) return [targetNode]; + return []; + } + return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; + }, + + className: function(nodes, root, className, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + return Selector.handlers.byClassName(nodes, root, className); + }, + + byClassName: function(nodes, root, className) { + if (!nodes) nodes = Selector.handlers.descendant([root]); + var needle = ' ' + className + ' '; + for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { + nodeClassName = node.className; + if (nodeClassName.length == 0) continue; + if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle)) + results.push(node); + } + return results; + }, + + attrPresence: function(nodes, root, attr) { + if (!nodes) nodes = root.getElementsByTagName("*"); + var results = []; + for (var i = 0, node; node = nodes[i]; i++) + if (Element.hasAttribute(node, attr)) results.push(node); + return results; + }, + + attr: function(nodes, root, attr, value, operator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + var handler = Selector.operators[operator], results = []; + for (var i = 0, node; node = nodes[i]; i++) { + var nodeValue = Element.readAttribute(node, attr); + if (nodeValue === null) continue; + if (handler(nodeValue, value)) results.push(node); + } + return results; + }, + + pseudo: function(nodes, name, value, root, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + if (!nodes) nodes = root.getElementsByTagName("*"); + return Selector.pseudos[name](nodes, value, root); + } + }, + + pseudos: { + 'first-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.previousElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'last-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.nextElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'only-child': function(nodes, value, root) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) + results.push(node); + return results; + }, + 'nth-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root); + }, + 'nth-last-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true); + }, + 'nth-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, false, true); + }, + 'nth-last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true, true); + }, + 'first-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, false, true); + }, + 'last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, true, true); + }, + 'only-of-type': function(nodes, formula, root) { + var p = Selector.pseudos; + return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); + }, + + // handles the an+b logic + getIndices: function(a, b, total) { + if (a == 0) return b > 0 ? [b] : []; + return $R(1, total).inject([], function(memo, i) { + if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i); + return memo; + }); + }, + + // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type + nth: function(nodes, formula, root, reverse, ofType) { + if (nodes.length == 0) return []; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + var h = Selector.handlers, results = [], indexed = [], m; + h.mark(nodes); + for (var i = 0, node; node = nodes[i]; i++) { + if (!node.parentNode._counted) { + h.index(node.parentNode, reverse, ofType); + indexed.push(node.parentNode); + } + } + if (formula.match(/^\d+$/)) { // just a number + formula = Number(formula); + for (var i = 0, node; node = nodes[i]; i++) + if (node.nodeIndex == formula) results.push(node); + } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (m[1] == "-") m[1] = -1; + var a = m[1] ? Number(m[1]) : 1; + var b = m[2] ? Number(m[2]) : 0; + var indices = Selector.pseudos.getIndices(a, b, nodes.length); + for (var i = 0, node, l = indices.length; node = nodes[i]; i++) { + for (var j = 0; j < l; j++) + if (node.nodeIndex == indices[j]) results.push(node); + } + } + h.unmark(nodes); + h.unmark(indexed); + return results; + }, + + 'empty': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + // IE treats comments as element nodes + if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue; + results.push(node); + } + return results; + }, + + 'not': function(nodes, selector, root) { + var h = Selector.handlers, selectorType, m; + var exclusions = new Selector(selector).findElements(root); + h.mark(exclusions); + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node._counted) results.push(node); + h.unmark(exclusions); + return results; + }, + + 'enabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node.disabled) results.push(node); + return results; + }, + + 'disabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.disabled) results.push(node); + return results; + }, + + 'checked': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.checked) results.push(node); + return results; + } + }, + + operators: { + '=': function(nv, v) { return nv == v; }, + '!=': function(nv, v) { return nv != v; }, + '^=': function(nv, v) { return nv.startsWith(v); }, + '$=': function(nv, v) { return nv.endsWith(v); }, + '*=': function(nv, v) { return nv.include(v); }, + '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, + '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); } + }, + + matchElements: function(elements, expression) { + var matches = new Selector(expression).findElements(), h = Selector.handlers; + h.mark(matches); + for (var i = 0, results = [], element; element = elements[i]; i++) + if (element._counted) results.push(element); + h.unmark(matches); + return results; + }, + + findElement: function(elements, expression, index) { + if (Object.isNumber(expression)) { + index = expression; expression = false; + } + return Selector.matchElements(elements, expression || '*')[index || 0]; + }, + + findChildElements: function(element, expressions) { + var exprs = expressions.join(','); + expressions = []; + exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { + expressions.push(m[1].strip()); + }); + var results = [], h = Selector.handlers; + for (var i = 0, l = expressions.length, selector; i < l; i++) { + selector = new Selector(expressions[i].strip()); + h.concat(results, selector.findElements(element)); + } + return (l > 1) ? h.unique(results) : results; + } +}); + +if (Prototype.Browser.IE) { + // IE returns comment nodes on getElementsByTagName("*"). + // Filter them out. + Selector.handlers.concat = function(a, b) { + for (var i = 0, node; node = b[i]; i++) + if (node.tagName !== "!") a.push(node); + return a; + }; +} + +function $$() { + return Selector.findChildElements(document, $A(arguments)); +} +var Form = { + reset: function(form) { + $(form).reset(); + return form; + }, + + serializeElements: function(elements, options) { + if (typeof options != 'object') options = { hash: !!options }; + else if (Object.isUndefined(options.hash)) options.hash = true; + var key, value, submitted = false, submit = options.submit; + + var data = elements.inject({ }, function(result, element) { + if (!element.disabled && element.name) { + key = element.name; value = $(element).getValue(); + if (value != null && (element.type != 'submit' || (!submitted && + submit !== false && (!submit || key == submit) && (submitted = true)))) { + if (key in result) { + // a key is already present; construct an array of values + if (!Object.isArray(result[key])) result[key] = [result[key]]; + result[key].push(value); + } + else result[key] = value; + } + } + return result; + }); + + return options.hash ? data : Object.toQueryString(data); + } +}; + +Form.Methods = { + serialize: function(form, options) { + return Form.serializeElements(Form.getElements(form), options); + }, + + getElements: function(form) { + return $A($(form).getElementsByTagName('*')).inject([], + function(elements, child) { + if (Form.Element.Serializers[child.tagName.toLowerCase()]) + elements.push(Element.extend(child)); + return elements; + } + ); + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) return $A(inputs).map(Element.extend); + + for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || (name && input.name != name)) + continue; + matchingInputs.push(Element.extend(input)); + } + + return matchingInputs; + }, + + disable: function(form) { + form = $(form); + Form.getElements(form).invoke('disable'); + return form; + }, + + enable: function(form) { + form = $(form); + Form.getElements(form).invoke('enable'); + return form; + }, + + findFirstElement: function(form) { + var elements = $(form).getElements().findAll(function(element) { + return 'hidden' != element.type && !element.disabled; + }); + var firstByIndex = elements.findAll(function(element) { + return element.hasAttribute('tabIndex') && element.tabIndex >= 0; + }).sortBy(function(element) { return element.tabIndex }).first(); + + return firstByIndex ? firstByIndex : elements.find(function(element) { + return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + form = $(form); + form.findFirstElement().activate(); + return form; + }, + + request: function(form, options) { + form = $(form), options = Object.clone(options || { }); + + var params = options.parameters, action = form.readAttribute('action') || ''; + if (action.blank()) action = window.location.href; + options.parameters = form.serialize(true); + + if (params) { + if (Object.isString(params)) params = params.toQueryParams(); + Object.extend(options.parameters, params); + } + + if (form.hasAttribute('method') && !options.method) + options.method = form.method; + + return new Ajax.Request(action, options); + } +}; + +/*--------------------------------------------------------------------------*/ + +Form.Element = { + focus: function(element) { + $(element).focus(); + return element; + }, + + select: function(element) { + $(element).select(); + return element; + } +}; + +Form.Element.Methods = { + serialize: function(element) { + element = $(element); + if (!element.disabled && element.name) { + var value = element.getValue(); + if (value != undefined) { + var pair = { }; + pair[element.name] = value; + return Object.toQueryString(pair); + } + } + return ''; + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + return Form.Element.Serializers[method](element); + }, + + setValue: function(element, value) { + element = $(element); + var method = element.tagName.toLowerCase(); + Form.Element.Serializers[method](element, value); + return element; + }, + + clear: function(element) { + $(element).value = ''; + return element; + }, + + present: function(element) { + return $(element).value != ''; + }, + + activate: function(element) { + element = $(element); + try { + element.focus(); + if (element.select && (element.tagName.toLowerCase() != 'input' || + !['button', 'reset', 'submit'].include(element.type))) + element.select(); + } catch (e) { } + return element; + }, + + disable: function(element) { + element = $(element); + element.blur(); + element.disabled = true; + return element; + }, + + enable: function(element) { + element = $(element); + element.disabled = false; + return element; + } +}; + +/*--------------------------------------------------------------------------*/ + +var Field = Form.Element; +var $F = Form.Element.Methods.getValue; + +/*--------------------------------------------------------------------------*/ + +Form.Element.Serializers = { + input: function(element, value) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element, value); + default: + return Form.Element.Serializers.textarea(element, value); + } + }, + + inputSelector: function(element, value) { + if (Object.isUndefined(value)) return element.checked ? element.value : null; + else element.checked = !!value; + }, + + textarea: function(element, value) { + if (Object.isUndefined(value)) return element.value; + else element.value = value; + }, + + select: function(element, index) { + if (Object.isUndefined(index)) + return this[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + else { + var opt, value, single = !Object.isArray(index); + for (var i = 0, length = element.length; i < length; i++) { + opt = element.options[i]; + value = this.optionValue(opt); + if (single) { + if (value == index) { + opt.selected = true; + return; + } + } + else opt.selected = index.include(value); + } + } + }, + + selectOne: function(element) { + var index = element.selectedIndex; + return index >= 0 ? this.optionValue(element.options[index]) : null; + }, + + selectMany: function(element) { + var values, length = element.length; + if (!length) return null; + + for (var i = 0, values = []; i < length; i++) { + var opt = element.options[i]; + if (opt.selected) values.push(this.optionValue(opt)); + } + return values; + }, + + optionValue: function(opt) { + // extend element because hasAttribute may not be native + return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; + } +}; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = Class.create(PeriodicalExecuter, { + initialize: function($super, element, frequency, callback) { + $super(callback, frequency); + this.element = $(element); + this.lastValue = this.getValue(); + }, + + execute: function() { + var value = this.getValue(); + if (Object.isString(this.lastValue) && Object.isString(value) ? + this.lastValue != value : String(this.lastValue) != String(value)) { + this.callback(this.element, value); + this.lastValue = value; + } + } +}); + +Form.Element.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = Class.create({ + 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() { + Form.getElements(this.element).each(this.registerCallback, this); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + default: + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +}); + +Form.Element.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) var Event = { }; + +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, + KEY_HOME: 36, + KEY_END: 35, + KEY_PAGEUP: 33, + KEY_PAGEDOWN: 34, + KEY_INSERT: 45, + + cache: { }, + + relatedTarget: function(event) { + var element; + switch(event.type) { + case 'mouseover': element = event.fromElement; break; + case 'mouseout': element = event.toElement; break; + default: return null; + } + return Element.extend(element); + } +}); + +Event.Methods = (function() { + var isButton; + + if (Prototype.Browser.IE) { + var buttonMap = { 0: 1, 1: 4, 2: 2 }; + isButton = function(event, code) { + return event.button == buttonMap[code]; + }; + + } else if (Prototype.Browser.WebKit) { + isButton = function(event, code) { + switch (code) { + case 0: return event.which == 1 && !event.metaKey; + case 1: return event.which == 1 && event.metaKey; + default: return false; + } + }; + + } else { + isButton = function(event, code) { + return event.which ? (event.which === code + 1) : (event.button === code); + }; + } + + return { + isLeftClick: function(event) { return isButton(event, 0) }, + isMiddleClick: function(event) { return isButton(event, 1) }, + isRightClick: function(event) { return isButton(event, 2) }, + + element: function(event) { + var node = Event.extend(event).target; + return Element.extend(node.nodeType == Node.TEXT_NODE ? node.parentNode : node); + }, + + findElement: function(event, expression) { + var element = Event.element(event); + if (!expression) return element; + var elements = [element].concat(element.ancestors()); + return Selector.findElement(elements, expression, 0); + }, + + pointer: function(event) { + return { + x: event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)), + y: event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)) + }; + }, + + pointerX: function(event) { return Event.pointer(event).x }, + pointerY: function(event) { return Event.pointer(event).y }, + + stop: function(event) { + Event.extend(event); + event.preventDefault(); + event.stopPropagation(); + event.stopped = true; + } + }; +})(); + +Event.extend = (function() { + var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { + m[name] = Event.Methods[name].methodize(); + return m; + }); + + if (Prototype.Browser.IE) { + Object.extend(methods, { + stopPropagation: function() { this.cancelBubble = true }, + preventDefault: function() { this.returnValue = false }, + inspect: function() { return "[object Event]" } + }); + + return function(event) { + if (!event) return false; + if (event._extendedByPrototype) return event; + + event._extendedByPrototype = Prototype.emptyFunction; + var pointer = Event.pointer(event); + Object.extend(event, { + target: event.srcElement, + relatedTarget: Event.relatedTarget(event), + pageX: pointer.x, + pageY: pointer.y + }); + return Object.extend(event, methods); + }; + + } else { + Event.prototype = Event.prototype || document.createEvent("HTMLEvents").__proto__; + Object.extend(Event.prototype, methods); + return Prototype.K; + } +})(); + +Object.extend(Event, (function() { + var cache = Event.cache; + + function getEventID(element) { + if (element._eventID) return element._eventID; + arguments.callee.id = arguments.callee.id || 1; + return element._eventID = ++arguments.callee.id; + } + + function getDOMEventName(eventName) { + if (eventName && eventName.include(':')) return "dataavailable"; + return eventName; + } + + function getCacheForID(id) { + return cache[id] = cache[id] || { }; + } + + function getWrappersForEventName(id, eventName) { + var c = getCacheForID(id); + return c[eventName] = c[eventName] || []; + } + + function createWrapper(element, eventName, handler) { + var id = getEventID(element); + var c = getWrappersForEventName(id, eventName); + if (c.pluck("handler").include(handler)) return false; + + var wrapper = function(event) { + if (!Event || !Event.extend || + (event.eventName && event.eventName != eventName)) + return false; + + Event.extend(event); + handler.call(element, event) + }; + + wrapper.handler = handler; + c.push(wrapper); + return wrapper; + } + + function findWrapper(id, eventName, handler) { + var c = getWrappersForEventName(id, eventName); + return c.find(function(wrapper) { return wrapper.handler == handler }); + } + + function destroyWrapper(id, eventName, handler) { + var c = getCacheForID(id); + if (!c[eventName]) return false; + c[eventName] = c[eventName].without(findWrapper(id, eventName, handler)); + } + + function destroyCache() { + for (var id in cache) + for (var eventName in cache[id]) + cache[id][eventName] = null; + } + + if (window.attachEvent) { + window.attachEvent("onunload", destroyCache); + } + + return { + observe: function(element, eventName, handler) { + element = $(element); + var name = getDOMEventName(eventName); + + var wrapper = createWrapper(element, eventName, handler); + if (!wrapper) return element; + + if (element.addEventListener) { + element.addEventListener(name, wrapper, false); + } else { + element.attachEvent("on" + name, wrapper); + } + + return element; + }, + + stopObserving: function(element, eventName, handler) { + element = $(element); + var id = getEventID(element), name = getDOMEventName(eventName); + + if (!handler && eventName) { + getWrappersForEventName(id, eventName).each(function(wrapper) { + element.stopObserving(eventName, wrapper.handler); + }); + return element; + + } else if (!eventName) { + Object.keys(getCacheForID(id)).each(function(eventName) { + element.stopObserving(eventName); + }); + return element; + } + + var wrapper = findWrapper(id, eventName, handler); + if (!wrapper) return element; + + if (element.removeEventListener) { + element.removeEventListener(name, wrapper, false); + } else { + element.detachEvent("on" + name, wrapper); + } + + destroyWrapper(id, eventName, handler); + + return element; + }, + + fire: function(element, eventName, memo) { + element = $(element); + if (element == document && document.createEvent && !element.dispatchEvent) + element = document.documentElement; + + if (document.createEvent) { + var event = document.createEvent("HTMLEvents"); + event.initEvent("dataavailable", true, true); + } else { + var event = document.createEventObject(); + event.eventType = "ondataavailable"; + } + + event.eventName = eventName; + event.memo = memo || { }; + + if (document.createEvent) { + element.dispatchEvent(event); + } else { + element.fireEvent(event.eventType, event); + } + + return Event.extend(event); + } + }; +})()); + +Object.extend(Event, Event.Methods); + +Element.addMethods({ + fire: Event.fire, + observe: Event.observe, + stopObserving: Event.stopObserving +}); + +Object.extend(document, { + fire: Element.Methods.fire.methodize(), + observe: Element.Methods.observe.methodize(), + stopObserving: Element.Methods.stopObserving.methodize() +}); + +(function() { + /* Support for the DOMContentLoaded event is based on work by Dan Webb, + Matthias Miller, Dean Edwards and John Resig. */ + + var timer, fired = false; + + function fireContentLoadedEvent() { + if (fired) return; + if (timer) window.clearInterval(timer); + document.fire("dom:loaded"); + fired = true; + } + + if (document.addEventListener) { + if (Prototype.Browser.WebKit) { + timer = window.setInterval(function() { + if (/loaded|complete/.test(document.readyState)) + fireContentLoadedEvent(); + }, 0); + + Event.observe(window, "load", fireContentLoadedEvent); + + } else { + document.addEventListener("DOMContentLoaded", + fireContentLoadedEvent, false); + } + + } else { + document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>"); + $("__onDOMContentLoaded").onreadystatechange = function() { + if (this.readyState == "complete") { + this.onreadystatechange = null; + fireContentLoadedEvent(); + } + }; + } +})(); +/*------------------------------- DEPRECATED -------------------------------*/ + +Hash.toQueryString = Object.toQueryString; + +var Toggle = { display: Element.toggle }; + +Element.Methods.childOf = Element.Methods.descendantOf; + +var Insertion = { + Before: function(element, content) { + return Element.insert(element, {before:content}); + }, + + Top: function(element, content) { + return Element.insert(element, {top:content}); + }, + + Bottom: function(element, content) { + return Element.insert(element, {bottom:content}); + }, + + After: function(element, content) { + return Element.insert(element, {after:content}); + } +}; + +var $continue = new Error('"throw $continue" is deprecated, use "return" instead'); + +// This should be moved to script.aculo.us; notice the deprecated methods +// further below, that map to the newer Element methods. +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; + }, + + // 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 = Element.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 = Element.cumulativeScrollOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = Element.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; + }, + + // Deprecation layer -- use newer Element methods now (1.5.2). + + cumulativeOffset: Element.Methods.cumulativeOffset, + + positionedOffset: Element.Methods.positionedOffset, + + absolutize: function(element) { + Position.prepare(); + return Element.absolutize(element); + }, + + relativize: function(element) { + Position.prepare(); + return Element.relativize(element); + }, + + realOffset: Element.Methods.cumulativeScrollOffset, + + offsetParent: Element.Methods.getOffsetParent, + + page: Element.Methods.viewportOffset, + + clone: function(source, target, options) { + options = options || { }; + return Element.clonePosition(target, source, options); + } +}; + +/*--------------------------------------------------------------------------*/ + +if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){ + function iter(name) { + return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]"; + } + + instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ? + function(element, className) { + className = className.toString().strip(); + var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className); + return cond ? document._getElementsByXPath('.//*' + cond, element) : []; + } : function(element, className) { + className = className.toString().strip(); + var elements = [], classNames = (/\s/.test(className) ? $w(className) : null); + if (!classNames && !className) return elements; + + var nodes = $(element).getElementsByTagName('*'); + className = ' ' + className + ' '; + + for (var i = 0, child, cn; child = nodes[i]; i++) { + if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) || + (classNames && classNames.all(function(name) { + return !name.toString().blank() && cn.include(' ' + name + ' '); + })))) + elements.push(Element.extend(child)); + } + return elements; + }; + + return function(className, parentElement) { + return $(parentElement || document.body).getElementsByClassName(className); + }; +}(Element.Methods); + +/*--------------------------------------------------------------------------*/ + +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($A(this).concat(classNameToAdd).join(' ')); + }, + + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set($A(this).without(classNameToRemove).join(' ')); + }, + + toString: function() { + return $A(this).join(' '); + } +}; + +Object.extend(Element.ClassNames.prototype, Enumerable); + +/*--------------------------------------------------------------------------*/ + +Element.addMethods(); \ No newline at end of file diff --git a/app/webroot/js/select_list_move.js b/app/webroot/js/select_list_move.js new file mode 100644 index 0000000..1ced882 --- /dev/null +++ b/app/webroot/js/select_list_move.js @@ -0,0 +1,55 @@ +var NS4 = (navigator.appName == "Netscape" && parseInt(navigator.appVersion) < 5); + +function addOption(theSel, theText, theValue) +{ + var newOpt = new Option(theText, theValue); + var selLength = theSel.length; + theSel.options[selLength] = newOpt; +} + +function deleteOption(theSel, theIndex) +{ + var selLength = theSel.length; + if(selLength>0) + { + theSel.options[theIndex] = null; + } +} + +function moveOptions(theSelFrom, theSelTo) +{ + + var selLength = theSelFrom.length; + var selectedText = new Array(); + var selectedValues = new Array(); + var selectedCount = 0; + + var i; + + for(i=selLength-1; i>=0; i--) + { + if(theSelFrom.options[i].selected) + { + selectedText[selectedCount] = theSelFrom.options[i].text; + selectedValues[selectedCount] = theSelFrom.options[i].value; + deleteOption(theSelFrom, i); + selectedCount++; + } + } + + for(i=selectedCount-1; i>=0; i--) + { + addOption(theSelTo, selectedText[i], selectedValues[i]); + } + + if(NS4) history.go(0); +} + +function selectAllOptions(id) +{ + var select = $(id); + for (var i=0; i<select.options.length; i++) { + select.options[i].selected = true; + } +} + diff --git a/app/webroot/js/vendors.php b/app/webroot/js/vendors.php new file mode 100755 index 0000000..a6fcf32 --- /dev/null +++ b/app/webroot/js/vendors.php @@ -0,0 +1,42 @@ +<?php +/* SVN FILE: $Id: vendors.php 7945 2008-12-19 02:16:01Z gwoo $ */ +/** + * Short description for file. + * + * This file includes js vendor-files from /vendor/ directory if they need to + * be accessible to the public. + * + * PHP versions 4 and 5 + * + * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org) + * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project + * @package cake + * @subpackage cake.app.webroot.js + * @since CakePHP(tm) v 0.2.9 + * @version $Revision: 7945 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2008-12-18 18:16:01 -0800 (Thu, 18 Dec 2008) $ + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ +/** + * Enter description here... + */ +if (isset($_GET['file'])) { + $file = $_GET['file']; + $pos = strpos($file, '..'); + if ($pos === false) { + if (is_file('../../vendors/javascript/'.$file) && (preg_match('/(\/.+)\\.js/', $file))) { + readfile('../../vendors/javascript/'.$file); + return; + } + } +} +header('HTTP/1.1 404 Not Found'); +?> \ No newline at end of file diff --git a/app/webroot/test.php b/app/webroot/test.php new file mode 100755 index 0000000..264d780 --- /dev/null +++ b/app/webroot/test.php @@ -0,0 +1,181 @@ +<?php +/* SVN FILE: $Id: test.php 7945 2008-12-19 02:16:01Z gwoo $ */ +/** + * Short description for file. + * + * Long description for file + * + * PHP versions 4 and 5 + * + * CakePHP(tm) Tests <https://trac.cakephp.org/wiki/Developement/TestSuite> + * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * + * Licensed under The Open Group Test Suite License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * @link https://trac.cakephp.org/wiki/Developement/TestSuite CakePHP(tm) Tests + * @package cake + * @subpackage cake.cake.tests.libs + * @since CakePHP(tm) v 1.2.0.4433 + * @version $Revision: 7945 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2008-12-18 18:16:01 -0800 (Thu, 18 Dec 2008) $ + * @license http://www.opensource.org/licenses/opengroup.php The Open Group Test Suite License + */ +error_reporting(E_ALL); +set_time_limit(0); +ini_set('memory_limit','128M'); +ini_set('display_errors', 1); +/** + * Use the DS to separate the directories in other defines + */ + if (!defined('DS')) { + define('DS', DIRECTORY_SEPARATOR); + } +/** + * These defines should only be edited if you have cake installed in + * a directory layout other than the way it is distributed. + * When using custom settings be sure to use the DS and do not add a trailing DS. + */ + +/** + * The full path to the directory which holds "app", WITHOUT a trailing DS. + * + */ + if (!defined('ROOT')) { + define('ROOT', dirname(dirname(dirname(__FILE__)))); + } +/** + * The actual directory name for the "app". + * + */ + if (!defined('APP_DIR')) { + define('APP_DIR', basename(dirname(dirname(__FILE__)))); + } +/** + * The absolute path to the "cake" directory, WITHOUT a trailing DS. + * + */ + if (!defined('CAKE_CORE_INCLUDE_PATH')) { + define('CAKE_CORE_INCLUDE_PATH', ROOT); + } + +/** + * Editing below this line should not be necessary. + * Change at your own risk. + * + */ +if (!defined('WEBROOT_DIR')) { + define('WEBROOT_DIR', basename(dirname(__FILE__))); +} +if (!defined('WWW_ROOT')) { + define('WWW_ROOT', dirname(__FILE__) . DS); +} +if (!defined('CORE_PATH')) { + if (function_exists('ini_set') && ini_set('include_path', CAKE_CORE_INCLUDE_PATH . PATH_SEPARATOR . ROOT . DS . APP_DIR . DS . PATH_SEPARATOR . ini_get('include_path'))) { + define('APP_PATH', null); + define('CORE_PATH', null); + } else { + define('APP_PATH', ROOT . DS . APP_DIR . DS); + define('CORE_PATH', CAKE_CORE_INCLUDE_PATH . DS); + } +} +if (!include(CORE_PATH . 'cake' . DS . 'bootstrap.php')) { + trigger_error("CakePHP core could not be found. Check the value of CAKE_CORE_INCLUDE_PATH in APP/webroot/index.php. It should point to the directory containing your " . DS . "cake core directory and your " . DS . "vendors root directory.", E_USER_ERROR); +} + +$corePath = Configure::corePaths('cake'); +if (isset($corePath[0])) { + define('TEST_CAKE_CORE_INCLUDE_PATH', rtrim($corePath[0], DS) . DS); +} else { + define('TEST_CAKE_CORE_INCLUDE_PATH', CAKE_CORE_INCLUDE_PATH); +} + +require_once CAKE_TESTS_LIB . 'test_manager.php'; + +if (Configure::read('debug') < 1) { + die(__('Debug setting does not allow access to this url.', true)); +} + +if (!isset($_SERVER['SERVER_NAME'])) { + $_SERVER['SERVER_NAME'] = ''; +} +if (empty( $_GET['output'])) { + $_GET['output'] = 'html'; +} +/** + * + * Used to determine output to display + */ +define('CAKE_TEST_OUTPUT_HTML', 1); +define('CAKE_TEST_OUTPUT_TEXT', 2); + +if (isset($_GET['output']) && $_GET['output'] == 'html') { + define('CAKE_TEST_OUTPUT', CAKE_TEST_OUTPUT_HTML); +} else { + Debugger::output('txt'); + define('CAKE_TEST_OUTPUT', CAKE_TEST_OUTPUT_TEXT); +} + +if (!App::import('Vendor', 'simpletest' . DS . 'reporter')) { + CakePHPTestHeader(); + include CAKE_TESTS_LIB . 'simpletest.php'; + CakePHPTestSuiteFooter(); + exit(); +} + +$analyzeCodeCoverage = false; +if (isset($_GET['code_coverage'])) { + $analyzeCodeCoverage = true; + require_once CAKE_TESTS_LIB . 'code_coverage_manager.php'; + if (!extension_loaded('xdebug')) { + CakePHPTestHeader(); + include CAKE_TESTS_LIB . 'xdebug.php'; + CakePHPTestSuiteFooter(); + exit(); + } +} + +CakePHPTestHeader(); +CakePHPTestSuiteHeader(); +define('RUN_TEST_LINK', $_SERVER['PHP_SELF']); + +if (isset($_GET['group'])) { + if ('all' == $_GET['group']) { + TestManager::runAllTests(CakeTestsGetReporter()); + } else { + if ($analyzeCodeCoverage) { + CodeCoverageManager::start($_GET['group'], CakeTestsGetReporter()); + } + TestManager::runGroupTest(ucfirst($_GET['group']), CakeTestsGetReporter()); + if ($analyzeCodeCoverage) { + CodeCoverageManager::report(); + } + } + + CakePHPTestRunMore(); + CakePHPTestAnalyzeCodeCoverage(); +} elseif (isset($_GET['case'])) { + if ($analyzeCodeCoverage) { + CodeCoverageManager::start($_GET['case'], CakeTestsGetReporter()); + } + + TestManager::runTestCase($_GET['case'], CakeTestsGetReporter()); + + if ($analyzeCodeCoverage) { + CodeCoverageManager::report(); + } + + CakePHPTestRunMore(); + CakePHPTestAnalyzeCodeCoverage(); +} elseif (isset($_GET['show']) && $_GET['show'] == 'cases') { + CakePHPTestCaseList(); +} else { + CakePHPTestGroupTestList(); +} +CakePHPTestSuiteFooter(); +$output = ob_get_clean(); +echo $output; +?> \ No newline at end of file diff --git a/index.php b/index.php new file mode 100755 index 0000000..968538c --- /dev/null +++ b/index.php @@ -0,0 +1,62 @@ +<?php +/* SVN FILE: $Id: index.php 7945 2008-12-19 02:16:01Z gwoo $ */ +/** + * Requests collector. + * + * This file collects requests if: + * - no mod_rewrite is avilable or .htaccess files are not supported + * -/public is not set as a web root. + * + * PHP versions 4 and 5 + * + * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org) + * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @filesource + * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org) + * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project + * @package cake + * @since CakePHP(tm) v 0.2.9 + * @version $Revision: 7945 $ + * @modifiedby $LastChangedBy: gwoo $ + * @lastmodified $Date: 2008-12-18 18:16:01 -0800 (Thu, 18 Dec 2008) $ + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ +/** + * Get Cake's root directory + */ + define('APP_DIR', 'app'); + define('DS', DIRECTORY_SEPARATOR); + define('ROOT', dirname(__FILE__)); + define('WEBROOT_DIR', 'webroot'); + define('WWW_ROOT', ROOT . DS . APP_DIR . DS . WEBROOT_DIR . DS); +/** + * This only needs to be changed if the cake installed libs are located + * outside of the distributed directory structure. + */ + if (!defined('CAKE_CORE_INCLUDE_PATH')) { + //define ('CAKE_CORE_INCLUDE_PATH', FULL PATH TO DIRECTORY WHERE CAKE CORE IS INSTALLED DO NOT ADD A TRAILING DIRECTORY SEPARATOR'; + define('CAKE_CORE_INCLUDE_PATH', ROOT); + } + if (function_exists('ini_set')) { + ini_set('include_path', ini_get('include_path') . PATH_SEPARATOR . CAKE_CORE_INCLUDE_PATH . PATH_SEPARATOR . ROOT . DS . APP_DIR . DS); + define('APP_PATH', null); + define('CORE_PATH', null); + } else { + define('APP_PATH', ROOT . DS . APP_DIR . DS); + define('CORE_PATH', CAKE_CORE_INCLUDE_PATH . DS); + } + require CORE_PATH . 'cake' . DS . 'basics.php'; + $TIME_START = getMicrotime(); + require CORE_PATH . 'cake' . DS . 'config' . DS . 'paths.php'; + require LIBS . 'object.php'; + require LIBS . 'inflector.php'; + require LIBS . 'configure.php'; + + $bootstrap = true; + $url = null; + require APP_DIR . DS . WEBROOT_DIR . DS . 'index.php'; +?> \ No newline at end of file diff --git a/vendors/css/empty b/vendors/css/empty new file mode 100755 index 0000000..e69de29 diff --git a/vendors/js/empty b/vendors/js/empty new file mode 100755 index 0000000..e69de29 diff --git a/vendors/shells/tasks/empty b/vendors/shells/tasks/empty new file mode 100755 index 0000000..e69de29 diff --git a/vendors/shells/templates/empty b/vendors/shells/templates/empty new file mode 100755 index 0000000..e69de29