diff --git a/.gitignore b/.gitignore
index f658d81..0a3189b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,5 +4,5 @@
.settings
/config.php
/dbconfig.conf
-/webui/.htaccess
-/webui/tmp/*
+/public/.htaccess
+
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..e456c05
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "externals/sihnon-js-lib"]
+ path = externals/sihnon-js-lib
+ url = ../sihnon-js-lib.git
diff --git a/build/ripping-cluster-worker.conf-gentoo b/build/ripping-cluster-worker.conf-gentoo
new file mode 100644
index 0000000..631bdb7
--- /dev/null
+++ b/build/ripping-cluster-worker.conf-gentoo
@@ -0,0 +1,5 @@
+# Which user to run the worker daemon as
+USER="media"
+
+# File to store the running daemon's PID in
+PID_FILE="/var/run/ripping-cluster-worker.pid"
diff --git a/build/ripping-cluster-worker.init-gentoo b/build/ripping-cluster-worker.init-gentoo
index 0abdb95..559f5fa 100644
--- a/build/ripping-cluster-worker.init-gentoo
+++ b/build/ripping-cluster-worker.init-gentoo
@@ -2,8 +2,6 @@
# Copyright 1999-2010 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
-PID_FILE="/var/run/ripping-cluster-worker.pid"
-
depend() {
need localmount net
use dns logger puppetmaster netmount nfsmount
@@ -13,7 +11,8 @@ start() {
ebegin "Starting ripping-cluster-worker"
start-stop-daemon --start --quiet \
--background --make-pidfile --pidfile ${PID_FILE} \
- --exec /usr/bin/php /usr/lib/ripping-cluster/worker/ripping-cluster-worker.php
+ --chuid ${USER} --user ${USER} \
+ --exec /usr/bin/php /usr/lib/ripping-cluster/source/worker/ripping-cluster-worker.php
eend $? "Failed to start ripping-cluster-worker"
}
@@ -22,8 +21,10 @@ stop() {
start-stop-daemon --stop --quiet \
--pidfile ${PID_FILE}
local ret=$?
- eend ${ret} "Failed to stop puppet"
- rm -f ${PID_FILE}
+
+ eend ${ret} "Failed to stop ripping-cluster-worker"
+ ${ret} || rm -f ${PID_FILE}
+
return ${ret}
}
diff --git a/build/schema/mysql.sql b/build/schema/mysql.sql
new file mode 100644
index 0000000..259c531
--- /dev/null
+++ b/build/schema/mysql.sql
@@ -0,0 +1,208 @@
+-- phpMyAdmin SQL Dump
+-- version 3.3.0
+-- http://www.phpmyadmin.net
+--
+-- Host: localhost:3306
+-- Generation Time: Jan 11, 2012 at 12:27 AM
+-- Server version: 5.1.53
+-- PHP Version: 5.3.6-pl1-gentoo
+
+SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
+
+--
+-- Database: `ripping-cluster`
+--
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `client_log`
+--
+
+DROP TABLE IF EXISTS `client_log`;
+CREATE TABLE IF NOT EXISTS `client_log` (
+ `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `job_id` int(10) unsigned DEFAULT NULL,
+ `level` varchar(32) NOT NULL,
+ `category` varchar(32) NOT NULL,
+ `ctime` int(11) NOT NULL,
+ `pid` int(11) NOT NULL,
+ `hostname` varchar(32) NOT NULL,
+ `progname` varchar(64) NOT NULL,
+ `file` text NOT NULL,
+ `line` int(11) NOT NULL,
+ `message` text NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `job_id` (`job_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `jobs`
+--
+
+DROP TABLE IF EXISTS `jobs`;
+CREATE TABLE IF NOT EXISTS `jobs` (
+ `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) NOT NULL,
+ `source_plugin` varchar(255) NOT NULL COMMENT 'Partial classname of the plugin used to read the source',
+ `rip_plugin` varchar(255) NOT NULL COMMENT 'Partial classname of the plugin used to perform the rip',
+ `source` text NOT NULL,
+ `destination` text NOT NULL,
+ `title` varchar(64) NOT NULL,
+ `format` varchar(4) NOT NULL,
+ `video_codec` varchar(8) NOT NULL,
+ `video_width` int(11) DEFAULT NULL,
+ `video_height` int(11) DEFAULT NULL,
+ `quantizer` float NOT NULL,
+ `deinterlace` double NOT NULL,
+ `audio_tracks` varchar(64) NOT NULL,
+ `audio_codecs` varchar(64) NOT NULL,
+ `audio_names` varchar(255) NOT NULL,
+ `subtitle_tracks` varchar(64) NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `job_status`
+--
+
+DROP TABLE IF EXISTS `job_status`;
+CREATE TABLE IF NOT EXISTS `job_status` (
+ `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `job_id` int(10) unsigned NOT NULL,
+ `status` int(10) unsigned NOT NULL,
+ `ctime` int(10) unsigned NOT NULL,
+ `mtime` int(10) unsigned NOT NULL,
+ `rip_progress` double DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `job_id` (`job_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `job_status_current`
+--
+
+DROP TABLE IF EXISTS `job_status_current`;
+CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `handbrake_cluster`.`job_status_current` AS select `js`.`id` AS `id`,`js`.`job_id` AS `job_id`,`js`.`status` AS `status`,`js`.`ctime` AS `ctime`,`js`.`mtime` AS `mtime`,`js`.`rip_progress` AS `rip_progress` from (`handbrake_cluster`.`job_status` `js` join `handbrake_cluster`.`job_status_current_int` `js2`) where ((`js2`.`job_id` = `js`.`job_id`) and (`js`.`id` = `js2`.`latest`));
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `job_status_current_int`
+--
+
+DROP TABLE IF EXISTS `job_status_current_int`;
+CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `handbrake_cluster`.`job_status_current_int` AS (select `handbrake_cluster`.`job_status`.`job_id` AS `job_id`,max(`handbrake_cluster`.`job_status`.`id`) AS `latest` from `handbrake_cluster`.`job_status` group by `handbrake_cluster`.`job_status`.`job_id`);
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `settings`
+--
+
+DROP TABLE IF EXISTS `settings`;
+CREATE TABLE IF NOT EXISTS `settings` (
+ `name` varchar(255) NOT NULL,
+ `value` text NOT NULL,
+ `type` enum('bool','int','float','string','array(string)','hash') DEFAULT 'string',
+ PRIMARY KEY (`name`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
+--
+-- Dumping data for table `settings`
+--
+
+INSERT INTO `settings` (`name`, `value`, `type`) VALUES
+('debug.display_exceptions', '1', 'bool'),
+('rips.nice', '15', 'int'),
+('cache.base_dir', '/dev/shm/hbc/', 'string'),
+('rips.cache_ttl', '86400', 'int'),
+('rips.job_servers', 'localhost:7003', 'string'),
+('rips.context', 'dev', 'string'),
+('rips.default.output_directory', '', 'string'),
+('rips.handbrake_binary', '/usr/bin/HandBrakeCLI', 'string'),
+('rips.nice_binary', '/usr/bin/nice', 'string'),
+('source.handbrake.dir', '', 'array(string)'),
+('source.mkvinfo.dir', '', 'array(string)'),
+('source.bluray.dir', '', 'array(string)'),
+('logging.plugins', 'Database\nFlatFile\nConsole', 'array(string)'),
+('logging.Console', 'stdout', 'array(string)'),
+('logging.Console.stdout.format', '%ctime% %hostname%:%pid% %progname%:%file%[%line%] %message%', 'string'),
+('logging.Console.stdout.severity', 'debug\ninfo\nwarning\nerror', 'array(string)'),
+('logging.Console.stdout.category', 'client\nworker', 'array(string)'),
+('logging.Database', 'webui\nworker', 'array(string)'),
+('logging.Database.webui.table', 'client_log', 'string'),
+('logging.Database.webui.severity', 'debug\ninfo\nwarning\ndebug', 'array(string)'),
+('logging.Database.webui.category', 'batch\nclient\ndefault', 'array(string)'),
+('logging.Database.worker.table', 'worker_log', 'string'),
+('logging.Database.worker.severity', 'debug\ninfo\nwarning\nerror', 'array(string)'),
+('logging.Database.worker.category', 'worker', 'array(string)'),
+('logging.FlatFile', 'stderr\nvarlog_worker', 'array(string)'),
+('logging.FlatFile.stderr.filename', 'php://stderr', 'string'),
+('logging.FlatFile.stderr.format', '%timestamp% %hostname%:%pid% %progname%:%file%[%line%] %message%', 'string'),
+('logging.FlatFile.stderr.severity', 'warning\nerror', 'array(string)'),
+('logging.FlatFile.stderr.category', 'batch\nclient\ndefault\nworker', 'array(string)'),
+('logging.FlatFile.varlog_worker.filename', '/var/log/ripping-cluster/worker.log', 'string'),
+('logging.FlatFile.varlog_worker.format', '%timestamp% %hostname%:%pid% %progname%:%file%[%line%] %message%', 'string'),
+('logging.FlatFile.varlog_worker.severity', 'debug\ninfo\nwarning\nerror', 'array(string)'),
+('logging.FlatFile.varlog_worker.category', 'worker', 'array(string)'),
+('logging.Syslog', 'local0', 'array(string)'),
+('logging.Syslog.local0.facility', '128', 'int'),
+('logging.Syslog.local0.severity', 'debug\ninfo\nwarning\nerror', 'array(string)'),
+('logging.Syslog.local0.category', 'batch\nclient\ndefault\nworker', 'array(string)'),
+('logging.Syslog.local0.format', '%file%[%line%] %message%', 'string'),
+('templates.tmp_path', '/var/tmp/ripping-cluster', 'string'),
+('rips.temp_dir', '/tmp', 'string'),
+('job.logs.default_display_count', '30', 'int'),
+('job.logs.default_order', 'DESC', 'string'),
+('rips.output_directories.default', '', 'hash'),
+('rips.output_directories.recent', '', 'array(string)'),
+('rips.output_directories.recent_limit', '10', 'int'),
+('auth', 'Config', 'string'),
+('auth.admin.username', 'admin', 'string'),
+('auth.admin_password', '489152af89501a7dc72f6e589123b8c337c01623', 'string');
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `worker_log`
+--
+
+DROP TABLE IF EXISTS `worker_log`;
+CREATE TABLE IF NOT EXISTS `worker_log` (
+ `id` bigint(20) NOT NULL AUTO_INCREMENT,
+ `job_id` int(10) unsigned DEFAULT NULL,
+ `level` varchar(32) NOT NULL,
+ `category` varchar(32) NOT NULL,
+ `ctime` int(11) NOT NULL,
+ `pid` int(11) NOT NULL,
+ `hostname` varchar(32) NOT NULL,
+ `progname` varchar(64) NOT NULL,
+ `file` text NOT NULL,
+ `line` int(11) NOT NULL,
+ `message` text NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `job_id` (`job_id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
+
+--
+-- Constraints for dumped tables
+--
+
+--
+-- Constraints for table `client_log`
+--
+ALTER TABLE `client_log`
+ ADD CONSTRAINT `client_log_ibfk_1` FOREIGN KEY (`job_id`) REFERENCES `jobs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+--
+-- Constraints for table `job_status`
+--
+ALTER TABLE `job_status`
+ ADD CONSTRAINT `job_status_ibfk_1` FOREIGN KEY (`job_id`) REFERENCES `jobs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/externals/sihnon-js-lib b/externals/sihnon-js-lib
new file mode 160000
index 0000000..0499e7e
--- /dev/null
+++ b/externals/sihnon-js-lib
@@ -0,0 +1 @@
+Subproject commit 0499e7ecaa888d72c624ccec2a84c0a55b505d7c
diff --git a/htaccess.dist b/private/htaccess.dist
similarity index 100%
rename from htaccess.dist
rename to private/htaccess.dist
diff --git a/webui/_inc.php b/public/_inc.php
similarity index 100%
rename from webui/_inc.php
rename to public/_inc.php
diff --git a/webui/a.php b/public/a.php
similarity index 100%
rename from webui/a.php
rename to public/a.php
diff --git a/webui/images/caution.png b/public/images/caution.png
similarity index 100%
rename from webui/images/caution.png
rename to public/images/caution.png
diff --git a/webui/images/clock.png b/public/images/clock.png
similarity index 100%
rename from webui/images/clock.png
rename to public/images/clock.png
diff --git a/public/images/jquery.progressbar/progressbar.gif b/public/images/jquery.progressbar/progressbar.gif
new file mode 100644
index 0000000..abe588c
Binary files /dev/null and b/public/images/jquery.progressbar/progressbar.gif differ
diff --git a/public/images/jquery.progressbar/progressbg_black.gif b/public/images/jquery.progressbar/progressbg_black.gif
new file mode 100644
index 0000000..74fd1f9
Binary files /dev/null and b/public/images/jquery.progressbar/progressbg_black.gif differ
diff --git a/public/images/jquery.progressbar/progressbg_green.gif b/public/images/jquery.progressbar/progressbg_green.gif
new file mode 100644
index 0000000..f3f3bf6
Binary files /dev/null and b/public/images/jquery.progressbar/progressbg_green.gif differ
diff --git a/public/images/jquery.progressbar/progressbg_orange.gif b/public/images/jquery.progressbar/progressbg_orange.gif
new file mode 100644
index 0000000..808cac7
Binary files /dev/null and b/public/images/jquery.progressbar/progressbg_orange.gif differ
diff --git a/public/images/jquery.progressbar/progressbg_red.gif b/public/images/jquery.progressbar/progressbg_red.gif
new file mode 100644
index 0000000..54dfa13
Binary files /dev/null and b/public/images/jquery.progressbar/progressbg_red.gif differ
diff --git a/public/images/jquery.progressbar/progressbg_yellow.gif b/public/images/jquery.progressbar/progressbg_yellow.gif
new file mode 100644
index 0000000..fdb0dfc
Binary files /dev/null and b/public/images/jquery.progressbar/progressbg_yellow.gif differ
diff --git a/webui/images/redo.png b/public/images/redo.png
similarity index 100%
rename from webui/images/redo.png
rename to public/images/redo.png
diff --git a/webui/images/trash.png b/public/images/trash.png
similarity index 100%
rename from webui/images/trash.png
rename to public/images/trash.png
diff --git a/webui/index.php b/public/index.php
similarity index 100%
rename from webui/index.php
rename to public/index.php
diff --git a/public/scripts/3rdparty/bootstrap-alerts.js b/public/scripts/3rdparty/bootstrap-alerts.js
new file mode 100644
index 0000000..37bb430
--- /dev/null
+++ b/public/scripts/3rdparty/bootstrap-alerts.js
@@ -0,0 +1,113 @@
+/* ==========================================================
+ * bootstrap-alerts.js v1.4.0
+ * http://twitter.github.com/bootstrap/javascript.html#alerts
+ * ==========================================================
+ * Copyright 2011 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================================================== */
+
+
+!function( $ ){
+
+ "use strict"
+
+ /* CSS TRANSITION SUPPORT (https://gist.github.com/373874)
+ * ======================================================= */
+
+ var transitionEnd
+
+ $(document).ready(function () {
+
+ $.support.transition = (function () {
+ var thisBody = document.body || document.documentElement
+ , thisStyle = thisBody.style
+ , support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined
+ return support
+ })()
+
+ // set CSS transition event type
+ if ( $.support.transition ) {
+ transitionEnd = "TransitionEnd"
+ if ( $.browser.webkit ) {
+ transitionEnd = "webkitTransitionEnd"
+ } else if ( $.browser.mozilla ) {
+ transitionEnd = "transitionend"
+ } else if ( $.browser.opera ) {
+ transitionEnd = "oTransitionEnd"
+ }
+ }
+
+ })
+
+ /* ALERT CLASS DEFINITION
+ * ====================== */
+
+ var Alert = function ( content, options ) {
+ this.settings = $.extend({}, $.fn.alert.defaults, options)
+ this.$element = $(content)
+ .delegate(this.settings.selector, 'click', this.close)
+ }
+
+ Alert.prototype = {
+
+ close: function (e) {
+ var $element = $(this).parent('.alert-message')
+
+ e && e.preventDefault()
+ $element.removeClass('in')
+
+ function removeElement () {
+ $element.remove()
+ }
+
+ $.support.transition && $element.hasClass('fade') ?
+ $element.bind(transitionEnd, removeElement) :
+ removeElement()
+ }
+
+ }
+
+
+ /* ALERT PLUGIN DEFINITION
+ * ======================= */
+
+ $.fn.alert = function ( options ) {
+
+ if ( options === true ) {
+ return this.data('alert')
+ }
+
+ return this.each(function () {
+ var $this = $(this)
+
+ if ( typeof options == 'string' ) {
+ return $this.data('alert')[options]()
+ }
+
+ $(this).data('alert', new Alert( this, options ))
+
+ })
+ }
+
+ $.fn.alert.defaults = {
+ selector: '.close'
+ }
+
+ $(document).ready(function () {
+ new Alert($('body'), {
+ selector: '.alert-message[data-alert] .close'
+ })
+ })
+
+}( window.jQuery || window.ender );
\ No newline at end of file
diff --git a/public/scripts/3rdparty/bootstrap-dropdown.js b/public/scripts/3rdparty/bootstrap-dropdown.js
new file mode 100644
index 0000000..cab0ec2
--- /dev/null
+++ b/public/scripts/3rdparty/bootstrap-dropdown.js
@@ -0,0 +1,55 @@
+/* ============================================================
+ * bootstrap-dropdown.js v1.4.0
+ * http://twitter.github.com/bootstrap/javascript.html#dropdown
+ * ============================================================
+ * Copyright 2011 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============================================================ */
+
+
+!function( $ ){
+
+ "use strict"
+
+ /* DROPDOWN PLUGIN DEFINITION
+ * ========================== */
+
+ $.fn.dropdown = function ( selector ) {
+ return this.each(function () {
+ $(this).delegate(selector || d, 'click', function (e) {
+ var li = $(this).parent('li')
+ , isActive = li.hasClass('open')
+
+ clearMenus()
+ !isActive && li.toggleClass('open')
+ return false
+ })
+ })
+ }
+
+ /* APPLY TO STANDARD DROPDOWN ELEMENTS
+ * =================================== */
+
+ var d = 'a.menu, .dropdown-toggle'
+
+ function clearMenus() {
+ $(d).parent('li').removeClass('open')
+ }
+
+ $(function () {
+ $('html').bind("click", clearMenus)
+ $('body').dropdown( '[data-dropdown] a.menu, [data-dropdown] .dropdown-toggle' )
+ })
+
+}( window.jQuery || window.ender );
\ No newline at end of file
diff --git a/public/scripts/3rdparty/bootstrap-modal.js b/public/scripts/3rdparty/bootstrap-modal.js
new file mode 100644
index 0000000..be2315a
--- /dev/null
+++ b/public/scripts/3rdparty/bootstrap-modal.js
@@ -0,0 +1,260 @@
+/* =========================================================
+ * bootstrap-modal.js v1.4.0
+ * http://twitter.github.com/bootstrap/javascript.html#modal
+ * =========================================================
+ * Copyright 2011 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================================================= */
+
+
+!function( $ ){
+
+ "use strict"
+
+ /* CSS TRANSITION SUPPORT (https://gist.github.com/373874)
+ * ======================================================= */
+
+ var transitionEnd
+
+ $(document).ready(function () {
+
+ $.support.transition = (function () {
+ var thisBody = document.body || document.documentElement
+ , thisStyle = thisBody.style
+ , support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined
+ return support
+ })()
+
+ // set CSS transition event type
+ if ( $.support.transition ) {
+ transitionEnd = "TransitionEnd"
+ if ( $.browser.webkit ) {
+ transitionEnd = "webkitTransitionEnd"
+ } else if ( $.browser.mozilla ) {
+ transitionEnd = "transitionend"
+ } else if ( $.browser.opera ) {
+ transitionEnd = "oTransitionEnd"
+ }
+ }
+
+ })
+
+
+ /* MODAL PUBLIC CLASS DEFINITION
+ * ============================= */
+
+ var Modal = function ( content, options ) {
+ this.settings = $.extend({}, $.fn.modal.defaults, options)
+ this.$element = $(content)
+ .delegate('.close', 'click.modal', $.proxy(this.hide, this))
+
+ if ( this.settings.show ) {
+ this.show()
+ }
+
+ return this
+ }
+
+ Modal.prototype = {
+
+ toggle: function () {
+ return this[!this.isShown ? 'show' : 'hide']()
+ }
+
+ , show: function () {
+ var that = this
+ this.isShown = true
+ this.$element.trigger('show')
+
+ escape.call(this)
+ backdrop.call(this, function () {
+ var transition = $.support.transition && that.$element.hasClass('fade')
+
+ that.$element
+ .appendTo(document.body)
+ .show()
+
+ if (transition) {
+ that.$element[0].offsetWidth // force reflow
+ }
+
+ that.$element.addClass('in')
+
+ transition ?
+ that.$element.one(transitionEnd, function () { that.$element.trigger('shown') }) :
+ that.$element.trigger('shown')
+
+ })
+
+ return this
+ }
+
+ , hide: function (e) {
+ e && e.preventDefault()
+
+ if ( !this.isShown ) {
+ return this
+ }
+
+ var that = this
+ this.isShown = false
+
+ escape.call(this)
+
+ this.$element
+ .trigger('hide')
+ .removeClass('in')
+
+ $.support.transition && this.$element.hasClass('fade') ?
+ hideWithTransition.call(this) :
+ hideModal.call(this)
+
+ return this
+ }
+
+ }
+
+
+ /* MODAL PRIVATE METHODS
+ * ===================== */
+
+ function hideWithTransition() {
+ // firefox drops transitionEnd events :{o
+ var that = this
+ , timeout = setTimeout(function () {
+ that.$element.unbind(transitionEnd)
+ hideModal.call(that)
+ }, 500)
+
+ this.$element.one(transitionEnd, function () {
+ clearTimeout(timeout)
+ hideModal.call(that)
+ })
+ }
+
+ function hideModal (that) {
+ this.$element
+ .hide()
+ .trigger('hidden')
+
+ backdrop.call(this)
+ }
+
+ function backdrop ( callback ) {
+ var that = this
+ , animate = this.$element.hasClass('fade') ? 'fade' : ''
+ if ( this.isShown && this.settings.backdrop ) {
+ var doAnimate = $.support.transition && animate
+
+ this.$backdrop = $('
')
+ .appendTo(document.body)
+
+ if ( this.settings.backdrop != 'static' ) {
+ this.$backdrop.click($.proxy(this.hide, this))
+ }
+
+ if ( doAnimate ) {
+ this.$backdrop[0].offsetWidth // force reflow
+ }
+
+ this.$backdrop.addClass('in')
+
+ doAnimate ?
+ this.$backdrop.one(transitionEnd, callback) :
+ callback()
+
+ } else if ( !this.isShown && this.$backdrop ) {
+ this.$backdrop.removeClass('in')
+
+ $.support.transition && this.$element.hasClass('fade')?
+ this.$backdrop.one(transitionEnd, $.proxy(removeBackdrop, this)) :
+ removeBackdrop.call(this)
+
+ } else if ( callback ) {
+ callback()
+ }
+ }
+
+ function removeBackdrop() {
+ this.$backdrop.remove()
+ this.$backdrop = null
+ }
+
+ function escape() {
+ var that = this
+ if ( this.isShown && this.settings.keyboard ) {
+ $(document).bind('keyup.modal', function ( e ) {
+ if ( e.which == 27 ) {
+ that.hide()
+ }
+ })
+ } else if ( !this.isShown ) {
+ $(document).unbind('keyup.modal')
+ }
+ }
+
+
+ /* MODAL PLUGIN DEFINITION
+ * ======================= */
+
+ $.fn.modal = function ( options ) {
+ var modal = this.data('modal')
+
+ if (!modal) {
+
+ if (typeof options == 'string') {
+ options = {
+ show: /show|toggle/.test(options)
+ }
+ }
+
+ return this.each(function () {
+ $(this).data('modal', new Modal(this, options))
+ })
+ }
+
+ if ( options === true ) {
+ return modal
+ }
+
+ if ( typeof options == 'string' ) {
+ modal[options]()
+ } else if ( modal ) {
+ modal.toggle()
+ }
+
+ return this
+ }
+
+ $.fn.modal.Modal = Modal
+
+ $.fn.modal.defaults = {
+ backdrop: false
+ , keyboard: false
+ , show: false
+ }
+
+
+ /* MODAL DATA- IMPLEMENTATION
+ * ========================== */
+
+ $(document).ready(function () {
+ $('body').delegate('[data-controls-modal]', 'click', function (e) {
+ e.preventDefault()
+ var $this = $(this).data('show', true)
+ $('#' + $this.attr('data-controls-modal')).modal( $this.data() )
+ })
+ })
+
+}( window.jQuery || window.ender );
\ No newline at end of file
diff --git a/public/scripts/3rdparty/bootstrap-popover.js b/public/scripts/3rdparty/bootstrap-popover.js
new file mode 100644
index 0000000..c637784
--- /dev/null
+++ b/public/scripts/3rdparty/bootstrap-popover.js
@@ -0,0 +1,90 @@
+/* ===========================================================
+ * bootstrap-popover.js v1.4.0
+ * http://twitter.github.com/bootstrap/javascript.html#popover
+ * ===========================================================
+ * Copyright 2011 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================================================== */
+
+
+!function( $ ) {
+
+ "use strict"
+
+ var Popover = function ( element, options ) {
+ this.$element = $(element)
+ this.options = options
+ this.enabled = true
+ this.fixTitle()
+ }
+
+ /* NOTE: POPOVER EXTENDS BOOTSTRAP-TWIPSY.js
+ ========================================= */
+
+ Popover.prototype = $.extend({}, $.fn.twipsy.Twipsy.prototype, {
+
+ setContent: function () {
+ var $tip = this.tip()
+ $tip.find('.title')[this.options.html ? 'html' : 'text'](this.getTitle())
+ $tip.find('.content p')[this.options.html ? 'html' : 'text'](this.getContent())
+ $tip[0].className = 'popover'
+ }
+
+ , hasContent: function () {
+ return this.getTitle() || this.getContent()
+ }
+
+ , getContent: function () {
+ var content
+ , $e = this.$element
+ , o = this.options
+
+ if (typeof this.options.content == 'string') {
+ content = $e.attr(this.options.content)
+ } else if (typeof this.options.content == 'function') {
+ content = this.options.content.call(this.$element[0])
+ }
+
+ return content
+ }
+
+ , tip: function() {
+ if (!this.$tip) {
+ this.$tip = $('')
+ .html(this.options.template)
+ }
+ return this.$tip
+ }
+
+ })
+
+
+ /* POPOVER PLUGIN DEFINITION
+ * ======================= */
+
+ $.fn.popover = function (options) {
+ if (typeof options == 'object') options = $.extend({}, $.fn.popover.defaults, options)
+ $.fn.twipsy.initWith.call(this, options, Popover, 'popover')
+ return this
+ }
+
+ $.fn.popover.defaults = $.extend({} , $.fn.twipsy.defaults, {
+ placement: 'right'
+ , content: 'data-content'
+ , template: ''
+ })
+
+ $.fn.twipsy.rejectAttrOptions.push( 'content' )
+
+}( window.jQuery || window.ender );
\ No newline at end of file
diff --git a/public/scripts/3rdparty/bootstrap-tabs.js b/public/scripts/3rdparty/bootstrap-tabs.js
new file mode 100644
index 0000000..a3c7ee1
--- /dev/null
+++ b/public/scripts/3rdparty/bootstrap-tabs.js
@@ -0,0 +1,80 @@
+/* ========================================================
+ * bootstrap-tabs.js v1.4.0
+ * http://twitter.github.com/bootstrap/javascript.html#tabs
+ * ========================================================
+ * Copyright 2011 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================== */
+
+
+!function( $ ){
+
+ "use strict"
+
+ function activate ( element, container ) {
+ container
+ .find('> .active')
+ .removeClass('active')
+ .find('> .dropdown-menu > .active')
+ .removeClass('active')
+
+ element.addClass('active')
+
+ if ( element.parent('.dropdown-menu') ) {
+ element.closest('li.dropdown').addClass('active')
+ }
+ }
+
+ function tab( e ) {
+ var $this = $(this)
+ , $ul = $this.closest('ul:not(.dropdown-menu)')
+ , href = $this.attr('href')
+ , previous
+ , $href
+
+ if ( /^#\w+/.test(href) ) {
+ e.preventDefault()
+
+ if ( $this.parent('li').hasClass('active') ) {
+ return
+ }
+
+ previous = $ul.find('.active a').last()[0]
+ $href = $(href)
+
+ activate($this.parent('li'), $ul)
+ activate($href, $href.parent())
+
+ $this.trigger({
+ type: 'change'
+ , relatedTarget: previous
+ })
+ }
+ }
+
+
+ /* TABS/PILLS PLUGIN DEFINITION
+ * ============================ */
+
+ $.fn.tabs = $.fn.pills = function ( selector ) {
+ return this.each(function () {
+ $(this).delegate(selector || '.tabs li > a, .pills > li > a', 'click', tab)
+ })
+ }
+
+ $(document).ready(function () {
+ $('body').tabs('ul[data-tabs] li > a, ul[data-pills] > li > a')
+ })
+
+}( window.jQuery || window.ender );
diff --git a/public/scripts/3rdparty/bootstrap-twipsy.js b/public/scripts/3rdparty/bootstrap-twipsy.js
new file mode 100644
index 0000000..5ebbddd
--- /dev/null
+++ b/public/scripts/3rdparty/bootstrap-twipsy.js
@@ -0,0 +1,321 @@
+/* ==========================================================
+ * bootstrap-twipsy.js v1.4.0
+ * http://twitter.github.com/bootstrap/javascript.html#twipsy
+ * Adapted from the original jQuery.tipsy by Jason Frame
+ * ==========================================================
+ * Copyright 2011 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================================================== */
+
+
+!function( $ ) {
+
+ "use strict"
+
+ /* CSS TRANSITION SUPPORT (https://gist.github.com/373874)
+ * ======================================================= */
+
+ var transitionEnd
+
+ $(document).ready(function () {
+
+ $.support.transition = (function () {
+ var thisBody = document.body || document.documentElement
+ , thisStyle = thisBody.style
+ , support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined
+ return support
+ })()
+
+ // set CSS transition event type
+ if ( $.support.transition ) {
+ transitionEnd = "TransitionEnd"
+ if ( $.browser.webkit ) {
+ transitionEnd = "webkitTransitionEnd"
+ } else if ( $.browser.mozilla ) {
+ transitionEnd = "transitionend"
+ } else if ( $.browser.opera ) {
+ transitionEnd = "oTransitionEnd"
+ }
+ }
+
+ })
+
+
+ /* TWIPSY PUBLIC CLASS DEFINITION
+ * ============================== */
+
+ var Twipsy = function ( element, options ) {
+ this.$element = $(element)
+ this.options = options
+ this.enabled = true
+ this.fixTitle()
+ }
+
+ Twipsy.prototype = {
+
+ show: function() {
+ var pos
+ , actualWidth
+ , actualHeight
+ , placement
+ , $tip
+ , tp
+
+ if (this.hasContent() && this.enabled) {
+ $tip = this.tip()
+ this.setContent()
+
+ if (this.options.animate) {
+ $tip.addClass('fade')
+ }
+
+ $tip
+ .remove()
+ .css({ top: 0, left: 0, display: 'block' })
+ .prependTo(document.body)
+
+ pos = $.extend({}, this.$element.offset(), {
+ width: this.$element[0].offsetWidth
+ , height: this.$element[0].offsetHeight
+ })
+
+ actualWidth = $tip[0].offsetWidth
+ actualHeight = $tip[0].offsetHeight
+
+ placement = maybeCall(this.options.placement, this, [ $tip[0], this.$element[0] ])
+
+ switch (placement) {
+ case 'below':
+ tp = {top: pos.top + pos.height + this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2}
+ break
+ case 'above':
+ tp = {top: pos.top - actualHeight - this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2}
+ break
+ case 'left':
+ tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth - this.options.offset}
+ break
+ case 'right':
+ tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width + this.options.offset}
+ break
+ }
+
+ $tip
+ .css(tp)
+ .addClass(placement)
+ .addClass('in')
+ }
+ }
+
+ , setContent: function () {
+ var $tip = this.tip()
+ $tip.find('.twipsy-inner')[this.options.html ? 'html' : 'text'](this.getTitle())
+ $tip[0].className = 'twipsy'
+ }
+
+ , hide: function() {
+ var that = this
+ , $tip = this.tip()
+
+ $tip.removeClass('in')
+
+ function removeElement () {
+ $tip.remove()
+ }
+
+ $.support.transition && this.$tip.hasClass('fade') ?
+ $tip.bind(transitionEnd, removeElement) :
+ removeElement()
+ }
+
+ , fixTitle: function() {
+ var $e = this.$element
+ if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
+ $e.attr('data-original-title', $e.attr('title') || '').removeAttr('title')
+ }
+ }
+
+ , hasContent: function () {
+ return this.getTitle()
+ }
+
+ , getTitle: function() {
+ var title
+ , $e = this.$element
+ , o = this.options
+
+ this.fixTitle()
+
+ if (typeof o.title == 'string') {
+ title = $e.attr(o.title == 'title' ? 'data-original-title' : o.title)
+ } else if (typeof o.title == 'function') {
+ title = o.title.call($e[0])
+ }
+
+ title = ('' + title).replace(/(^\s*|\s*$)/, "")
+
+ return title || o.fallback
+ }
+
+ , tip: function() {
+ return this.$tip = this.$tip || $('').html(this.options.template)
+ }
+
+ , validate: function() {
+ if (!this.$element[0].parentNode) {
+ this.hide()
+ this.$element = null
+ this.options = null
+ }
+ }
+
+ , enable: function() {
+ this.enabled = true
+ }
+
+ , disable: function() {
+ this.enabled = false
+ }
+
+ , toggleEnabled: function() {
+ this.enabled = !this.enabled
+ }
+
+ , toggle: function () {
+ this[this.tip().hasClass('in') ? 'hide' : 'show']()
+ }
+
+ }
+
+
+ /* TWIPSY PRIVATE METHODS
+ * ====================== */
+
+ function maybeCall ( thing, ctx, args ) {
+ return typeof thing == 'function' ? thing.apply(ctx, args) : thing
+ }
+
+ /* TWIPSY PLUGIN DEFINITION
+ * ======================== */
+
+ $.fn.twipsy = function (options) {
+ $.fn.twipsy.initWith.call(this, options, Twipsy, 'twipsy')
+ return this
+ }
+
+ $.fn.twipsy.initWith = function (options, Constructor, name) {
+ var twipsy
+ , binder
+ , eventIn
+ , eventOut
+
+ if (options === true) {
+ return this.data(name)
+ } else if (typeof options == 'string') {
+ twipsy = this.data(name)
+ if (twipsy) {
+ twipsy[options]()
+ }
+ return this
+ }
+
+ options = $.extend({}, $.fn[name].defaults, options)
+
+ function get(ele) {
+ var twipsy = $.data(ele, name)
+
+ if (!twipsy) {
+ twipsy = new Constructor(ele, $.fn.twipsy.elementOptions(ele, options))
+ $.data(ele, name, twipsy)
+ }
+
+ return twipsy
+ }
+
+ function enter() {
+ var twipsy = get(this)
+ twipsy.hoverState = 'in'
+
+ if (options.delayIn == 0) {
+ twipsy.show()
+ } else {
+ twipsy.fixTitle()
+ setTimeout(function() {
+ if (twipsy.hoverState == 'in') {
+ twipsy.show()
+ }
+ }, options.delayIn)
+ }
+ }
+
+ function leave() {
+ var twipsy = get(this)
+ twipsy.hoverState = 'out'
+ if (options.delayOut == 0) {
+ twipsy.hide()
+ } else {
+ setTimeout(function() {
+ if (twipsy.hoverState == 'out') {
+ twipsy.hide()
+ }
+ }, options.delayOut)
+ }
+ }
+
+ if (!options.live) {
+ this.each(function() {
+ get(this)
+ })
+ }
+
+ if (options.trigger != 'manual') {
+ binder = options.live ? 'live' : 'bind'
+ eventIn = options.trigger == 'hover' ? 'mouseenter' : 'focus'
+ eventOut = options.trigger == 'hover' ? 'mouseleave' : 'blur'
+ this[binder](eventIn, enter)[binder](eventOut, leave)
+ }
+
+ return this
+ }
+
+ $.fn.twipsy.Twipsy = Twipsy
+
+ $.fn.twipsy.defaults = {
+ animate: true
+ , delayIn: 0
+ , delayOut: 0
+ , fallback: ''
+ , placement: 'above'
+ , html: false
+ , live: false
+ , offset: 0
+ , title: 'title'
+ , trigger: 'hover'
+ , template: ''
+ }
+
+ $.fn.twipsy.rejectAttrOptions = [ 'title' ]
+
+ $.fn.twipsy.elementOptions = function(ele, options) {
+ var data = $(ele).data()
+ , rejects = $.fn.twipsy.rejectAttrOptions
+ , i = rejects.length
+
+ while (i--) {
+ delete data[rejects[i]]
+ }
+
+ return $.extend({}, options, data)
+ }
+
+}( window.jQuery || window.ender );
\ No newline at end of file
diff --git a/public/scripts/3rdparty/jquery.asmselect.js b/public/scripts/3rdparty/jquery.asmselect.js
new file mode 100644
index 0000000..cc1571f
--- /dev/null
+++ b/public/scripts/3rdparty/jquery.asmselect.js
@@ -0,0 +1,407 @@
+/*
+ * Alternate Select Multiple (asmSelect) 1.0.4 beta - jQuery Plugin
+ * http://www.ryancramer.com/projects/asmselect/
+ *
+ * Copyright (c) 2008 by Ryan Cramer - http://www.ryancramer.com
+ *
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ */
+
+(function($) {
+
+ $.fn.asmSelect = function(customOptions) {
+
+ var options = {
+
+ listType: 'ol', // Ordered list 'ol', or unordered list 'ul'
+ sortable: false, // Should the list be sortable?
+ highlight: false, // Use the highlight feature?
+ animate: false, // Animate the the adding/removing of items in the list?
+ addItemTarget: 'bottom', // Where to place new selected items in list: top or bottom
+ hideWhenAdded: false, // Hide the option when added to the list? works only in FF
+ debugMode: false, // Debug mode keeps original select visible
+
+ removeLabel: 'remove', // Text used in the "remove" link
+ highlightAddedLabel: 'Added: ', // Text that precedes highlight of added item
+ highlightRemovedLabel: 'Removed: ', // Text that precedes highlight of removed item
+
+ containerClass: 'asmContainer', // Class for container that wraps this widget
+ selectClass: 'asmSelect', // Class for the newly created