From 050fdda848c766b17b3ee158069be6e746a63765 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 4 Dec 2014 10:23:10 +0000 Subject: [PATCH 001/329] Initial commit --- PHPCI/Helper/Lang.php | 64 +++++++++++++++++++++++++++++++++++++ PHPCI/Languages/lang.en.php | 14 ++++++++ PHPCI/View/layout.phtml | 11 +++++-- bootstrap.php | 2 ++ public/assets/js/sprintf.js | 4 +++ 5 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 PHPCI/Helper/Lang.php create mode 100644 PHPCI/Languages/lang.en.php create mode 100644 public/assets/js/sprintf.js diff --git a/PHPCI/Helper/Lang.php b/PHPCI/Helper/Lang.php new file mode 100644 index 00000000..44017ae6 --- /dev/null +++ b/PHPCI/Helper/Lang.php @@ -0,0 +1,64 @@ +get('phpci.default_language', 'en'); + self::$strings = self::loadLanguage(); + + if (is_null(self::$strings)) { + self::$language = 'en'; + self::$strings = self::loadLanguage(); + } + } + + protected static function loadLanguage() + { + $langFile = PHPCI_DIR . 'PHPCI/Languages/lang.' . self::$language . '.php'; + + if (!file_exists($langFile)) { + return null; + } + + require_once($langFile); + + if (is_null($strings) || !is_array($strings) || !count($strings)) { + return null; + } + + return $strings; + } +} \ No newline at end of file diff --git a/PHPCI/Languages/lang.en.php b/PHPCI/Languages/lang.en.php new file mode 100644 index 00000000..4dc96d09 --- /dev/null +++ b/PHPCI/Languages/lang.en.php @@ -0,0 +1,14 @@ + 'Toggle Navigation', + 'n_builds_pending' => '%d builds pending', + 'n_builds_running' => '%d builds running', +); \ No newline at end of file diff --git a/PHPCI/View/layout.phtml b/PHPCI/View/layout.phtml index dac44274..dfcea870 100644 --- a/PHPCI/View/layout.phtml +++ b/PHPCI/View/layout.phtml @@ -1,3 +1,6 @@ + @@ -18,6 +21,7 @@ + @@ -26,7 +30,10 @@ - + diff --git a/public/assets/css/AdminLTE-custom.css b/public/assets/css/AdminLTE-custom.css new file mode 100644 index 00000000..009a360b --- /dev/null +++ b/public/assets/css/AdminLTE-custom.css @@ -0,0 +1,53 @@ +.skin-blue .logo, .skin-blue .logo:hover { + background-image: url('/assets/img/logo-large.png'); + background-repeat: no-repeat; + background-size: 40%; + background-position: 65px; + text-indent: -5000px; +} + +.build-info-panel { + +} + + .build-info-panel .box-header h1.box-title { + border: 0; + font-size: 1.5em; + font-weight: bold; + margin-left: 110px; + } + + .build-info-panel h1.box-title span { + font-weight: normal; + } + + .build-info-panel img { + border: 2px solid #fff; + border-radius: 50%; + margin-top: -40px; + } + + .build-info-panel #build-info { + margin-left: 110px; + min-height: 50px; + } + + .build-info-panel .commit-message { + margin-bottom: 20px; + } + +.small-box h3 a, .small-box h4 a { + color: #fff; +} + +.pagination>li>span { + font-weight: bold; + background: #337ab7; + color: #fff; +} + +#plugins table td { + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/public/assets/js/build-plugins/phpcpd.js b/public/assets/js/build-plugins/phpcpd.js index c9733678..2a0f3e16 100644 --- a/public/assets/js/build-plugins/phpcpd.js +++ b/public/assets/js/build-plugins/phpcpd.js @@ -23,14 +23,14 @@ var phpcpdPlugin = ActiveBuild.UiPlugin.extend({ render: function() { - return $('' + + return $('
' + '' + '' + ' ' + ' ' + ' ' + '' + - '
'+Lang.get('file')+''+Lang.get('start')+''+Lang.get('end')+'
'); + ''); }, diff --git a/public/assets/js/build-plugins/phpcs.js b/public/assets/js/build-plugins/phpcs.js index f3b1fe67..3e71ba72 100644 --- a/public/assets/js/build-plugins/phpcs.js +++ b/public/assets/js/build-plugins/phpcs.js @@ -22,14 +22,14 @@ var phpcsPlugin = ActiveBuild.UiPlugin.extend({ }, render: function() { - return $('' + + return $('
' + '' + '' + ' ' + ' ' + ' ' + '' + - '
'+Lang.get('file')+''+Lang.get('line')+''+Lang.get('message')+'
'); + ''); }, onUpdate: function(e) { diff --git a/public/assets/js/build-plugins/phpdoccheck.js b/public/assets/js/build-plugins/phpdoccheck.js index 7d284617..04a6a0ad 100644 --- a/public/assets/js/build-plugins/phpdoccheck.js +++ b/public/assets/js/build-plugins/phpdoccheck.js @@ -24,7 +24,7 @@ var phpdoccheckPlugin = ActiveBuild.UiPlugin.extend({ }, render: function() { - return $('' + + return $('
' + '' + '' + ' ' + @@ -32,7 +32,7 @@ var phpdoccheckPlugin = ActiveBuild.UiPlugin.extend({ ' ' + ' ' + '' + - '
'+Lang.get('file')+''+Lang.get('class')+''+Lang.get('method')+'
'); + ''); }, onUpdate: function(e) { diff --git a/public/assets/js/build-plugins/phpmd.js b/public/assets/js/build-plugins/phpmd.js index 57723ba9..d6d5f867 100644 --- a/public/assets/js/build-plugins/phpmd.js +++ b/public/assets/js/build-plugins/phpmd.js @@ -25,7 +25,7 @@ var phpmdPlugin = ActiveBuild.UiPlugin.extend({ render: function() { - return $('' + + return $('
' + '' + '' + ' ' + @@ -33,7 +33,7 @@ var phpmdPlugin = ActiveBuild.UiPlugin.extend({ ' ' + ' ' + '' + - '
'+Lang.get('file')+''+Lang.get('end')+''+Lang.get('message')+'
'); + ''); }, onUpdate: function(e) { diff --git a/public/assets/js/build-plugins/phptallint.js b/public/assets/js/build-plugins/phptallint.js index 9d981c53..fe549b99 100644 --- a/public/assets/js/build-plugins/phptallint.js +++ b/public/assets/js/build-plugins/phptallint.js @@ -22,14 +22,14 @@ var phptalPlugin = ActiveBuild.UiPlugin.extend({ }, render: function() { - return $('' + + return $('
' + '' + '' + ' ' + ' ' + ' ' + '' + - '
FileLineMessage
'); + ''); }, onUpdate: function(e) { diff --git a/public/assets/js/build-plugins/phpunit.js b/public/assets/js/build-plugins/phpunit.js index 0e648e9d..4a55d905 100644 --- a/public/assets/js/build-plugins/phpunit.js +++ b/public/assets/js/build-plugins/phpunit.js @@ -25,12 +25,12 @@ var phpunitPlugin = ActiveBuild.UiPlugin.extend({ render: function() { - return $('' + + return $('
' + '' + '' + ' ' + '' + - '
'+Lang.get('test')+'
'); + ''); }, onUpdate: function(e) { From 8d2c7045a4252ab797cc6b49d55ec3298fec1538 Mon Sep 17 00:00:00 2001 From: corpsee Date: Sun, 11 Jan 2015 13:35:38 +0600 Subject: [PATCH 065/329] Update the "Manual Build" string for the Russian translation. Closes #737 --- PHPCI/Languages/lang.ru.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/Languages/lang.ru.php b/PHPCI/Languages/lang.ru.php index 4abc9721..dfef84d9 100644 --- a/PHPCI/Languages/lang.ru.php +++ b/PHPCI/Languages/lang.ru.php @@ -84,7 +84,7 @@ PHPCI', 'success' => 'Успешно', 'successful' => 'Успешна', 'failed' => 'Провалена', - 'manual_build' => 'Ручной сборки', + 'manual_build' => 'Запущена вручную', // Add/Edit Project: 'new_project' => 'Новый проект', From 5dced5f990aebf47b3157e6efc7aa274c4fb4cbc Mon Sep 17 00:00:00 2001 From: corpsee Date: Sun, 11 Jan 2015 16:44:34 +0600 Subject: [PATCH 066/329] Fixing fatal error with $this->controller->layout in Application class. Closes #736 --- PHPCI/Application.php | 7 ++++++- PHPCI/Controller.php | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/PHPCI/Application.php b/PHPCI/Application.php index 88380c87..a8790804 100644 --- a/PHPCI/Application.php +++ b/PHPCI/Application.php @@ -22,6 +22,11 @@ use PHPCI\Model\Build; */ class Application extends b8\Application { + /** + * @var \PHPCI\Controller + */ + protected $controller; + /** * Initialise PHPCI - Handles session verification, routing, etc. */ @@ -100,7 +105,7 @@ class Application extends b8\Application $this->response->setContent($view->render()); } - if ($this->response->hasLayout()) { + if ($this->response->hasLayout() && $this->controller->layout) { $this->setLayoutVariables($this->controller->layout); $this->controller->layout->content = $this->response->getContent(); diff --git a/PHPCI/Controller.php b/PHPCI/Controller.php index b065e857..5fd22594 100644 --- a/PHPCI/Controller.php +++ b/PHPCI/Controller.php @@ -30,6 +30,11 @@ class Controller extends \b8\Controller */ protected $view; + /** + * @var \b8\View + */ + public $layout; + /** * Initialise the controller. */ From cd4ff6c4ea94be4b127dbfb4a1c9988715e2558d Mon Sep 17 00:00:00 2001 From: Adirelle Date: Fri, 9 Jan 2015 08:51:35 +0100 Subject: [PATCH 067/329] Localisation support for dates throughout the front-end using moment.js. Closes #734 Closes #732 --- PHPCI/Helper/Lang.php | 29 + PHPCI/View/Home/index.phtml | 4 +- PHPCI/View/SummaryTable.phtml | 8 +- PHPCI/View/layout.phtml | 1 + Tests/PHPCI/Helper/LangTest.php | 28 + .../daterangepicker/daterangepicker-bs3.css | 85 +- public/assets/js/phpci.js | 9 + .../daterangepicker/daterangepicker.js | 1061 +++++++++++------ 8 files changed, 869 insertions(+), 356 deletions(-) create mode 100644 Tests/PHPCI/Helper/LangTest.php diff --git a/PHPCI/Helper/Lang.php b/PHPCI/Helper/Lang.php index 1877426c..c86de1aa 100644 --- a/PHPCI/Helper/Lang.php +++ b/PHPCI/Helper/Lang.php @@ -13,6 +13,7 @@ use b8\Config; /** * Languages Helper Class - Handles loading strings files and the strings within them. + * * @package PHPCI\Helper */ class Lang @@ -23,6 +24,7 @@ class Lang /** * Get a specific string from the language file. + * * @param $string * @return mixed|string */ @@ -48,6 +50,7 @@ class Lang /** * Get the currently active language. + * * @return string|null */ public static function getLanguage() @@ -57,7 +60,9 @@ class Lang /** * Try and load a language, and if successful, set it for use throughout the system. + * * @param $language + * * @return bool */ public static function setLanguage($language) @@ -73,6 +78,7 @@ class Lang /** * Return a list of available languages and their names. + * * @return array */ public static function getLanguageOptions() @@ -90,6 +96,7 @@ class Lang /** * Get the strings for the currently active language. + * * @return string[] */ public static function getStrings() @@ -99,6 +106,7 @@ class Lang /** * Initialise the Language helper, try load the language file for the user's browser or the configured default. + * * @param Config $config */ public static function init(Config $config) @@ -137,6 +145,7 @@ class Lang /** * Load a specific language file. + * * @return string[]|null */ protected static function loadLanguage() @@ -168,4 +177,24 @@ class Lang } } } + + /** + * Create a time tag for localization. + * + * See http://momentjs.com/docs/#/displaying/format/ for a list of supported formats. + * + * @param \DateTime $dateTime The dateTime to represent. + * @param string $format The moment.js format to use. + * + * @return string The formatted tag. + */ + public static function formatDateTime(\DateTime $dateTime, $format = 'lll') + { + return sprintf( + '', + $dateTime->format(\DateTime::ISO8601), + $format, + $dateTime->format(\DateTime::RFC2822) + ); + } } diff --git a/PHPCI/View/Home/index.phtml b/PHPCI/View/Home/index.phtml index 11db43cf..6e27bb79 100644 --- a/PHPCI/View/Home/index.phtml +++ b/PHPCI/View/Home/index.phtml @@ -48,7 +48,7 @@ ?>
  • - format('M j Y'); ?> +
  • @@ -58,7 +58,7 @@
  • - format('H:i'); ?> +

    getProject()->getTitle(); ?> diff --git a/PHPCI/View/SummaryTable.phtml b/PHPCI/View/SummaryTable.phtml index d8ee8a56..10782a5b 100644 --- a/PHPCI/View/SummaryTable.phtml +++ b/PHPCI/View/SummaryTable.phtml @@ -27,12 +27,12 @@ foreach($projects as $project): case 2: $successes++; $statuses[] = 'ok'; - $success = is_null($success) && !is_null($build->getFinished()) ? $build->getFinished()->format('M j Y g:ia') : $success; + $success = is_null($success) && !is_null($build->getFinished()) ? Lang::formatDateTime($build->getFinished()) : $success; break; case 3: $failures++; $statuses[] = 'failed'; - $failure = is_null($failure) && !is_null($build->getFinished()) ? $build->getFinished()->format('M j Y g:ia') : $failure; + $failure = is_null($failure) && !is_null($build->getFinished()) ? Lang::formatDateTime($build->getFinished()) : $failure; break; } } @@ -59,7 +59,7 @@ foreach($projects as $project): $message = Lang::get('x_of_x_failed', $failures, $buildCount); if (!is_null($lastSuccess) && !is_null($lastSuccess->getFinished())) { - $message .= Lang::get('last_successful_build', $lastSuccess->getFinished()->format('M j Y')); + $message .= Lang::get('last_successful_build', Lang::formatDateTime($lastSuccess->getFinished())); } else { $message .= Lang::get('never_built_successfully'); } @@ -68,7 +68,7 @@ foreach($projects as $project): $shortMessage = Lang::get('all_builds_passed_short', $buildCount, $buildCount); if (!is_null($lastFailure) && !is_null($lastFailure->getFinished())) { - $message .= Lang::get('last_failed_build', $lastFailure->getFinished()->format('M j Y')); + $message .= Lang::get('last_failed_build', Lang::formatDateTime($lastFailure->getFinished())); } else { $message .= Lang::get('never_failed_build'); } diff --git a/PHPCI/View/layout.phtml b/PHPCI/View/layout.phtml index 3375d9df..64ec0834 100644 --- a/PHPCI/View/layout.phtml +++ b/PHPCI/View/layout.phtml @@ -275,6 +275,7 @@ + diff --git a/Tests/PHPCI/Helper/LangTest.php b/Tests/PHPCI/Helper/LangTest.php new file mode 100644 index 00000000..492cca52 --- /dev/null +++ b/Tests/PHPCI/Helper/LangTest.php @@ -0,0 +1,28 @@ +prophesize('DateTime'); + $dateTime->format(DateTime::ISO8601)->willReturn("ISODATE"); + $dateTime->format(DateTime::RFC2822)->willReturn("RFCDATE"); + + $this->assertEquals('', Lang::formatDateTime($dateTime->reveal(), 'FORMAT')); + } + + public function testLang_UseDefaultFormat() + { + $dateTime = $this->prophesize('DateTime'); + $dateTime->format(DateTime::ISO8601)->willReturn("ISODATE"); + $dateTime->format(DateTime::RFC2822)->willReturn("RFCDATE"); + + $this->assertEquals('', Lang::formatDateTime($dateTime->reveal())); + } +} diff --git a/public/assets/css/daterangepicker/daterangepicker-bs3.css b/public/assets/css/daterangepicker/daterangepicker-bs3.css index eed1e9f4..9ddabb1f 100755 --- a/public/assets/css/daterangepicker/daterangepicker-bs3.css +++ b/public/assets/css/daterangepicker/daterangepicker-bs3.css @@ -18,11 +18,16 @@ margin: 4px; } -.daterangepicker.opensright .ranges, .daterangepicker.opensright .calendar { +.daterangepicker.opensright .ranges, .daterangepicker.opensright .calendar, +.daterangepicker.openscenter .ranges, .daterangepicker.openscenter .calendar { float: right; margin: 4px; } +.daterangepicker.single .ranges, .daterangepicker.single .calendar { + float: none; +} + .daterangepicker .ranges { width: 160px; text-align: left; @@ -41,6 +46,14 @@ max-width: 270px; } +.daterangepicker.show-calendar .calendar { + display: block; +} + +.daterangepicker .calendar.single .calendar-date { + border: none; +} + .daterangepicker .calendar th, .daterangepicker .calendar td { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; white-space: nowrap; @@ -48,7 +61,8 @@ min-width: 32px; } -.daterangepicker .ranges label { +.daterangepicker .daterangepicker_start_input label, +.daterangepicker .daterangepicker_end_input label { color: #333; display: block; font-size: 11px; @@ -66,7 +80,6 @@ } .daterangepicker .ranges .input-mini { - background-color: #eee; border: 1px solid #ccc; border-radius: 4px; color: #555; @@ -153,6 +166,37 @@ content: ''; } +.daterangepicker.openscenter:before { + position: absolute; + top: -7px; + left: 0; + right: 0; + width: 0; + margin-left: auto; + margin-right: auto; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.daterangepicker.openscenter:after { + position: absolute; + top: -6px; + left: 0; + right: 0; + width: 0; + margin-left: auto; + margin-right: auto; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #fff; + border-left: 6px solid transparent; + content: ''; +} + .daterangepicker.opensright:before { position: absolute; top: -7px; @@ -196,7 +240,7 @@ color: #999; } -.daterangepicker td.disabled { +.daterangepicker td.disabled, .daterangepicker option.disabled { color: #999; } @@ -211,6 +255,24 @@ border-radius: 0; } +.daterangepicker td.start-date { + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.daterangepicker td.end-date { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.daterangepicker td.start-date.end-date { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + .daterangepicker td.active, .daterangepicker td.active:hover { background-color: #357ebd; border-color: #3071a9; @@ -239,7 +301,20 @@ width: 40%; } -.daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.ampmselect { +.daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect { width: 50px; margin-bottom: 0; } + +.daterangepicker_start_input { + float: left; +} + +.daterangepicker_end_input { + float: left; + padding-left: 11px +} + +.daterangepicker th.month { + width: auto; +} diff --git a/public/assets/js/phpci.js b/public/assets/js/phpci.js index f98ea959..7e0b0f8b 100644 --- a/public/assets/js/phpci.js +++ b/public/assets/js/phpci.js @@ -3,7 +3,16 @@ var PHPCI = { intervals: {}, init: function () { + // Setup the date locale + moment.locale(PHPCI_LANGUAGE); + $(document).ready(function () { + // Format datetimes + $('time[datetime]').each(function() { + var $this = $(this); + $this.text(moment(this.dateTime).format($this.data('format') || 'lll')); + }); + // Update latest builds every 5 seconds: PHPCI.getBuilds(); PHPCI.intervals.getBuilds = setInterval(PHPCI.getBuilds, 5000); diff --git a/public/assets/js/plugins/daterangepicker/daterangepicker.js b/public/assets/js/plugins/daterangepicker/daterangepicker.js index 11a00fa3..891a1bf4 100755 --- a/public/assets/js/plugins/daterangepicker/daterangepicker.js +++ b/public/assets/js/plugins/daterangepicker/daterangepicker.js @@ -1,60 +1,39 @@ -// moment.js -// version : 2.1.0 -// author : Tim Wood -// license : MIT -// momentjs.com -!function(t){function e(t,e){return function(n){return u(t.call(this,n),e)}}function n(t,e){return function(n){return this.lang().ordinal(t.call(this,n),e)}}function s(){}function i(t){a(this,t)}function r(t){var e=t.years||t.year||t.y||0,n=t.months||t.month||t.M||0,s=t.weeks||t.week||t.w||0,i=t.days||t.day||t.d||0,r=t.hours||t.hour||t.h||0,a=t.minutes||t.minute||t.m||0,o=t.seconds||t.second||t.s||0,u=t.milliseconds||t.millisecond||t.ms||0;this._input=t,this._milliseconds=u+1e3*o+6e4*a+36e5*r,this._days=i+7*s,this._months=n+12*e,this._data={},this._bubble()}function a(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}function o(t){return 0>t?Math.ceil(t):Math.floor(t)}function u(t,e){for(var n=t+"";n.lengthn;n++)~~t[n]!==~~e[n]&&r++;return r+i}function f(t){return t?ie[t]||t.toLowerCase().replace(/(.)s$/,"$1"):t}function l(t,e){return e.abbr=t,x[t]||(x[t]=new s),x[t].set(e),x[t]}function _(t){if(!t)return H.fn._lang;if(!x[t]&&A)try{require("./lang/"+t)}catch(e){return H.fn._lang}return x[t]}function m(t){return t.match(/\[.*\]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function y(t){var e,n,s=t.match(E);for(e=0,n=s.length;n>e;e++)s[e]=ue[s[e]]?ue[s[e]]:m(s[e]);return function(i){var r="";for(e=0;n>e;e++)r+=s[e]instanceof Function?s[e].call(i,t):s[e];return r}}function M(t,e){function n(e){return t.lang().longDateFormat(e)||e}for(var s=5;s--&&N.test(e);)e=e.replace(N,n);return re[e]||(re[e]=y(e)),re[e](t)}function g(t,e){switch(t){case"DDDD":return V;case"YYYY":return X;case"YYYYY":return $;case"S":case"SS":case"SSS":case"DDD":return I;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return R;case"a":case"A":return _(e._l)._meridiemParse;case"X":return B;case"Z":case"ZZ":return j;case"T":return q;case"MM":case"DD":case"YY":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":return J;default:return new RegExp(t.replace("\\",""))}}function p(t){var e=(j.exec(t)||[])[0],n=(e+"").match(ee)||["-",0,0],s=+(60*n[1])+~~n[2];return"+"===n[0]?-s:s}function D(t,e,n){var s,i=n._a;switch(t){case"M":case"MM":i[1]=null==e?0:~~e-1;break;case"MMM":case"MMMM":s=_(n._l).monthsParse(e),null!=s?i[1]=s:n._isValid=!1;break;case"D":case"DD":case"DDD":case"DDDD":null!=e&&(i[2]=~~e);break;case"YY":i[0]=~~e+(~~e>68?1900:2e3);break;case"YYYY":case"YYYYY":i[0]=~~e;break;case"a":case"A":n._isPm=_(n._l).isPM(e);break;case"H":case"HH":case"h":case"hh":i[3]=~~e;break;case"m":case"mm":i[4]=~~e;break;case"s":case"ss":i[5]=~~e;break;case"S":case"SS":case"SSS":i[6]=~~(1e3*("0."+e));break;case"X":n._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":n._useUTC=!0,n._tzm=p(e)}null==e&&(n._isValid=!1)}function Y(t){var e,n,s=[];if(!t._d){for(e=0;7>e;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];s[3]+=~~((t._tzm||0)/60),s[4]+=~~((t._tzm||0)%60),n=new Date(0),t._useUTC?(n.setUTCFullYear(s[0],s[1],s[2]),n.setUTCHours(s[3],s[4],s[5],s[6])):(n.setFullYear(s[0],s[1],s[2]),n.setHours(s[3],s[4],s[5],s[6])),t._d=n}}function w(t){var e,n,s=t._f.match(E),i=t._i;for(t._a=[],e=0;eo&&(u=o,s=n);a(t,s)}function v(t){var e,n=t._i,s=K.exec(n);if(s){for(t._f="YYYY-MM-DD"+(s[2]||" "),e=0;4>e;e++)if(te[e][1].exec(n)){t._f+=te[e][0];break}j.exec(n)&&(t._f+=" Z"),w(t)}else t._d=new Date(n)}function T(e){var n=e._i,s=G.exec(n);n===t?e._d=new Date:s?e._d=new Date(+s[1]):"string"==typeof n?v(e):d(n)?(e._a=n.slice(0),Y(e)):e._d=n instanceof Date?new Date(+n):new Date(n)}function b(t,e,n,s,i){return i.relativeTime(e||1,!!n,t,s)}function S(t,e,n){var s=W(Math.abs(t)/1e3),i=W(s/60),r=W(i/60),a=W(r/24),o=W(a/365),u=45>s&&["s",s]||1===i&&["m"]||45>i&&["mm",i]||1===r&&["h"]||22>r&&["hh",r]||1===a&&["d"]||25>=a&&["dd",a]||45>=a&&["M"]||345>a&&["MM",W(a/30)]||1===o&&["y"]||["yy",o];return u[2]=e,u[3]=t>0,u[4]=n,b.apply({},u)}function F(t,e,n){var s,i=n-e,r=n-t.day();return r>i&&(r-=7),i-7>r&&(r+=7),s=H(t).add("d",r),{week:Math.ceil(s.dayOfYear()/7),year:s.year()}}function O(t){var e=t._i,n=t._f;return null===e||""===e?null:("string"==typeof e&&(t._i=e=_().preparse(e)),H.isMoment(e)?(t=a({},e),t._d=new Date(+e._d)):n?d(n)?k(t):w(t):T(t),new i(t))}function z(t,e){H.fn[t]=H.fn[t+"s"]=function(t){var n=this._isUTC?"UTC":"";return null!=t?(this._d["set"+n+e](t),H.updateOffset(this),this):this._d["get"+n+e]()}}function C(t){H.duration.fn[t]=function(){return this._data[t]}}function L(t,e){H.duration.fn["as"+t]=function(){return+this/e}}for(var H,P,U="2.1.0",W=Math.round,x={},A="undefined"!=typeof module&&module.exports,G=/^\/?Date\((\-?\d+)/i,Z=/(\-)?(\d*)?\.?(\d+)\:(\d+)\:(\d+)\.?(\d{3})?/,E=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,N=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,J=/\d\d?/,I=/\d{1,3}/,V=/\d{3}/,X=/\d{1,4}/,$=/[+\-]?\d{1,6}/,R=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,j=/Z|[\+\-]\d\d:?\d\d/i,q=/T/i,B=/[\+\-]?\d+(\.\d{1,3})?/,K=/^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,Q="YYYY-MM-DDTHH:mm:ssZ",te=[["HH:mm:ss.S",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],ee=/([\+\-]|\d\d)/gi,ne="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),se={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},ie={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",w:"week",M:"month",y:"year"},re={},ae="DDD w W M D d".split(" "),oe="M D H h m s w W".split(" "),ue={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return u(this.year()%100,2)},YYYY:function(){return u(this.year(),4)},YYYYY:function(){return u(this.year(),5)},gg:function(){return u(this.weekYear()%100,2)},gggg:function(){return this.weekYear()},ggggg:function(){return u(this.weekYear(),5)},GG:function(){return u(this.isoWeekYear()%100,2)},GGGG:function(){return this.isoWeekYear()},GGGGG:function(){return u(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return~~(this.milliseconds()/100)},SS:function(){return u(~~(this.milliseconds()/10),2)},SSS:function(){return u(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+u(~~(t/60),2)+":"+u(~~t%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+u(~~(10*t/6),4)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()}};ae.length;)P=ae.pop(),ue[P+"o"]=n(ue[P],P);for(;oe.length;)P=oe.pop(),ue[P+P]=e(ue[P],2);for(ue.DDDD=e(ue.DDD,3),s.prototype={set:function(t){var e,n;for(n in t)e=t[n],"function"==typeof e?this[n]=e:this["_"+n]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,n,s;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(n=H([2e3,e]),s="^"+this.months(n,"")+"|^"+this.monthsShort(n,""),this._monthsParse[e]=new RegExp(s.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},weekdaysParse:function(t){var e,n,s;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(n=H([2e3,1]).day(e),s="^"+this.weekdays(n,"")+"|^"+this.weekdaysShort(n,"")+"|^"+this.weekdaysMin(n,""),this._weekdaysParse[e]=new RegExp(s.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase()[0]},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var n=this._calendar[t];return"function"==typeof n?n.apply(e):n},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,n,s){var i=this._relativeTime[n];return"function"==typeof i?i(t,e,n,s):i.replace(/%d/i,t)},pastFuture:function(t,e){var n=this._relativeTime[t>0?"future":"past"];return"function"==typeof n?n(e):n.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return F(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6}},H=function(t,e,n){return O({_i:t,_f:e,_l:n,_isUTC:!1})},H.utc=function(t,e,n){return O({_useUTC:!0,_isUTC:!0,_l:n,_i:t,_f:e})},H.unix=function(t){return H(1e3*t)},H.duration=function(t,e){var n,s,i=H.isDuration(t),a="number"==typeof t,o=i?t._input:a?{}:t,u=Z.exec(t);return a?e?o[e]=t:o.milliseconds=t:u&&(n="-"===u[1]?-1:1,o={y:0,d:~~u[2]*n,h:~~u[3]*n,m:~~u[4]*n,s:~~u[5]*n,ms:~~u[6]*n}),s=new r(o),i&&t.hasOwnProperty("_lang")&&(s._lang=t._lang),s},H.version=U,H.defaultFormat=Q,H.updateOffset=function(){},H.lang=function(t,e){return t?(e?l(t,e):x[t]||_(t),H.duration.fn._lang=H.fn._lang=_(t),void 0):H.fn._lang._abbr},H.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),_(t)},H.isMoment=function(t){return t instanceof i},H.isDuration=function(t){return t instanceof r},H.fn=i.prototype={clone:function(){return H(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){return M(H(this).utc(),"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var t=this;return[t.year(),t.month(),t.date(),t.hours(),t.minutes(),t.seconds(),t.milliseconds()]},isValid:function(){return null==this._isValid&&(this._isValid=this._a?!c(this._a,(this._isUTC?H.utc(this._a):H(this._a)).toArray()):!isNaN(this._d.getTime())),!!this._isValid},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=M(this,t||H.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var n;return n="string"==typeof t?H.duration(+e,t):H.duration(t,e),h(this,n,1),this},subtract:function(t,e){var n;return n="string"==typeof t?H.duration(+e,t):H.duration(t,e),h(this,n,-1),this},diff:function(t,e,n){var s,i,r=this._isUTC?H(t).zone(this._offset||0):H(t).local(),a=6e4*(this.zone()-r.zone());return e=f(e),"year"===e||"month"===e?(s=432e5*(this.daysInMonth()+r.daysInMonth()),i=12*(this.year()-r.year())+(this.month()-r.month()),i+=(this-H(this).startOf("month")-(r-H(r).startOf("month")))/s,i-=6e4*(this.zone()-H(this).startOf("month").zone()-(r.zone()-H(r).startOf("month").zone()))/s,"year"===e&&(i/=12)):(s=this-r,i="second"===e?s/1e3:"minute"===e?s/6e4:"hour"===e?s/36e5:"day"===e?(s-a)/864e5:"week"===e?(s-a)/6048e5:s),n?i:o(i)},from:function(t,e){return H.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(H(),t)},calendar:function(){var t=this.diff(H().startOf("day"),"days",!0),e=-6>t?"sameElse":-1>t?"lastWeek":0>t?"lastDay":1>t?"sameDay":2>t?"nextDay":7>t?"nextWeek":"sameElse";return this.format(this.lang().calendar(e,this))},isLeapYear:function(){var t=this.year();return 0===t%4&&0!==t%100||0===t%400},isDST:function(){return this.zone()+H(t).startOf(e)},isBefore:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)<+H(t).startOf(e)},isSame:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)===+H(t).startOf(e)},min:function(t){return t=H.apply(null,arguments),this>t?this:t},max:function(t){return t=H.apply(null,arguments),t>this?this:t},zone:function(t){var e=this._offset||0;return null==t?this._isUTC?e:this._d.getTimezoneOffset():("string"==typeof t&&(t=p(t)),Math.abs(t)<16&&(t=60*t),this._offset=t,this._isUTC=!0,e!==t&&h(this,H.duration(e-t,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},daysInMonth:function(){return H.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(t){var e=W((H(this).startOf("day")-H(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},weekYear:function(t){var e=F(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=F(this,1,4).year;return null==t?e:this.add("y",t-e)},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},isoWeek:function(t){var e=F(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this._d.getDay()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},lang:function(e){return e===t?this._lang:(this._lang=_(e),this)}},P=0;P

    ' + - '
    ' + + '
    ' + + '
    ' + '
    ' + '
    ' + - '
    ' + - '' + - '' + + '
    ' + + '' + + '' + '
    ' + - '
    ' + - '' + - '' + + '
    ' + + '' + + '' + '
    ' + - ' ' + - '' + + ' ' + + '' + '
    ' + '
    ' + '
    '; - this.parentEl = (hasOptions && options.parentEl && $(options.parentEl)) || $(this.parentEl); - //the date range picker + //custom options + if (typeof options !== 'object' || options === null) + options = {}; + + this.parentEl = (typeof options === 'object' && options.parentEl && $(options.parentEl).length) ? $(options.parentEl) : $(this.parentEl); this.container = $(DRPTemplate).appendTo(this.parentEl); - if (hasOptions) { + this.setOptions(options, cb); - if (typeof options.format == 'string') + //apply CSS classes and labels to buttons + var c = this.container; + $.each(this.buttonClasses, function (idx, val) { + c.find('button').addClass(val); + }); + this.container.find('.daterangepicker_start_input label').html(this.locale.fromLabel); + this.container.find('.daterangepicker_end_input label').html(this.locale.toLabel); + if (this.applyClass.length) + this.container.find('.applyBtn').addClass(this.applyClass); + if (this.cancelClass.length) + this.container.find('.cancelBtn').addClass(this.cancelClass); + this.container.find('.applyBtn').html(this.locale.applyLabel); + this.container.find('.cancelBtn').html(this.locale.cancelLabel); + + //event listeners + + this.container.find('.calendar') + .on('click.daterangepicker', '.prev', $.proxy(this.clickPrev, this)) + .on('click.daterangepicker', '.next', $.proxy(this.clickNext, this)) + .on('click.daterangepicker', 'td.available', $.proxy(this.clickDate, this)) + .on('mouseenter.daterangepicker', 'td.available', $.proxy(this.hoverDate, this)) + .on('mouseleave.daterangepicker', 'td.available', $.proxy(this.updateFormInputs, this)) + .on('change.daterangepicker', 'select.yearselect', $.proxy(this.updateMonthYear, this)) + .on('change.daterangepicker', 'select.monthselect', $.proxy(this.updateMonthYear, this)) + .on('change.daterangepicker', 'select.hourselect,select.minuteselect,select.secondselect,select.ampmselect', $.proxy(this.updateTime, this)); + + this.container.find('.ranges') + .on('click.daterangepicker', 'button.applyBtn', $.proxy(this.clickApply, this)) + .on('click.daterangepicker', 'button.cancelBtn', $.proxy(this.clickCancel, this)) + .on('click.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.showCalendars, this)) + .on('change.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.inputsChanged, this)) + .on('keydown.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.inputsKeydown, this)) + .on('click.daterangepicker', 'li', $.proxy(this.clickRange, this)) + .on('mouseenter.daterangepicker', 'li', $.proxy(this.enterRange, this)) + .on('mouseleave.daterangepicker', 'li', $.proxy(this.updateFormInputs, this)); + + if (this.element.is('input')) { + this.element.on({ + 'click.daterangepicker': $.proxy(this.show, this), + 'focus.daterangepicker': $.proxy(this.show, this), + 'keyup.daterangepicker': $.proxy(this.updateFromControl, this) + }); + } else { + this.element.on('click.daterangepicker', $.proxy(this.toggle, this)); + } + + }; + + DateRangePicker.prototype = { + + constructor: DateRangePicker, + + setOptions: function(options, callback) { + + this.startDate = moment().startOf('day'); + this.endDate = moment().endOf('day'); + this.timeZone = moment().zone(); + this.minDate = false; + this.maxDate = false; + this.dateLimit = false; + + this.showDropdowns = false; + this.showWeekNumbers = false; + this.timePicker = false; + this.timePickerSeconds = false; + this.timePickerIncrement = 30; + this.timePicker12Hour = true; + this.singleDatePicker = false; + this.ranges = {}; + + this.opens = 'right'; + if (this.element.hasClass('pull-right')) + this.opens = 'left'; + + this.buttonClasses = ['btn', 'btn-small btn-sm']; + this.applyClass = 'btn-success'; + this.cancelClass = 'btn-default'; + + this.format = 'MM/DD/YYYY'; + this.separator = ' - '; + + this.locale = { + applyLabel: 'Apply', + cancelLabel: 'Cancel', + fromLabel: 'From', + toLabel: 'To', + weekLabel: 'W', + customRangeLabel: 'Custom Range', + daysOfWeek: moment.weekdaysMin(), + monthNames: moment.monthsShort(), + firstDay: moment.localeData()._week.dow + }; + + this.cb = function () { }; + + if (typeof options.format === 'string') this.format = options.format; - if (typeof options.separator == 'string') + if (typeof options.separator === 'string') this.separator = options.separator; - if (typeof options.startDate == 'string') + if (typeof options.startDate === 'string') this.startDate = moment(options.startDate, this.format); - if (typeof options.endDate == 'string') + if (typeof options.endDate === 'string') this.endDate = moment(options.endDate, this.format); - if (typeof options.minDate == 'string') + if (typeof options.minDate === 'string') this.minDate = moment(options.minDate, this.format); - if (typeof options.maxDate == 'string') + if (typeof options.maxDate === 'string') this.maxDate = moment(options.maxDate, this.format); - if (typeof options.startDate == 'object') + if (typeof options.startDate === 'object') this.startDate = moment(options.startDate); - if (typeof options.endDate == 'object') + if (typeof options.endDate === 'object') this.endDate = moment(options.endDate); - if (typeof options.minDate == 'object') + if (typeof options.minDate === 'object') this.minDate = moment(options.minDate); - if (typeof options.maxDate == 'object') + if (typeof options.maxDate === 'object') this.maxDate = moment(options.maxDate); - if (typeof options.ranges == 'object') { - for (var range in options.ranges) { + if (typeof options.applyClass === 'string') + this.applyClass = options.applyClass; - var start = moment(options.ranges[range][0]); - var end = moment(options.ranges[range][1]); + if (typeof options.cancelClass === 'string') + this.cancelClass = options.cancelClass; + + if (typeof options.dateLimit === 'object') + this.dateLimit = options.dateLimit; + + if (typeof options.locale === 'object') { + + if (typeof options.locale.daysOfWeek === 'object') { + // Create a copy of daysOfWeek to avoid modification of original + // options object for reusability in multiple daterangepicker instances + this.locale.daysOfWeek = options.locale.daysOfWeek.slice(); + } + + if (typeof options.locale.monthNames === 'object') { + this.locale.monthNames = options.locale.monthNames.slice(); + } + + if (typeof options.locale.firstDay === 'number') { + this.locale.firstDay = options.locale.firstDay; + } + + if (typeof options.locale.applyLabel === 'string') { + this.locale.applyLabel = options.locale.applyLabel; + } + + if (typeof options.locale.cancelLabel === 'string') { + this.locale.cancelLabel = options.locale.cancelLabel; + } + + if (typeof options.locale.fromLabel === 'string') { + this.locale.fromLabel = options.locale.fromLabel; + } + + if (typeof options.locale.toLabel === 'string') { + this.locale.toLabel = options.locale.toLabel; + } + + if (typeof options.locale.weekLabel === 'string') { + this.locale.weekLabel = options.locale.weekLabel; + } + + if (typeof options.locale.customRangeLabel === 'string') { + this.locale.customRangeLabel = options.locale.customRangeLabel; + } + } + + if (typeof options.opens === 'string') + this.opens = options.opens; + + if (typeof options.showWeekNumbers === 'boolean') { + this.showWeekNumbers = options.showWeekNumbers; + } + + if (typeof options.buttonClasses === 'string') { + this.buttonClasses = [options.buttonClasses]; + } + + if (typeof options.buttonClasses === 'object') { + this.buttonClasses = options.buttonClasses; + } + + if (typeof options.showDropdowns === 'boolean') { + this.showDropdowns = options.showDropdowns; + } + + if (typeof options.singleDatePicker === 'boolean') { + this.singleDatePicker = options.singleDatePicker; + if (this.singleDatePicker) { + this.endDate = this.startDate.clone(); + } + } + + if (typeof options.timePicker === 'boolean') { + this.timePicker = options.timePicker; + } + + if (typeof options.timePickerSeconds === 'boolean') { + this.timePickerSeconds = options.timePickerSeconds; + } + + if (typeof options.timePickerIncrement === 'number') { + this.timePickerIncrement = options.timePickerIncrement; + } + + if (typeof options.timePicker12Hour === 'boolean') { + this.timePicker12Hour = options.timePicker12Hour; + } + + // update day names order to firstDay + if (this.locale.firstDay != 0) { + var iterator = this.locale.firstDay; + while (iterator > 0) { + this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift()); + iterator--; + } + } + + var start, end, range; + + //if no start/end dates set, check if an input element contains initial values + if (typeof options.startDate === 'undefined' && typeof options.endDate === 'undefined') { + if ($(this.element).is('input[type=text]')) { + var val = $(this.element).val(), + split = val.split(this.separator); + + start = end = null; + + if (split.length == 2) { + start = moment(split[0], this.format); + end = moment(split[1], this.format); + } else if (this.singleDatePicker && val !== "") { + start = moment(val, this.format); + end = moment(val, this.format); + } + if (start !== null && end !== null) { + this.startDate = start; + this.endDate = end; + } + } + } + + // bind the time zone used to build the calendar to either the timeZone passed in through the options or the zone of the startDate (which will be the local time zone by default) + if (typeof options.timeZone === 'string' || typeof options.timeZone === 'number') { + this.timeZone = options.timeZone; + this.startDate.zone(this.timeZone); + this.endDate.zone(this.timeZone); + } else { + this.timeZone = moment(this.startDate).zone(); + } + + if (typeof options.ranges === 'object') { + for (range in options.ranges) { + + if (typeof options.ranges[range][0] === 'string') + start = moment(options.ranges[range][0], this.format); + else + start = moment(options.ranges[range][0]); + + if (typeof options.ranges[range][1] === 'string') + end = moment(options.ranges[range][1], this.format); + else + end = moment(options.ranges[range][1]); // If we have a min/max date set, bound this range // to it, but only if it would otherwise fall @@ -173,169 +365,123 @@ } var list = '
      '; - for (var range in this.ranges) { + for (range in this.ranges) { list += '
    • ' + range + '
    • '; } list += '
    • ' + this.locale.customRangeLabel + '
    • '; list += '
    '; + this.container.find('.ranges ul').remove(); this.container.find('.ranges').prepend(list); } - if (typeof options.dateLimit == 'object') - this.dateLimit = options.dateLimit; + if (typeof callback === 'function') { + this.cb = callback; + } - // update day names order to firstDay - if (typeof options.locale == 'object') { + if (!this.timePicker) { + this.startDate = this.startDate.startOf('day'); + this.endDate = this.endDate.endOf('day'); + } - if (typeof options.locale.daysOfWeek == 'object') { + if (this.singleDatePicker) { + this.opens = 'right'; + this.container.addClass('single'); + this.container.find('.calendar.right').show(); + this.container.find('.calendar.left').hide(); + if (!this.timePicker) { + this.container.find('.ranges').hide(); + } else { + this.container.find('.ranges .daterangepicker_start_input, .ranges .daterangepicker_end_input').hide(); + } + if (!this.container.find('.calendar.right').hasClass('single')) + this.container.find('.calendar.right').addClass('single'); + } else { + this.container.removeClass('single'); + this.container.find('.calendar.right').removeClass('single'); + this.container.find('.ranges').show(); + } - // Create a copy of daysOfWeek to avoid modification of original - // options object for reusability in multiple daterangepicker instances - this.locale.daysOfWeek = options.locale.daysOfWeek.slice(); + this.oldStartDate = this.startDate.clone(); + this.oldEndDate = this.endDate.clone(); + this.oldChosenLabel = this.chosenLabel; + + this.leftCalendar = { + month: moment([this.startDate.year(), this.startDate.month(), 1, this.startDate.hour(), this.startDate.minute(), this.startDate.second()]), + calendar: [] + }; + + this.rightCalendar = { + month: moment([this.endDate.year(), this.endDate.month(), 1, this.endDate.hour(), this.endDate.minute(), this.endDate.second()]), + calendar: [] + }; + + if (this.opens == 'right' || this.opens == 'center') { + //swap calendar positions + var first = this.container.find('.calendar.first'); + var second = this.container.find('.calendar.second'); + + if (second.hasClass('single')) { + second.removeClass('single'); + first.addClass('single'); } - if (typeof options.locale.firstDay == 'number') { - this.locale.firstDay = options.locale.firstDay; - var iterator = options.locale.firstDay; - while (iterator > 0) { - this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift()); - iterator--; - } + first.removeClass('left').addClass('right'); + second.removeClass('right').addClass('left'); + + if (this.singleDatePicker) { + first.show(); + second.hide(); } } - if (typeof options.opens == 'string') - this.opens = options.opens; - - if (typeof options.showWeekNumbers == 'boolean') { - this.showWeekNumbers = options.showWeekNumbers; + if (typeof options.ranges === 'undefined' && !this.singleDatePicker) { + this.container.addClass('show-calendar'); } - if (typeof options.buttonClasses == 'string') { - this.buttonClasses = [options.buttonClasses]; - } + this.container.addClass('opens' + this.opens); - if (typeof options.buttonClasses == 'object') { - this.buttonClasses = options.buttonClasses; - } + this.updateView(); + this.updateCalendars(); - if (typeof options.showDropdowns == 'boolean') { - this.showDropdowns = options.showDropdowns; - } + }, - if (typeof options.timePicker == 'boolean') { - this.timePicker = options.timePicker; - } + setStartDate: function(startDate) { + if (typeof startDate === 'string') + this.startDate = moment(startDate, this.format).zone(this.timeZone); - if (typeof options.timePickerIncrement == 'number') { - this.timePickerIncrement = options.timePickerIncrement; - } + if (typeof startDate === 'object') + this.startDate = moment(startDate); - if (typeof options.timePicker12Hour == 'boolean') { - this.timePicker12Hour = options.timePicker12Hour; - } + if (!this.timePicker) + this.startDate = this.startDate.startOf('day'); - } + this.oldStartDate = this.startDate.clone(); - if (!this.timePicker) { - this.startDate = this.startDate.startOf('day'); - this.endDate = this.endDate.startOf('day'); - } + this.updateView(); + this.updateCalendars(); + this.updateInputText(); + }, - //apply CSS classes to buttons - var c = this.container; - $.each(this.buttonClasses, function (idx, val) { - c.find('button').addClass(val); - }); + setEndDate: function(endDate) { + if (typeof endDate === 'string') + this.endDate = moment(endDate, this.format).zone(this.timeZone); - if (this.opens == 'right') { - //swap calendar positions - var left = this.container.find('.calendar.left'); - var right = this.container.find('.calendar.right'); - left.removeClass('left').addClass('right'); - right.removeClass('right').addClass('left'); - } + if (typeof endDate === 'object') + this.endDate = moment(endDate); - if (typeof options == 'undefined' || typeof options.ranges == 'undefined') { - this.container.find('.calendar').show(); - this.move(); - } + if (!this.timePicker) + this.endDate = this.endDate.endOf('day'); - if (typeof cb == 'function') - this.cb = cb; + this.oldEndDate = this.endDate.clone(); - this.container.addClass('opens' + this.opens); - - //try parse date if in text input - if (!hasOptions || (typeof options.startDate == 'undefined' && typeof options.endDate == 'undefined')) { - if ($(this.element).is('input[type=text]')) { - var val = $(this.element).val(); - var split = val.split(this.separator); - var start, end; - if (split.length == 2) { - start = moment(split[0], this.format); - end = moment(split[1], this.format); - } - if (start != null && end != null) { - this.startDate = start; - this.endDate = end; - } - } - } - - //state - this.oldStartDate = this.startDate.clone(); - this.oldEndDate = this.endDate.clone(); - - this.leftCalendar = { - month: moment([this.startDate.year(), this.startDate.month(), 1, this.startDate.hour(), this.startDate.minute()]), - calendar: [] - }; - - this.rightCalendar = { - month: moment([this.endDate.year(), this.endDate.month(), 1, this.endDate.hour(), this.endDate.minute()]), - calendar: [] - }; - - //event listeners - this.container.on('mousedown', $.proxy(this.mousedown, this)); - - this.container.find('.calendar') - .on('click', '.prev', $.proxy(this.clickPrev, this)) - .on('click', '.next', $.proxy(this.clickNext, this)) - .on('click', 'td.available', $.proxy(this.clickDate, this)) - .on('mouseenter', 'td.available', $.proxy(this.enterDate, this)) - .on('mouseleave', 'td.available', $.proxy(this.updateFormInputs, this)) - .on('change', 'select.yearselect', $.proxy(this.updateMonthYear, this)) - .on('change', 'select.monthselect', $.proxy(this.updateMonthYear, this)) - .on('change', 'select.hourselect,select.minuteselect,select.ampmselect', $.proxy(this.updateTime, this)); - - this.container.find('.ranges') - .on('click', 'button.applyBtn', $.proxy(this.clickApply, this)) - .on('click', 'button.cancelBtn', $.proxy(this.clickCancel, this)) - .on('click', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.showCalendars, this)) - .on('click', 'li', $.proxy(this.clickRange, this)) - .on('mouseenter', 'li', $.proxy(this.enterRange, this)) - .on('mouseleave', 'li', $.proxy(this.updateFormInputs, this)); - - this.element.on('keyup', $.proxy(this.updateFromControl, this)); - - this.updateView(); - this.updateCalendars(); - - }; - - DateRangePicker.prototype = { - - constructor: DateRangePicker, - - mousedown: function (e) { - e.stopPropagation(); + this.updateView(); + this.updateCalendars(); + this.updateInputText(); }, updateView: function () { - this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()); - this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()); + this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()).hour(this.startDate.hour()).minute(this.startDate.minute()); + this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()).hour(this.endDate.hour()).minute(this.endDate.minute()); this.updateFormInputs(); }, @@ -354,11 +500,20 @@ if (!this.element.is('input')) return; if (!this.element.val().length) return; - var dateString = this.element.val().split(this.separator); - var start = moment(dateString[0], this.format); - var end = moment(dateString[1], this.format); + var dateString = this.element.val().split(this.separator), + start = null, + end = null; + + if(dateString.length === 2) { + start = moment(dateString[0], this.format).zone(this.timeZone); + end = moment(dateString[1], this.format).zone(this.timeZone); + } + + if (this.singleDatePicker || start === null || end === null) { + start = moment(this.element.val(), this.format).zone(this.timeZone); + end = start; + } - if (start == null || end == null) return; if (end.isBefore(start)) return; this.oldStartDate = this.startDate.clone(); @@ -375,18 +530,24 @@ notify: function () { this.updateView(); - this.cb(this.startDate, this.endDate); + this.cb(this.startDate, this.endDate, this.chosenLabel); }, move: function () { - var parentOffset = { - top: this.parentEl.offset().top - (this.parentEl.is('body') ? 0 : this.parentEl.scrollTop()), - left: this.parentEl.offset().left - (this.parentEl.is('body') ? 0 : this.parentEl.scrollLeft()) - }; + var parentOffset = { top: 0, left: 0 }; + var parentRightEdge = $(window).width(); + if (!this.parentEl.is('body')) { + parentOffset = { + top: this.parentEl.offset().top - this.parentEl.scrollTop(), + left: this.parentEl.offset().left - this.parentEl.scrollLeft() + }; + parentRightEdge = this.parentEl[0].clientWidth + this.parentEl.offset().left; + } + if (this.opens == 'left') { this.container.css({ top: this.element.offset().top + this.element.outerHeight() - parentOffset.top, - right: $(window).width() - this.element.offset().left - this.element.outerWidth() - parentOffset.left, + right: parentRightEdge - this.element.offset().left - this.element.outerWidth(), left: 'auto' }); if (this.container.offset().left < 0) { @@ -395,6 +556,19 @@ left: 9 }); } + } else if (this.opens == 'center') { + this.container.css({ + top: this.element.offset().top + this.element.outerHeight() - parentOffset.top, + left: this.element.offset().left - parentOffset.left + this.element.outerWidth() / 2 + - this.container.outerWidth() / 2, + right: 'auto' + }); + if (this.container.offset().left < 0) { + this.container.css({ + right: 'auto', + left: 9 + }); + } } else { this.container.css({ top: this.element.offset().top + this.element.outerHeight() - parentOffset.top, @@ -410,20 +584,58 @@ } }, + toggle: function (e) { + if (this.element.hasClass('active')) { + this.hide(); + } else { + this.show(); + } + }, + show: function (e) { + if (this.isShowing) return; + + this.element.addClass('active'); this.container.show(); this.move(); - if (e) { - e.stopPropagation(); - e.preventDefault(); - } + // Create a click proxy that is private to this instance of datepicker, for unbinding + this._outsideClickProxy = $.proxy(function (e) { this.outsideClick(e); }, this); + // Bind global datepicker mousedown for hiding and + $(document) + .on('mousedown.daterangepicker', this._outsideClickProxy) + // also support mobile devices + .on('touchend.daterangepicker', this._outsideClickProxy) + // also explicitly play nice with Bootstrap dropdowns, which stopPropagation when clicking them + .on('click.daterangepicker', '[data-toggle=dropdown]', this._outsideClickProxy) + // and also close when focus changes to outside the picker (eg. tabbing between controls) + .on('focusin.daterangepicker', this._outsideClickProxy); - $(document).on('mousedown', $.proxy(this.hide, this)); - this.element.trigger('shown', {target: e.target, picker: this}); + this.isShowing = true; + this.element.trigger('show.daterangepicker', this); + }, + + outsideClick: function (e) { + var target = $(e.target); + // if the page is clicked anywhere except within the daterangerpicker/button + // itself then call this.hide() + if ( + // ie modal dialog fix + e.type == "focusin" || + target.closest(this.element).length || + target.closest(this.container).length || + target.closest('.calendar-date').length + ) return; + this.hide(); }, hide: function (e) { + if (!this.isShowing) return; + + $(document) + .off('.daterangepicker'); + + this.element.removeClass('active'); this.container.hide(); if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate)) @@ -432,11 +644,12 @@ this.oldStartDate = this.startDate.clone(); this.oldEndDate = this.endDate.clone(); - $(document).off('mousedown', this.hide); - this.element.trigger('hidden', { picker: this }); + this.isShowing = false; + this.element.trigger('hide.daterangepicker', this); }, enterRange: function (e) { + // mouse pointer has entered a range label var label = e.target.innerHTML; if (label == this.locale.customRangeLabel) { this.updateView(); @@ -448,17 +661,51 @@ }, showCalendars: function() { - this.container.find('.calendar').show(); + this.container.addClass('show-calendar'); this.move(); + this.element.trigger('showCalendar.daterangepicker', this); + }, + + hideCalendars: function() { + this.container.removeClass('show-calendar'); + this.element.trigger('hideCalendar.daterangepicker', this); + }, + + // when a date is typed into the start to end date textboxes + inputsChanged: function (e) { + var el = $(e.target); + var date = moment(el.val(), this.format); + if (!date.isValid()) return; + + var startDate, endDate; + if (el.attr('name') === 'daterangepicker_start') { + startDate = date; + endDate = this.endDate; + } else { + startDate = this.startDate; + endDate = date; + } + this.setCustomDates(startDate, endDate); + }, + + inputsKeydown: function(e) { + if (e.keyCode === 13) { + this.inputsChanged(e); + this.notify(); + } }, updateInputText: function() { - if (this.element.is('input')) + if (this.element.is('input') && !this.singleDatePicker) { this.element.val(this.startDate.format(this.format) + this.separator + this.endDate.format(this.format)); + } else if (this.element.is('input')) { + this.element.val(this.endDate.format(this.format)); + } }, clickRange: function (e) { var label = e.target.innerHTML; + this.chosenLabel = label; if (label == this.locale.customRangeLabel) { this.showCalendars(); } else { @@ -469,7 +716,7 @@ if (!this.timePicker) { this.startDate.startOf('day'); - this.endDate.startOf('day'); + this.endDate.endOf('day'); } this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()).hour(this.startDate.hour()).minute(this.startDate.minute()); @@ -478,17 +725,18 @@ this.updateInputText(); - this.container.find('.calendar').hide(); + this.hideCalendars(); this.hide(); + this.element.trigger('apply.daterangepicker', this); } }, clickPrev: function (e) { var cal = $(e.target).parents('.calendar'); if (cal.hasClass('left')) { - this.leftCalendar.month.subtract('month', 1); + this.leftCalendar.month.subtract(1, 'month'); } else { - this.rightCalendar.month.subtract('month', 1); + this.rightCalendar.month.subtract(1, 'month'); } this.updateCalendars(); }, @@ -496,15 +744,14 @@ clickNext: function (e) { var cal = $(e.target).parents('.calendar'); if (cal.hasClass('left')) { - this.leftCalendar.month.add('month', 1); + this.leftCalendar.month.add(1, 'month'); } else { - this.rightCalendar.month.add('month', 1); + this.rightCalendar.month.add(1, 'month'); } this.updateCalendars(); }, - enterDate: function (e) { - + hoverDate: function (e) { var title = $(e.target).attr('data-title'); var row = title.substr(1, 1); var col = title.substr(3, 1); @@ -515,7 +762,19 @@ } else { this.container.find('input[name=daterangepicker_end]').val(this.rightCalendar.calendar[row][col].format(this.format)); } + }, + setCustomDates: function(startDate, endDate) { + this.chosenLabel = this.locale.customRangeLabel; + if (startDate.isAfter(endDate)) { + var difference = this.endDate.diff(this.startDate); + endDate = moment(startDate).add(difference, 'ms'); + } + this.startDate = startDate; + this.endDate = endDate; + + this.updateView(); + this.updateCalendars(); }, clickDate: function (e) { @@ -524,19 +783,20 @@ var col = title.substr(3, 1); var cal = $(e.target).parents('.calendar'); + var startDate, endDate; if (cal.hasClass('left')) { - var startDate = this.leftCalendar.calendar[row][col]; - var endDate = this.endDate; - if (typeof this.dateLimit == 'object') { + startDate = this.leftCalendar.calendar[row][col]; + endDate = this.endDate; + if (typeof this.dateLimit === 'object') { var maxDate = moment(startDate).add(this.dateLimit).startOf('day'); if (endDate.isAfter(maxDate)) { endDate = maxDate; } } } else { - var startDate = this.startDate; - var endDate = this.rightCalendar.calendar[row][col]; - if (typeof this.dateLimit == 'object') { + startDate = this.startDate; + endDate = this.rightCalendar.calendar[row][col]; + if (typeof this.dateLimit === 'object') { var minDate = moment(endDate).subtract(this.dateLimit).startOf('day'); if (startDate.isBefore(minDate)) { startDate = minDate; @@ -544,72 +804,72 @@ } } - cal.find('td').removeClass('active'); - - if (startDate.isSame(endDate) || startDate.isBefore(endDate)) { - $(e.target).addClass('active'); - this.startDate = startDate; - this.endDate = endDate; - } else if (startDate.isAfter(endDate)) { - $(e.target).addClass('active'); - this.startDate = startDate; - this.endDate = moment(startDate).add('day', 1).startOf('day'); + if (this.singleDatePicker && cal.hasClass('left')) { + endDate = startDate.clone(); + } else if (this.singleDatePicker && cal.hasClass('right')) { + startDate = endDate.clone(); } - this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()); - this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()); - this.updateCalendars(); + cal.find('td').removeClass('active'); + + $(e.target).addClass('active'); + + this.setCustomDates(startDate, endDate); + + if (!this.timePicker) + endDate.endOf('day'); + + if (this.singleDatePicker && !this.timePicker) + this.clickApply(); }, clickApply: function (e) { this.updateInputText(); this.hide(); + this.element.trigger('apply.daterangepicker', this); }, clickCancel: function (e) { this.startDate = this.oldStartDate; this.endDate = this.oldEndDate; + this.chosenLabel = this.oldChosenLabel; this.updateView(); this.updateCalendars(); this.hide(); + this.element.trigger('cancel.daterangepicker', this); }, updateMonthYear: function (e) { - - var isLeft = $(e.target).closest('.calendar').hasClass('left'); - var cal = this.container.find('.calendar.left'); - if (!isLeft) - cal = this.container.find('.calendar.right'); + var isLeft = $(e.target).closest('.calendar').hasClass('left'), + leftOrRight = isLeft ? 'left' : 'right', + cal = this.container.find('.calendar.'+leftOrRight); // Month must be Number for new moment versions var month = parseInt(cal.find('.monthselect').val(), 10); var year = cal.find('.yearselect').val(); - if (isLeft) { - this.leftCalendar.month.month(month).year(year); - } else { - this.rightCalendar.month.month(month).year(year); - } - + this[leftOrRight+'Calendar'].month.month(month).year(year); this.updateCalendars(); - }, updateTime: function(e) { - var isLeft = $(e.target).closest('.calendar').hasClass('left'); - var cal = this.container.find('.calendar.left'); - if (!isLeft) - cal = this.container.find('.calendar.right'); + var cal = $(e.target).closest('.calendar'), + isLeft = cal.hasClass('left'); - var hour = parseInt(cal.find('.hourselect').val()); - var minute = parseInt(cal.find('.minuteselect').val()); + var hour = parseInt(cal.find('.hourselect').val(), 10); + var minute = parseInt(cal.find('.minuteselect').val(), 10); + var second = 0; + + if (this.timePickerSeconds) { + second = parseInt(cal.find('.secondselect').val(), 10); + } if (this.timePicker12Hour) { var ampm = cal.find('.ampmselect').val(); - if (ampm == 'PM' && hour < 12) + if (ampm === 'PM' && hour < 12) hour += 12; - if (ampm == 'AM' && hour == 12) + if (ampm === 'AM' && hour === 12) hour = 0; } @@ -617,25 +877,31 @@ var start = this.startDate.clone(); start.hour(hour); start.minute(minute); + start.second(second); this.startDate = start; - this.leftCalendar.month.hour(hour).minute(minute); + this.leftCalendar.month.hour(hour).minute(minute).second(second); + if (this.singleDatePicker) + this.endDate = start.clone(); } else { var end = this.endDate.clone(); end.hour(hour); end.minute(minute); + end.second(second); this.endDate = end; - this.rightCalendar.month.hour(hour).minute(minute); + if (this.singleDatePicker) + this.startDate = end.clone(); + this.rightCalendar.month.hour(hour).minute(minute).second(second); } + this.updateView(); this.updateCalendars(); - }, updateCalendars: function () { - this.leftCalendar.calendar = this.buildCalendar(this.leftCalendar.month.month(), this.leftCalendar.month.year(), this.leftCalendar.month.hour(), this.leftCalendar.month.minute(), 'left'); - this.rightCalendar.calendar = this.buildCalendar(this.rightCalendar.month.month(), this.rightCalendar.month.year(), this.rightCalendar.month.hour(), this.rightCalendar.month.minute(), 'right'); - this.container.find('.calendar.left').html(this.renderCalendar(this.leftCalendar.calendar, this.startDate, this.minDate, this.maxDate)); - this.container.find('.calendar.right').html(this.renderCalendar(this.rightCalendar.calendar, this.endDate, this.startDate, this.maxDate)); + this.leftCalendar.calendar = this.buildCalendar(this.leftCalendar.month.month(), this.leftCalendar.month.year(), this.leftCalendar.month.hour(), this.leftCalendar.month.minute(), this.leftCalendar.month.second(), 'left'); + this.rightCalendar.calendar = this.buildCalendar(this.rightCalendar.month.month(), this.rightCalendar.month.year(), this.rightCalendar.month.hour(), this.rightCalendar.month.minute(), this.rightCalendar.month.second(), 'right'); + this.container.find('.calendar.left').empty().html(this.renderCalendar(this.leftCalendar.calendar, this.startDate, this.minDate, this.maxDate, 'left')); + this.container.find('.calendar.right').empty().html(this.renderCalendar(this.rightCalendar.calendar, this.endDate, this.singleDatePicker ? this.minDate : this.startDate, this.maxDate, 'right')); this.container.find('.ranges li').removeClass('active'); var customRange = true; @@ -644,34 +910,44 @@ if (this.timePicker) { if (this.startDate.isSame(this.ranges[range][0]) && this.endDate.isSame(this.ranges[range][1])) { customRange = false; - this.container.find('.ranges li:eq(' + i + ')').addClass('active'); + this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')') + .addClass('active').html(); } } else { //ignore times when comparing dates if time picker is not enabled if (this.startDate.format('YYYY-MM-DD') == this.ranges[range][0].format('YYYY-MM-DD') && this.endDate.format('YYYY-MM-DD') == this.ranges[range][1].format('YYYY-MM-DD')) { customRange = false; - this.container.find('.ranges li:eq(' + i + ')').addClass('active'); + this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')') + .addClass('active').html(); } } i++; } - if (customRange) - this.container.find('.ranges li:last').addClass('active'); + if (customRange) { + this.chosenLabel = this.container.find('.ranges li:last').addClass('active').html(); + this.showCalendars(); + } }, - buildCalendar: function (month, year, hour, minute, side) { - + buildCalendar: function (month, year, hour, minute, second, side) { + var daysInMonth = moment([year, month]).daysInMonth(); var firstDay = moment([year, month, 1]); - var lastMonth = moment(firstDay).subtract('month', 1).month(); - var lastYear = moment(firstDay).subtract('month', 1).year(); + var lastDay = moment([year, month, daysInMonth]); + var lastMonth = moment(firstDay).subtract(1, 'month').month(); + var lastYear = moment(firstDay).subtract(1, 'month').year(); var daysInLastMonth = moment([lastYear, lastMonth]).daysInMonth(); var dayOfWeek = firstDay.day(); + var i; + //initialize a 6 rows x 7 columns array for the calendar var calendar = []; - for (var i = 0; i < 6; i++) { + calendar.firstDay = firstDay; + calendar.lastDay = lastDay; + + for (i = 0; i < 6; i++) { calendar[i] = []; } @@ -683,25 +959,39 @@ if (dayOfWeek == this.locale.firstDay) startDay = daysInLastMonth - 6; - var curDate = moment([lastYear, lastMonth, startDay, 12, minute]); - for (var i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add('hour', 24)) { - if (i > 0 && col % 7 == 0) { + var curDate = moment([lastYear, lastMonth, startDay, 12, minute, second]).zone(this.timeZone); + + var col, row; + for (i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add(24, 'hour')) { + if (i > 0 && col % 7 === 0) { col = 0; row++; } calendar[row][col] = curDate.clone().hour(hour); curDate.hour(12); + + if (this.minDate && calendar[row][col].format('YYYY-MM-DD') == this.minDate.format('YYYY-MM-DD') && calendar[row][col].isBefore(this.minDate) && side == 'left') { + calendar[row][col] = this.minDate.clone(); + } + + if (this.maxDate && calendar[row][col].format('YYYY-MM-DD') == this.maxDate.format('YYYY-MM-DD') && calendar[row][col].isAfter(this.maxDate) && side == 'right') { + calendar[row][col] = this.maxDate.clone(); + } + } return calendar; - }, renderDropdowns: function (selected, minDate, maxDate) { var currentMonth = selected.month(); + var currentYear = selected.year(); + var maxYear = (maxDate && maxDate.year()) || (currentYear + 5); + var minYear = (minDate && minDate.year()) || (currentYear - 50); + var monthHtml = '"; - var currentYear = selected.year(); - var maxYear = (maxDate && maxDate.year()) || (currentYear + 5); - var minYear = (minDate && minDate.year()) || (currentYear - 50); var yearHtml = ''; + + // Disallow selections before the minDate or after the maxDate + var min_hour = 0; + var max_hour = 23; + + if (minDate && (side == 'left' || this.singleDatePicker) && selected.format('YYYY-MM-DD') == minDate.format('YYYY-MM-DD')) { + min_hour = minDate.hour(); + if (selected.hour() < min_hour) + selected.hour(min_hour); + if (this.timePicker12Hour && min_hour >= 12 && selected.hour() >= 12) + min_hour -= 12; + if (this.timePicker12Hour && min_hour == 12) + min_hour = 1; + } + + if (maxDate && (side == 'right' || this.singleDatePicker) && selected.format('YYYY-MM-DD') == maxDate.format('YYYY-MM-DD')) { + max_hour = maxDate.hour(); + if (selected.hour() > max_hour) + selected.hour(max_hour); + if (this.timePicker12Hour && max_hour >= 12 && selected.hour() >= 12) + max_hour -= 12; + } + var start = 0; var end = 23; var selected_hour = selected.hour(); @@ -822,13 +1133,16 @@ end = 12; if (selected_hour >= 12) selected_hour -= 12; - if (selected_hour == 0) + if (selected_hour === 0) selected_hour = 12; } - for (var i = start; i <= end; i++) { + for (i = start; i <= end; i++) { + if (i == selected_hour) { html += ''; + } else if (i < min_hour || i > max_hour) { + html += ''; } else { html += ''; } @@ -838,12 +1152,30 @@ html += ' '; + if (this.timePickerSeconds) { + html += ': '; + } + if (this.timePicker12Hour) { html += ''; } @@ -867,6 +1229,14 @@ return html; + }, + + remove: function() { + + this.container.remove(); + this.element.off('.daterangepicker'); + this.element.removeData('daterangepicker'); + } }; @@ -874,10 +1244,11 @@ $.fn.daterangepicker = function (options, cb) { this.each(function () { var el = $(this); - if (!el.data('daterangepicker')) - el.data('daterangepicker', new DateRangePicker(el, options, cb)); + if (el.data('daterangepicker')) + el.data('daterangepicker').remove(); + el.data('daterangepicker', new DateRangePicker(el, options, cb)); }); return this; }; -}(window.jQuery); +})); From 0be37b9e8578115728b86644de21b4465504bfd0 Mon Sep 17 00:00:00 2001 From: Adirelle Date: Thu, 8 Jan 2015 17:30:15 +0100 Subject: [PATCH 068/329] Franch translation updates. Closes #733 --- PHPCI/Languages/lang.fr.php | 56 ++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/PHPCI/Languages/lang.fr.php b/PHPCI/Languages/lang.fr.php index bd85bccd..9e7cd339 100644 --- a/PHPCI/Languages/lang.fr.php +++ b/PHPCI/Languages/lang.fr.php @@ -14,21 +14,21 @@ $strings = array( // Log in: 'log_in_to_phpci' => 'Connectez-vous à PHPCI', 'login_error' => 'Adresse email ou mot de passe invalide', - 'forgotten_password_link' => 'Mot de passe oublié ?', + 'forgotten_password_link' => 'Mot de passe oublié ?', 'reset_emailed' => 'Nous vous avons envoyé un email avec un lien pour réinitialiser votre mot de passe.', 'reset_header' => 'Pas d\'inquiétude
    Entrez simplement votre adresse email ci-dessous et nous vous enverrons un message avec un lien pour réinitialiser votre mot de passe.', 'reset_email_address' => 'Entrez votre adresse email:', 'reset_send_email' => 'Envoyer le mail', 'reset_enter_password' => 'Veuillez entrer un nouveau mot de passe', - 'reset_new_password' => 'Nouveau mot de passe :', + 'reset_new_password' => 'Nouveau mot de passe :', 'reset_change_password' => 'Modifier le mot de passe', 'reset_no_user_exists' => 'Il n\'existe aucun utilisateur avec cette adresse email, merci de réessayer.', 'reset_email_body' => 'Bonjour %s, Vous avez reçu cet email parce qu\'une demande de réinitialisation de mot de passe a été faite pour votre compte PHPCI. -Si c\'est bien vous, merci de cliquer sur le lien suivant pour réinitialiser votre mot de passe : %ssession/reset-password/%d/%s +Si c\'est bien vous, merci de cliquer sur le lien suivant pour réinitialiser votre mot de passe : %ssession/reset-password/%d/%s Sinon, merci d\'ignorer ce message. @@ -49,9 +49,9 @@ PHPCI', 'n_builds_running' => '%d builds en cours d\'exécution', 'edit_profile' => 'Éditer le profil', 'sign_out' => 'Déconnexion', - 'branch_x' => 'Branche : %s', - 'created_x' => 'Créé à : %s', - 'started_x' => 'Démarré à : %s', + 'branch_x' => 'Branche : %s', + 'created_x' => 'Créé à : %s', + 'started_x' => 'Démarré à : %s', // Sidebar 'hello_name' => 'Salut %s', @@ -67,19 +67,19 @@ PHPCI', 'delete_project' => 'Supprimer le projet', // Project Summary: - 'no_builds_yet' => 'Aucun build pour le moment!', - 'x_of_x_failed' => '%d parmis les derniers %d builds ont échoué.', - 'x_of_x_failed_short' => '%d / %d ont échoué.', - 'last_successful_build' => ' Le dernier build qui a réussi est %s.', - 'never_built_successfully' => ' Aucun build n\'a été exécuté avec succès sur ce projet.', - 'all_builds_passed' => 'Les derniers %d builds ont réussis.', - 'all_builds_passed_short' => '%d / %d ont réussis.', - 'last_failed_build' => ' Le dernier build en échec est %s.', - 'never_failed_build' => ' Ce projet n\'a jamais eu un build en échec.', + 'no_builds_yet' => 'Aucun build pour le moment !', + 'x_of_x_failed' => '%d des %d derniers builds ont échoué.', + 'x_of_x_failed_short' => '%d échecs / %d.', + 'last_successful_build' => ' Le dernier build réussi date du %s.', + 'never_built_successfully' => ' Aucun build de ce projet n\'a réussi.', + 'all_builds_passed' => 'Les %d derniers builds ont réussi.', + 'all_builds_passed_short' => '%d réussites / %d.', + 'last_failed_build' => ' Le dernier build en échec date du %s.', + 'never_failed_build' => ' Aucun build de ce projet n\'a échoué.', 'view_project' => 'Voir le projet', // Timeline: - 'latest_builds' => 'Les derniers Builds', + 'latest_builds' => 'Derniers builds', 'pending' => 'En attente', 'running' => 'En cours', 'success' => 'Terminé', @@ -102,8 +102,8 @@ PHPCI', 'local' => 'Chemin local', 'hg' => 'Mercurial', - 'where_hosted' => 'Où est hébergé votre projet ?', - 'choose_github' => 'Choisissez un dépôt GitHub :', + 'where_hosted' => 'Où est hébergé votre projet ?', + 'choose_github' => 'Choisissez un dépôt GitHub :', 'repo_name' => 'Nom du dépôt / URL (distance) ou chemin (local)', 'project_title' => 'Titre du projet', @@ -112,7 +112,7 @@ PHPCI', 'build_config' => 'Configuration PHPCI spécifique pour ce projet (si vous ne pouvez pas ajouter de fichier phpci.yml à la racine du dépôt)', 'default_branch' => 'Nom de la branche par défaut', - 'allow_public_status' => 'Activer la page de statut publique et l\'image pour ce projet ?', + 'allow_public_status' => 'Activer la page de statut publique et l\'image pour ce projet ?', 'save_project' => 'Enregistrer le projet', 'error_mercurial' => 'Les URLs de dépôt Mercurial doivent commencer par http:// ou https://', @@ -155,7 +155,7 @@ PHPCI', 'committed_by_x' => 'Committé par %s', - 'commit_id_x' => 'Commit: %s', + 'commit_id_x' => 'Commit : %s', 'chart_display' => 'Ce graphique s\'affichera une fois que le build sera terminé.', @@ -207,7 +207,7 @@ PHPCI', 'update_your_details' => 'Mettre à jour vos préférences', 'your_details_updated' => 'Vos préférences ont été bien mises à jour.', 'add_user' => 'Ajouter un utilisateur', - 'is_admin' => 'Est-il administrateur ?', + 'is_admin' => 'Est-il administrateur ?', 'yes' => 'Oui', 'no' => 'Non', 'edit' => 'Éditer', @@ -287,17 +287,17 @@ PHPCI', 'config_path' => 'Chemin vers le fichier de configuration', 'install_phpci' => 'Installer PHPCI', 'welcome_to_phpci' => 'Bienvenue sur PHPCI', - 'please_answer' => 'Merci de répondre aux questions suivantes:', + 'please_answer' => 'Merci de répondre aux questions suivantes :', 'phpci_php_req' => 'PHPCI requiert au moins PHP 5.3.8 pour fonctionner.', - 'extension_required' => 'Extensions requises: %s', + 'extension_required' => 'Extensions requises : %s', 'function_required' => 'PHPCI doit être capable d\'appeler la fonction %s(). Est-ce qu\'elle est désactivée dans votre php.ini?', 'requirements_not_met' => 'PHPCI ne peut pas être installé parce que toutes les conditions requises ne sont pas respectées. Merci de corriger les erreurs ci-dessus avant de continuer.', 'must_be_valid_email' => 'Doit être une adresse email valide.', 'must_be_valid_url' => 'Doit être une URL valide.', - 'enter_name' => 'Nom de l\'admin:', - 'enter_email' => 'Email de l\'admin:', - 'enter_password' => 'Mot de passe de l\'admin:', + 'enter_name' => 'Nom de l\'admin :', + 'enter_email' => 'Email de l\'admin :', + 'enter_password' => 'Mot de passe de l\'admin :', 'enter_phpci_url' => 'Votre URL vers PHPCI (par exemple "http://phpci.local"): ', 'enter_db_host' => 'Merci d\'entrer le nom d\'hôte MySQL [localhost]: ', @@ -306,7 +306,7 @@ PHPCI', 'enter_db_pass' => 'Merci d\'entrer le mot de passe MySQL: ', 'could_not_connect' => 'PHPCI ne peut pas se connecter à MySQL à partir des informations fournies. Veuillez réessayer..', 'setting_up_db' => 'Paramétrage de la base de données... ', - 'user_created' => 'Le compte utilisateur a été créé!', + 'user_created' => 'Le compte utilisateur a été créé !', 'failed_to_create' => 'PHPCI n\'a pas réussi à créer votre compte admin.', 'config_exists' => 'Le fichier de configuration PHPCI existe et n\'est pas vide.', 'update_instead' => 'Si vous essayez de mettre à jour PHPCI, merci d\'utiliser la commande phpci:update.', @@ -365,7 +365,7 @@ PHPCI', 'n_emails_sent' => '%d emails envoyés.', 'n_emails_failed' => '%d emails dont l\'envoi a échoué.', 'unable_to_set_env' => 'Impossible d\'initialiser la variable d\'environnement', - 'tag_created' => 'Tag créé par PHPCI: %s', + 'tag_created' => 'Tag créé par PHPCI : %s', 'x_built_at_x' => '%PROJECT_TITLE% construit à %BUILD_URI%', 'hipchat_settings' => 'Merci de définir une "room" et un "authToken" pour le plugin hipchat_notify', 'irc_settings' => 'Vous devez configurer un serveur, une "room" et un "nick".', From 353c4cafdb3a4f49914821cac2082ebd7f488540 Mon Sep 17 00:00:00 2001 From: Adirelle Date: Thu, 8 Jan 2015 16:48:28 +0100 Subject: [PATCH 069/329] Remove duplicates from the list of recipients in the email plugin. Closes #731 --- PHPCI/Plugin/Email.php | 2 +- Tests/PHPCI/Plugin/EmailTest.php | 57 ++++++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/PHPCI/Plugin/Email.php b/PHPCI/Plugin/Email.php index 98b226e3..7c5171d6 100644 --- a/PHPCI/Plugin/Email.php +++ b/PHPCI/Plugin/Email.php @@ -182,7 +182,7 @@ class Email implements \PHPCI\Plugin $addresses[] = $this->options['default_mailto_address']; return $addresses; } - return $addresses; + return array_unique($addresses); } /** diff --git a/Tests/PHPCI/Plugin/EmailTest.php b/Tests/PHPCI/Plugin/EmailTest.php index 87225772..0fa2d8f0 100644 --- a/Tests/PHPCI/Plugin/EmailTest.php +++ b/Tests/PHPCI/Plugin/EmailTest.php @@ -57,6 +57,10 @@ class EmailTest extends \PHPUnit_Framework_TestCase ->method('getStatus') ->will($this->returnValue(\PHPCI\Model\Build::STATUS_SUCCESS)); + $this->mockBuild->expects($this->any()) + ->method('getCommitterEmail') + ->will($this->returnValue("committer@test.com")); + $this->mockCiBuilder = $this->getMock( '\PHPCI\Builder', array( @@ -151,8 +155,53 @@ class EmailTest extends \PHPUnit_Framework_TestCase ); $this->assertEquals($expectedReturn, $returnValue); + } + /** + * @covers PHPUnit::execute + */ + public function testExecute_UniqueRecipientsFromWithCommitter() + { + $this->loadEmailPluginWithOptions( + array( + 'addresses' => array('test-receiver@example.com', 'test-receiver2@example.com') + ) + ); + $actualMails = []; + $this->catchMailPassedToSend($actualMails); + + $returnValue = $this->testedEmailPlugin->execute(); + $this->assertTrue($returnValue); + + $this->assertCount(2, $actualMails); + + $actualTos = array(key($actualMails[0]->getTo()), key($actualMails[1]->getTo())); + $this->assertContains('test-receiver@example.com', $actualTos); + $this->assertContains('test-receiver2@example.com', $actualTos); + } + + /** + * @covers PHPUnit::execute + */ + public function testExecute_UniqueRecipientsWithCommiter() + { + $this->loadEmailPluginWithOptions( + array( + 'commiter' => true, + 'addresses' => array('test-receiver@example.com', 'committer@test.com') + ) + ); + + $actualMails = []; + $this->catchMailPassedToSend($actualMails); + + $returnValue = $this->testedEmailPlugin->execute(); + $this->assertTrue($returnValue); + + $actualTos = array(key($actualMails[0]->getTo()), key($actualMails[1]->getTo())); + $this->assertContains('test-receiver@example.com', $actualTos); + $this->assertContains('committer@test.com', $actualTos); } /** @@ -215,12 +264,16 @@ class EmailTest extends \PHPUnit_Framework_TestCase */ protected function catchMailPassedToSend(&$actualMail) { - $this->mockMailer->expects($this->once()) + $this->mockMailer->expects(is_array($actualMail) ? $this->atLeast(1) : $this->once()) ->method('send') ->will( $this->returnCallback( function ($passedMail) use (&$actualMail) { - $actualMail = $passedMail; + if(is_array($actualMail)) { + $actualMail[] = $passedMail; + } else { + $actualMail = $passedMail; + } return array(); } ) From e531c80718e635c1bb0a838bc9e81aac093f686e Mon Sep 17 00:00:00 2001 From: Daniel Seif Date: Mon, 5 Jan 2015 00:53:40 +0100 Subject: [PATCH 070/329] Reset the status of a duplicated build to avoid strict-mode errors. When using SQL strict mode, mysql would complain that no default value of the status column is set. Setting the status to 0 before duplicating fixes this. Closes #725 --- PHPCI/Service/BuildService.php | 1 + 1 file changed, 1 insertion(+) diff --git a/PHPCI/Service/BuildService.php b/PHPCI/Service/BuildService.php index b4721779..ca2336c8 100644 --- a/PHPCI/Service/BuildService.php +++ b/PHPCI/Service/BuildService.php @@ -100,6 +100,7 @@ class BuildService $build = new Build(); $build->setValues($data); $build->setCreated(new \DateTime()); + $build->setStatus(0); return $this->buildStore->save($build); } From 4a1d62b44aff9cb48464828f6fbd53bacd2f5795 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Tue, 3 Feb 2015 11:04:55 +0000 Subject: [PATCH 071/329] Make build log and meta value columns use MEDIUMTEXT data type. Closes #777 --- .../20150203105015_fix_column_types.php | 28 +++++ composer.lock | 107 +++++------------- 2 files changed, 59 insertions(+), 76 deletions(-) create mode 100644 PHPCI/Migrations/20150203105015_fix_column_types.php diff --git a/PHPCI/Migrations/20150203105015_fix_column_types.php b/PHPCI/Migrations/20150203105015_fix_column_types.php new file mode 100644 index 00000000..9a693594 --- /dev/null +++ b/PHPCI/Migrations/20150203105015_fix_column_types.php @@ -0,0 +1,28 @@ +table('build'); + $build->changeColumn('log', 'text', array( + 'null' => true, + 'default' => '', + 'limit' => MysqlAdapter::TEXT_MEDIUM, + )); + + // Update the build meta value column to MEDIUMTEXT: + $buildMeta = $this->table('build_meta'); + $buildMeta->changeColumn('meta_value', 'text', array( + 'null' => false, + 'limit' => MysqlAdapter::TEXT_MEDIUM, + )); + } +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index a65ea8fd..4ec183ff 100644 --- a/composer.lock +++ b/composer.lock @@ -255,21 +255,20 @@ }, { "name": "robmorgan/phinx", - "version": "v0.4.1", + "version": "v0.4.2.1", "source": { "type": "git", "url": "https://github.com/robmorgan/phinx.git", - "reference": "357210707c000f50edea802d84b74724ad122478" + "reference": "1bc1396392d4073b8b29ee5289e445889cbc12b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/robmorgan/phinx/zipball/357210707c000f50edea802d84b74724ad122478", - "reference": "357210707c000f50edea802d84b74724ad122478", + "url": "https://api.github.com/repos/robmorgan/phinx/zipball/1bc1396392d4073b8b29ee5289e445889cbc12b5", + "reference": "1bc1396392d4073b8b29ee5289e445889cbc12b5", "shasum": "" }, "require": { "php": ">=5.3.2", - "symfony/class-loader": "~2.6.0", "symfony/config": "~2.6.0", "symfony/console": "~2.6.0", "symfony/yaml": "~2.6.0" @@ -283,8 +282,8 @@ ], "type": "library", "autoload": { - "psr-0": { - "Phinx": "src/" + "psr-4": { + "Phinx\\": "src/Phinx" } }, "notification-url": "https://packagist.org/downloads/", @@ -297,6 +296,12 @@ "email": "robbym@gmail.com", "homepage": "http://robmorgan.id.au", "role": "Lead Developer" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com", + "homepage": "http://shadowhand.me", + "role": "Developer" } ], "description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.", @@ -308,7 +313,7 @@ "migrations", "phinx" ], - "time": "2014-12-23 06:06:14" + "time": "2015-02-08 03:41:44" }, { "name": "swiftmailer/swiftmailer", @@ -362,56 +367,6 @@ ], "time": "2014-12-05 14:17:14" }, - { - "name": "symfony/class-loader", - "version": "v2.6.4", - "target-dir": "Symfony/Component/ClassLoader", - "source": { - "type": "git", - "url": "https://github.com/symfony/ClassLoader.git", - "reference": "deac802f76910708ab50d039806cfd1866895b52" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/ClassLoader/zipball/deac802f76910708ab50d039806cfd1866895b52", - "reference": "deac802f76910708ab50d039806cfd1866895b52", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "symfony/finder": "~2.0,>=2.0.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.6-dev" - } - }, - "autoload": { - "psr-0": { - "Symfony\\Component\\ClassLoader\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - }, - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - } - ], - "description": "Symfony ClassLoader Component", - "homepage": "http://symfony.com", - "time": "2015-01-05 14:28:40" - }, { "name": "symfony/config", "version": "v2.6.4", @@ -761,27 +716,27 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "38743b677965c48a637097b2746a281264ae2347" + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/38743b677965c48a637097b2746a281264ae2347", - "reference": "38743b677965c48a637097b2746a281264ae2347", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "phpunit/phpunit": "3.7.*@stable" + "phpunit/phpunit": "~4.0" }, "suggest": { - "dflydev/markdown": "1.0.*", - "erusev/parsedown": "~0.7" + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" }, "type": "library", "extra": { @@ -806,7 +761,7 @@ "email": "mike.vanriel@naenius.com" } ], - "time": "2014-08-09 10:27:07" + "time": "2015-02-03 12:10:50" }, { "name": "phploc/phploc", @@ -1274,16 +1229,16 @@ }, { "name": "phpunit/phpunit", - "version": "4.4.5", + "version": "4.5.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "2e8580deebb7d1ac92ac878595e6bffe01069c2a" + "reference": "5b578d3865a9128b9c209b011fda6539ec06e7a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2e8580deebb7d1ac92ac878595e6bffe01069c2a", - "reference": "2e8580deebb7d1ac92ac878595e6bffe01069c2a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5b578d3865a9128b9c209b011fda6539ec06e7a5", + "reference": "5b578d3865a9128b9c209b011fda6539ec06e7a5", "shasum": "" }, "require": { @@ -1293,17 +1248,17 @@ "ext-reflection": "*", "ext-spl": "*", "php": ">=5.3.3", + "phpspec/prophecy": "~1.3.1", "phpunit/php-code-coverage": "~2.0", "phpunit/php-file-iterator": "~1.3.2", "phpunit/php-text-template": "~1.2", "phpunit/php-timer": "~1.0.2", "phpunit/phpunit-mock-objects": "~2.3", - "sebastian/comparator": "~1.0", + "sebastian/comparator": "~1.1", "sebastian/diff": "~1.1", - "sebastian/environment": "~1.1", - "sebastian/exporter": "~1.1", + "sebastian/environment": "~1.2", + "sebastian/exporter": "~1.2", "sebastian/global-state": "~1.0", - "sebastian/recursion-context": "~1.0", "sebastian/version": "~1.0", "symfony/yaml": "~2.0" }, @@ -1316,7 +1271,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4.x-dev" + "dev-master": "4.5.x-dev" } }, "autoload": { @@ -1342,7 +1297,7 @@ "testing", "xunit" ], - "time": "2015-01-27 16:06:15" + "time": "2015-02-05 15:51:19" }, { "name": "phpunit/phpunit-mock-objects", From f0d53638dcfd60ad23c09f0fbd10943e50e2e645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=CC=80lex=20Corretge=CC=81?= Date: Mon, 9 Feb 2015 10:00:42 +0100 Subject: [PATCH 072/329] Allow modification of PHPCI variables without modifying versioned code. Closes #786 Closes #781 --- .gitignore | 1 + bootstrap.php | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/.gitignore b/.gitignore index fbfc1a28..79784a60 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ PHPCI/Model/Migration.php PHPCI/Model/Base/MigrationBase.php PHPCI/Store/MigrationStore.php PHPCI/Store/Base/MigrationStoreBase.php +local_vars.php diff --git a/bootstrap.php b/bootstrap.php index 89d7c95b..b15a7e59 100755 --- a/bootstrap.php +++ b/bootstrap.php @@ -62,6 +62,17 @@ if (file_exists($configFile)) { $config->loadYaml($configFile); } +/** + * Allow to modify PHPCI configuration without modify versioned code. + * Dameons should be killed to apply changes in the file. + * + * @ticket 781 + */ +$localVarsFile = dirname(__FILE__) . '/local_vars.php'; +if (is_readable($localVarsFile)) { + require_once $localVarsFile; +} + require_once(dirname(__FILE__) . '/vars.php'); \PHPCI\Helper\Lang::init($config); From 935ffe447377e4fb0f6c5708e44766e10b5f767a Mon Sep 17 00:00:00 2001 From: Luca Date: Sun, 8 Feb 2015 16:36:14 +0100 Subject: [PATCH 073/329] Some Italian language translation improvements. Closes #784 --- PHPCI/Languages/lang.it.php | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/PHPCI/Languages/lang.it.php b/PHPCI/Languages/lang.it.php index 7134c488..e4b531e0 100644 --- a/PHPCI/Languages/lang.it.php +++ b/PHPCI/Languages/lang.it.php @@ -16,20 +16,19 @@ $strings = array( 'log_in_to_phpci' => 'Accedi a PHPCI', 'login_error' => 'Indirizzo email o password errati', 'forgotten_password_link' => 'Hai dimenticato la tua password?', - 'reset_emailed' => 'Ti abbiamo inviato un link per ripristinare la tua password via email.', + 'reset_emailed' => 'Ti abbiamo inviato un link via email per ripristinare la tua password.', 'reset_header' => 'Non preoccuparti!
    E\' sufficiente inserire il tuo indirizzo email di seguito e ti invieremo una email con il link per il ripristino della tua password.', 'reset_email_address' => 'Inserisci il tuo indirizzo email:', 'reset_send_email' => 'Invia il link di reset della password', 'reset_enter_password' => 'Per favore inserisci la nuova password', 'reset_new_password' => 'Nuova password:', 'reset_change_password' => 'Cambia password', - 'reset_no_user_exists' => 'No user exists with that email address, please try again.', 'reset_no_user_exists' => 'Non esiste nessun utente con questo indirizzo email, per favore prova ancora.', 'reset_email_body' => 'Ciao %s, hai ricevuto questa email perché tu, o qualcun\'altro, ha richiesto un reset della password per PHPCI. -Se questa mail è tua, per favore apri il seguente link per resettare la tua password: %ssession/reset-password/%d/%s +Se questa mail è tua, per favore apri il seguente link per ripristinare la tua password: %ssession/reset-password/%d/%s altrimenti, per favore, ignora questa email e nessuna azione verrà intrapresa. @@ -113,7 +112,7 @@ PHPCI', (se non puoi aggiungere il file phpci.yml nel repository di questo progetto)', 'default_branch' => 'Nome del branch di default', 'allow_public_status' => 'Vuoi rendere pubblica la pagina dello stato e l\'immagine per questo progetto?', - 'save_project' => 'Salca il Progetto', + 'save_project' => 'Salva il Progetto', 'error_mercurial' => 'L\'URL del repository Mercurial URL deve iniziare con http:// o https://', 'error_remote' => 'L\'URL del repository deve iniziare con git://, http:// o https://', @@ -137,16 +136,16 @@ PHPCI', 'delete_build' => 'Rimuovi build', 'webhooks' => 'Webhooks', - 'webhooks_help_github' => 'Per efettuare la build automatica di questo progetto quando vengono pushati nuovi commit, + 'webhooks_help_github' => 'Per effettuare la build automatica di questo progetto quando vengono inseriti nuovi commit, aggiungi l\'URL seguente come "Webhook" nella sezione
    Webhooks and Services del tuo repository su GitHub.', - 'webhooks_help_gitlab' => 'Per efettuare la build automatica di questo progetto quando vengono pushati nuovi commit, + 'webhooks_help_gitlab' => 'Per effettuare la build automatica di questo progetto quando vengono inseriti nuovi commit, aggiungi l\'URL seguente come "Webhook URL" nella sezione "WebHook URL" del tuo repository GitLab.', - 'webhooks_help_bitbucket' => 'Per efettuare la build automatica di questo progetto quando vengono pushati nuovi + 'webhooks_help_bitbucket' => 'Per effettuare la build automatica di questo progetto quando vengono inseriti nuovi commit, aggiungi l\'URL seguente come serizio "POST" nella sezione Services del tuo repository su BITBUCKET.', @@ -157,7 +156,7 @@ PHPCI', 'rebuild_now' => 'Esegui nuovamente la build ora', - 'committed_by_x' => 'Committato da %s', + 'committed_by_x' => 'Inviato da %s', 'commit_id_x' => 'Commit: %s', 'chart_display' => 'Questo grafico verrà mostrato una volta terminata la build.', @@ -169,9 +168,9 @@ PHPCI', 'logical_lines' => 'Linee di logica', 'lines_of_code' => 'Linee di codice', 'build_log' => 'Log della build', - 'quality_trend' => 'Trand della qualità', - 'phpmd_warnings' => 'Warning di PHPMD', - 'phpcs_warnings' => 'Warning di PHPCS', + 'quality_trend' => 'Trend della qualità', + 'phpmd_warnings' => 'Avvisi di PHPMD', + 'phpcs_warnings' => 'Avvisi di PHPCS', 'phpcs_errors' => 'Errori di PHPCS', 'phplint_errors' => 'Errori di Lint', 'phpunit_errors' => 'Errori di PHPUnit', @@ -228,16 +227,16 @@ PHPCI', 'settings_github_linked' => 'Il tuo account GitHub è stato collegato.', 'settings_github_not_linked' => 'Il tuo account GitHub non può essere collegato.', 'build_settings' => 'Configurazioni della build', - 'github_application' => 'Applicatzione GitHub', + 'github_application' => 'Applicazione GitHub', 'github_sign_in' => 'Prima di poter iniziare ad usare GitHub, è necessario collegarsi e garantire a PHPCI l\'accesso al tuo account.', 'github_phpci_linked' => 'PHPCI è stato collegato correttamente al tuo account GitHub.', 'github_where_to_find' => 'Dove trovare queste...', - 'github_where_help' => 'Se sei il proprietario dell\'applicazione, puoi trovare queste informazioni nell\'are delle + 'github_where_help' => 'Se sei il proprietario dell\'applicazione, puoi trovare queste informazioni nell\'area delle configurazioni dell\'applicazione.', 'email_settings' => 'Impostazioni Email', - 'email_settings_help' => 'Prima che possa inviare le email con lo status PHPCI, devi configurare l\'SMTP qio sotto.', + 'email_settings_help' => 'Prima che possa inviare le email con lo status PHPCI, devi configurare l\'SMTP qui sotto.', 'application_id' => 'ID dell\'Applicazione', 'application_secret' => 'Secret dell\'Applicazione', @@ -263,7 +262,7 @@ PHPCI', // Plugins 'cannot_update_composer' => 'PHPCI non può aggiornare composer.json per te non essendo scrivibile.', 'x_has_been_removed' => '%s è stato rimosso.', - 'x_has_been_added' => '%s è stato aggiunto al composer.json per te verrà installato la prossima volta che eseguirai + 'x_has_been_added' => '%s è stato aggiunto al file composer.json per te, verrà installato la prossima volta che eseguirai composer update.', 'enabled_plugins' => 'Plugins attivati', 'provided_by_package' => 'Fornito dal pacchetto', @@ -338,7 +337,7 @@ PHPCI', 'finding_builds' => 'Ricerca delel build da processare', 'found_n_builds' => 'Trovate %d build', 'skipping_build' => 'Saltata la build %d - La build del progetto è già in corso.', - 'marked_as_failed' => 'Build %d è satata contrassegnata come come fallita per un timeout.', + 'marked_as_failed' => 'Build %d è stata contrassegnata come fallita per un timeout.', // Builder 'missing_phpci_yml' => 'Questo progetto non contiene il file phpci.yml, o il file è vuoto.', From 4f73063fb7d9f25b190526fdc1093aeaee668abc Mon Sep 17 00:00:00 2001 From: Lee Willis Date: Sun, 8 Feb 2015 10:24:27 +0000 Subject: [PATCH 074/329] Redesigned build summary for the dashboard to include recent builds. Closes #783 Closes #708 --- PHPCI/View/SummaryTable.phtml | 96 +++++++++++++++++++-------- public/assets/css/AdminLTE-custom.css | 24 +++++++ 2 files changed, 93 insertions(+), 27 deletions(-) diff --git a/PHPCI/View/SummaryTable.phtml b/PHPCI/View/SummaryTable.phtml index 10782a5b..56df6e38 100644 --- a/PHPCI/View/SummaryTable.phtml +++ b/PHPCI/View/SummaryTable.phtml @@ -3,17 +3,32 @@ use PHPCI\Helper\Lang; foreach($projects as $project): $statuses = array(); - $successes = 0; $failures = 0; - $subcls = ''; + $subcls = 'yellow'; $cls = ''; $success = null; $failure = null; if (count($builds[$project->getId()])) { + // Get the most recent build status to determine the main block colour. + $last_build = $builds[$project->getId()][0]; + $status = $last_build->getStatus(); + switch($status) { + case 0: + $subcls = 'blue'; + break; + case 1: + $subcls = 'yellow'; + break; + case 2: + $subcls = 'green'; + break; + case 3: + $subcls = 'red'; + break; + } // Use the last 5 builds to determine project health: - $successes = 0; $failures = 0; foreach ($builds[$project->getId()] as $build) { @@ -25,7 +40,6 @@ foreach($projects as $project): $statuses[] = 'running'; break; case 2: - $successes++; $statuses[] = 'ok'; $success = is_null($success) && !is_null($build->getFinished()) ? Lang::formatDateTime($build->getFinished()) : $success; break; @@ -38,15 +52,6 @@ foreach($projects as $project): } } - if ($failures == 0) { - $subcls = 'green'; - } elseif ($successes == 0) { - $subcls = 'red'; - } else { - $subcls = 'yellow'; - } - - $buildCount = count($builds[$project->getId()]); $lastSuccess = $successful[$project->getId()]; $lastFailure = $failed[$project->getId()]; @@ -77,24 +82,31 @@ foreach($projects as $project): ?> + 10) { + $containerClass = 'small-box-minimal'; + } else { + $containerClass = 'small-box-full'; + } +?> + +
    + 10): ?> -
    -
    -

    - - - getTitle(); ?> - - - - -

    -
    +
    +

    + + + getTitle(); ?> + + - + +

    -
    + + getId()][$idx])) { + echo ''; + } else { + $build = $builds[$project->getId()][$idx]; + $link = PHPCI_URL . 'build/view/' . $build->id; + switch ($build->getStatus()) { + case 0: + $class = 'bg-blue'; + $icon = 'fa-minus'; + break; + case 1: + $class = 'bg-yellow'; + $icon = 'fa-clock-o'; + break; + case 2: + $class = 'bg-green'; + $icon = 'fa-check'; + break; + case 3: + $class = 'bg-red'; + $icon = 'fa-times'; + break; + } + echo ''; + } + } ?> +
    +
    + diff --git a/public/assets/css/AdminLTE-custom.css b/public/assets/css/AdminLTE-custom.css index 009a360b..a0fb3dd7 100644 --- a/public/assets/css/AdminLTE-custom.css +++ b/public/assets/css/AdminLTE-custom.css @@ -51,3 +51,27 @@ overflow: hidden; text-overflow: ellipsis; } + +.small-box > .inner { + border-bottom: 1px solid rgba(0, 0, 0, 0.15); +} + +.small-box > .small-box-footer-project { + width: 60%; + float: left; +} +.small-box > .small-box-footer-build { + width: 8%; + float: left; +} +.small-box-minimal > .inner { + width: 71%; + float: left; +} +.small-box-minimal > .small-box-footer-build { + width: 5%; + border: 1px solid rgba(0, 0, 0, 0.15); + margin-left: 1px; + margin-top: 19px; + font-size: 11px; +} \ No newline at end of file From 4d91bd15e73329a0db3c0cc759061383854f2268 Mon Sep 17 00:00:00 2001 From: vigo5190 Date: Wed, 14 Jan 2015 14:06:32 +0300 Subject: [PATCH 075/329] Update Slack plugin to support the latest version of the library it relies on. Closes #742 Closes #747 --- PHPCI/Plugin/SlackNotify.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/PHPCI/Plugin/SlackNotify.php b/PHPCI/Plugin/SlackNotify.php index 229715ea..1e1e3961 100644 --- a/PHPCI/Plugin/SlackNotify.php +++ b/PHPCI/Plugin/SlackNotify.php @@ -1,7 +1,6 @@ icon = $options['icon']; } - } else { throw new \Exception('Please define the webhook_url for slack_notify plugin!'); } @@ -91,9 +89,9 @@ class SlackNotify implements \PHPCI\Plugin // Build up the attachment data $attachment = new \Maknz\Slack\Attachment(array( 'fallback' => $message, - 'pretext' => $message, - 'color' => $color, - 'fields' => array( + 'pretext' => $message, + 'color' => $color, + 'fields' => array( new \Maknz\Slack\AttachmentField(array( 'title' => 'Status', 'value' => $status, @@ -104,23 +102,25 @@ class SlackNotify implements \PHPCI\Plugin $client = new \Maknz\Slack\Client($this->webHook); + $message = $client->createMessage(); + if (!empty($this->room)) { - $client->setChannel($this->room); + $message->setChannel($this->room); } if (!empty($this->username)) { - $client->setUsername($this->username); + $message->setUsername($this->username); } if (!empty($this->icon)) { - $client->setIcon($this->icon); + $message->setIcon($this->icon); } - $client->attach($attachment); + $message->attach($attachment); $success = true; - $client->send(''); // FIXME: Handle errors + $message->send(''); // FIXME: Handle errors return $success; } From 9379da13939b60cecd525977e7e4fc0439c11d69 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 12 Feb 2015 10:39:41 +0000 Subject: [PATCH 076/329] Fixing SensioLabs Insight 'Critical' violations. --- PHPCI/Command/DaemoniseCommand.php | 3 ++- PHPCI/Helper/WindowsCommandExecutor.php | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/PHPCI/Command/DaemoniseCommand.php b/PHPCI/Command/DaemoniseCommand.php index 915890bd..0bcd42f6 100644 --- a/PHPCI/Command/DaemoniseCommand.php +++ b/PHPCI/Command/DaemoniseCommand.php @@ -87,7 +87,8 @@ class DaemoniseCommand extends Command try { $buildCount = $runner->run($emptyInput, $output); } catch (\Exception $e) { - var_dump($e); + $output->writeln('Exception: ' . $e->getMessage() . ''); + $output->writeln('Line: ' . $e->getLine() . ' - File: ' . $e->getFile() . ''); } if (0 == $buildCount && $this->sleep < 15) { diff --git a/PHPCI/Helper/WindowsCommandExecutor.php b/PHPCI/Helper/WindowsCommandExecutor.php index aa01fea4..a1d0889f 100644 --- a/PHPCI/Helper/WindowsCommandExecutor.php +++ b/PHPCI/Helper/WindowsCommandExecutor.php @@ -22,6 +22,9 @@ class WindowsCommandExecutor extends BaseCommandExecutor */ protected function findGlobalBinary($binary) { - return trim(shell_exec('where ' . $binary)); + $command = sprintf('where %s', $binary); + $result = shell_exec($command); + + return trim($result); } } From d481140ea2a4ffc4a07449b78a972d8352e7466c Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 12 Feb 2015 11:08:45 +0000 Subject: [PATCH 077/329] Fixing issues related to LIMIT in Base Store files. --- PHPCI/Model/Base/ProjectBase.php | 76 ++++++++++++------------- PHPCI/Store/Base/BuildMetaStoreBase.php | 57 ++++--------------- PHPCI/Store/Base/BuildStoreBase.php | 57 ++++--------------- PHPCI/Store/Base/ProjectStoreBase.php | 35 ++---------- PHPCI/Store/Base/UserStoreBase.php | 20 ------- 5 files changed, 63 insertions(+), 182 deletions(-) diff --git a/PHPCI/Model/Base/ProjectBase.php b/PHPCI/Model/Base/ProjectBase.php index bcf793ab..305cafbf 100644 --- a/PHPCI/Model/Base/ProjectBase.php +++ b/PHPCI/Model/Base/ProjectBase.php @@ -38,11 +38,11 @@ class ProjectBase extends Model 'reference' => null, 'branch' => null, 'ssh_private_key' => null, - 'ssh_public_key' => null, 'type' => null, 'access_information' => null, 'last_commit' => null, 'build_config' => null, + 'ssh_public_key' => null, 'allow_public_status' => null, ); @@ -56,11 +56,11 @@ class ProjectBase extends Model 'reference' => 'getReference', 'branch' => 'getBranch', 'ssh_private_key' => 'getSshPrivateKey', - 'ssh_public_key' => 'getSshPublicKey', 'type' => 'getType', 'access_information' => 'getAccessInformation', 'last_commit' => 'getLastCommit', 'build_config' => 'getBuildConfig', + 'ssh_public_key' => 'getSshPublicKey', 'allow_public_status' => 'getAllowPublicStatus', // Foreign key getters: @@ -76,11 +76,11 @@ class ProjectBase extends Model 'reference' => 'setReference', 'branch' => 'setBranch', 'ssh_private_key' => 'setSshPrivateKey', - 'ssh_public_key' => 'setSshPublicKey', 'type' => 'setType', 'access_information' => 'setAccessInformation', 'last_commit' => 'setLastCommit', 'build_config' => 'setBuildConfig', + 'ssh_public_key' => 'setSshPublicKey', 'allow_public_status' => 'setAllowPublicStatus', // Foreign key setters: @@ -117,11 +117,6 @@ class ProjectBase extends Model 'nullable' => true, 'default' => null, ), - 'ssh_public_key' => array( - 'type' => 'text', - 'nullable' => true, - 'default' => null, - ), 'type' => array( 'type' => 'varchar', 'length' => 50, @@ -144,6 +139,11 @@ class ProjectBase extends Model 'nullable' => true, 'default' => null, ), + 'ssh_public_key' => array( + 'type' => 'text', + 'nullable' => true, + 'default' => null, + ), 'allow_public_status' => array( 'type' => 'int', 'length' => 11, @@ -224,18 +224,6 @@ class ProjectBase extends Model return $rtn; } - /** - * Get the value of SshPublicKey / ssh_public_key. - * - * @return string - */ - public function getSshPublicKey() - { - $rtn = $this->data['ssh_public_key']; - - return $rtn; - } - /** * Get the value of Type / type. * @@ -284,6 +272,18 @@ class ProjectBase extends Model return $rtn; } + /** + * Get the value of SshPublicKey / ssh_public_key. + * + * @return string + */ + public function getSshPublicKey() + { + $rtn = $this->data['ssh_public_key']; + + return $rtn; + } + /** * Get the value of AllowPublicStatus / allow_public_status. * @@ -394,24 +394,6 @@ class ProjectBase extends Model $this->_setModified('ssh_private_key'); } - /** - * Set the value of SshPublicKey / ssh_public_key. - * - * @param $value string - */ - public function setSshPublicKey($value) - { - $this->_validateString('SshPublicKey', $value); - - if ($this->data['ssh_public_key'] === $value) { - return; - } - - $this->data['ssh_public_key'] = $value; - - $this->_setModified('ssh_public_key'); - } - /** * Set the value of Type / type. * @@ -486,6 +468,24 @@ class ProjectBase extends Model $this->_setModified('build_config'); } + /** + * Set the value of SshPublicKey / ssh_public_key. + * + * @param $value string + */ + public function setSshPublicKey($value) + { + $this->_validateString('SshPublicKey', $value); + + if ($this->data['ssh_public_key'] === $value) { + return; + } + + $this->data['ssh_public_key'] = $value; + + $this->_setModified('ssh_public_key'); + } + /** * Set the value of AllowPublicStatus / allow_public_status. * diff --git a/PHPCI/Store/Base/BuildMetaStoreBase.php b/PHPCI/Store/Base/BuildMetaStoreBase.php index 36a94f9d..6c4cfc15 100644 --- a/PHPCI/Store/Base/BuildMetaStoreBase.php +++ b/PHPCI/Store/Base/BuildMetaStoreBase.php @@ -20,24 +20,11 @@ class BuildMetaStoreBase extends Store protected $modelName = '\PHPCI\Model\BuildMeta'; protected $primaryKey = 'id'; - /** - * Get a BuildMeta by primary key. - * @param mixed $value Primary key. - * @param string $useConnection Connection to use (read / write) - * @return \PHPCI\Model\BuildMeta|null - */ public function getByPrimaryKey($value, $useConnection = 'read') { return $this->getById($value, $useConnection); } - /** - * Get a BuildMeta by Id. - * @param mixed $value. - * @param string $useConnection Connection to use (read / write) - * @throws \b8\Exception\HttpException - * @return \PHPCI\Model\BuildMeta|null; - */ public function getById($value, $useConnection = 'read') { if (is_null($value)) { @@ -57,31 +44,17 @@ class BuildMetaStoreBase extends Store return null; } - /** - * Get an array of BuildMeta by ProjectId. - * @param mixed $value. - * @param int $limit - * @param string $useConnection Connection to use (read / write) - * @throws \b8\Exception\HttpException - * @return \PHPCI\Model\BuildMeta[] - */ - public function getByProjectId($value, $limit = null, $useConnection = 'read') + public function getByProjectId($value, $limit = 1000, $useConnection = 'read') { if (is_null($value)) { throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } - $add = ''; - if ($limit) { - $add .= ' LIMIT ' . $limit; - } - - $count = null; - - $query = 'SELECT * FROM `build_meta` WHERE `project_id` = :project_id' . $add; + $query = 'SELECT * FROM `build_meta` WHERE `project_id` = :project_id LIMIT :limit'; $stmt = Database::getConnection($useConnection)->prepare($query); $stmt->bindValue(':project_id', $value); + $stmt->bindValue(':limit', (int)$limit, \PDO::PARAM_INT); if ($stmt->execute()) { $res = $stmt->fetchAll(\PDO::FETCH_ASSOC); @@ -91,37 +64,25 @@ class BuildMetaStoreBase extends Store }; $rtn = array_map($map, $res); + $count = count($rtn); + return array('items' => $rtn, 'count' => $count); } else { return array('items' => array(), 'count' => 0); } } - /** - * Get an array of BuildMeta by BuildId. - * @param mixed $value. - * @param int $limit - * @param string $useConnection Connection to use (read / write) - * @throws \b8\Exception\HttpException - * @return \PHPCI\Model\BuildMeta[] - */ - public function getByBuildId($value, $limit = null, $useConnection = 'read') + public function getByBuildId($value, $limit = 1000, $useConnection = 'read') { if (is_null($value)) { throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } - $add = ''; - if ($limit) { - $add .= ' LIMIT ' . $limit; - } - - $count = null; - - $query = 'SELECT * FROM `build_meta` WHERE `build_id` = :build_id' . $add; + $query = 'SELECT * FROM `build_meta` WHERE `build_id` = :build_id LIMIT :limit'; $stmt = Database::getConnection($useConnection)->prepare($query); $stmt->bindValue(':build_id', $value); + $stmt->bindValue(':limit', (int)$limit, \PDO::PARAM_INT); if ($stmt->execute()) { $res = $stmt->fetchAll(\PDO::FETCH_ASSOC); @@ -131,6 +92,8 @@ class BuildMetaStoreBase extends Store }; $rtn = array_map($map, $res); + $count = count($rtn); + return array('items' => $rtn, 'count' => $count); } else { return array('items' => array(), 'count' => 0); diff --git a/PHPCI/Store/Base/BuildStoreBase.php b/PHPCI/Store/Base/BuildStoreBase.php index cb1b2824..89d3a82f 100644 --- a/PHPCI/Store/Base/BuildStoreBase.php +++ b/PHPCI/Store/Base/BuildStoreBase.php @@ -20,24 +20,11 @@ class BuildStoreBase extends Store protected $modelName = '\PHPCI\Model\Build'; protected $primaryKey = 'id'; - /** - * Get a Build by primary key. - * @param mixed $value Primary key. - * @param string $useConnection Connection to use (read / write) - * @return \PHPCI\Model\Build|null - */ public function getByPrimaryKey($value, $useConnection = 'read') { return $this->getById($value, $useConnection); } - /** - * Get a Build by Id. - * @param mixed $value. - * @param string $useConnection Connection to use (read / write) - * @throws \b8\Exception\HttpException - * @return \PHPCI\Model\Build|null; - */ public function getById($value, $useConnection = 'read') { if (is_null($value)) { @@ -57,31 +44,17 @@ class BuildStoreBase extends Store return null; } - /** - * Get an array of Build by ProjectId. - * @param mixed $value. - * @param int $limit - * @param string $useConnection Connection to use (read / write) - * @throws \b8\Exception\HttpException - * @return \PHPCI\Model\Build[] - */ - public function getByProjectId($value, $limit = null, $useConnection = 'read') + public function getByProjectId($value, $limit = 1000, $useConnection = 'read') { if (is_null($value)) { throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } - $add = ''; - if ($limit) { - $add .= ' LIMIT ' . $limit; - } - - $count = null; - - $query = 'SELECT * FROM `build` WHERE `project_id` = :project_id' . $add; + $query = 'SELECT * FROM `build` WHERE `project_id` = :project_id LIMIT :limit'; $stmt = Database::getConnection($useConnection)->prepare($query); $stmt->bindValue(':project_id', $value); + $stmt->bindValue(':limit', (int)$limit, \PDO::PARAM_INT); if ($stmt->execute()) { $res = $stmt->fetchAll(\PDO::FETCH_ASSOC); @@ -91,37 +64,25 @@ class BuildStoreBase extends Store }; $rtn = array_map($map, $res); + $count = count($rtn); + return array('items' => $rtn, 'count' => $count); } else { return array('items' => array(), 'count' => 0); } } - /** - * Get an array of Build by Status. - * @param mixed $value. - * @param int $limit - * @param string $useConnection Connection to use (read / write) - * @throws \b8\Exception\HttpException - * @return \PHPCI\Model\Build[] - */ - public function getByStatus($value, $limit = null, $useConnection = 'read') + public function getByStatus($value, $limit = 1000, $useConnection = 'read') { if (is_null($value)) { throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } - $add = ''; - if ($limit) { - $add .= ' LIMIT ' . $limit; - } - - $count = null; - - $query = 'SELECT * FROM `build` WHERE `status` = :status' . $add; + $query = 'SELECT * FROM `build` WHERE `status` = :status LIMIT :limit'; $stmt = Database::getConnection($useConnection)->prepare($query); $stmt->bindValue(':status', $value); + $stmt->bindValue(':limit', (int)$limit, \PDO::PARAM_INT); if ($stmt->execute()) { $res = $stmt->fetchAll(\PDO::FETCH_ASSOC); @@ -131,6 +92,8 @@ class BuildStoreBase extends Store }; $rtn = array_map($map, $res); + $count = count($rtn); + return array('items' => $rtn, 'count' => $count); } else { return array('items' => array(), 'count' => 0); diff --git a/PHPCI/Store/Base/ProjectStoreBase.php b/PHPCI/Store/Base/ProjectStoreBase.php index 02692f62..e5a01cdd 100644 --- a/PHPCI/Store/Base/ProjectStoreBase.php +++ b/PHPCI/Store/Base/ProjectStoreBase.php @@ -20,24 +20,11 @@ class ProjectStoreBase extends Store protected $modelName = '\PHPCI\Model\Project'; protected $primaryKey = 'id'; - /** - * Get a Project by primary key. - * @param mixed $value Primary key. - * @param string $useConnection Connection to use (read / write) - * @return \PHPCI\Model\Project|null - */ public function getByPrimaryKey($value, $useConnection = 'read') { return $this->getById($value, $useConnection); } - /** - * Get a Project by Id. - * @param mixed $value. - * @param string $useConnection Connection to use (read / write) - * @throws \b8\Exception\HttpException - * @return \PHPCI\Model\Project|null; - */ public function getById($value, $useConnection = 'read') { if (is_null($value)) { @@ -57,31 +44,17 @@ class ProjectStoreBase extends Store return null; } - /** - * Get an array of Project by Title. - * @param mixed $value. - * @param int $limit - * @param string $useConnection Connection to use (read / write) - * @throws \b8\Exception\HttpException - * @return \PHPCI\Model\Project[] - */ - public function getByTitle($value, $limit = null, $useConnection = 'read') + public function getByTitle($value, $limit = 1000, $useConnection = 'read') { if (is_null($value)) { throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } - $add = ''; - if ($limit) { - $add .= ' LIMIT ' . $limit; - } - - $count = null; - - $query = 'SELECT * FROM `project` WHERE `title` = :title' . $add; + $query = 'SELECT * FROM `project` WHERE `title` = :title LIMIT :limit'; $stmt = Database::getConnection($useConnection)->prepare($query); $stmt->bindValue(':title', $value); + $stmt->bindValue(':limit', (int)$limit, \PDO::PARAM_INT); if ($stmt->execute()) { $res = $stmt->fetchAll(\PDO::FETCH_ASSOC); @@ -91,6 +64,8 @@ class ProjectStoreBase extends Store }; $rtn = array_map($map, $res); + $count = count($rtn); + return array('items' => $rtn, 'count' => $count); } else { return array('items' => array(), 'count' => 0); diff --git a/PHPCI/Store/Base/UserStoreBase.php b/PHPCI/Store/Base/UserStoreBase.php index 24d247e3..fd903d8e 100644 --- a/PHPCI/Store/Base/UserStoreBase.php +++ b/PHPCI/Store/Base/UserStoreBase.php @@ -20,24 +20,11 @@ class UserStoreBase extends Store protected $modelName = '\PHPCI\Model\User'; protected $primaryKey = 'id'; - /** - * Get a User by primary key. - * @param mixed $value Primary key. - * @param string $useConnection Connection to use (read / write) - * @return \PHPCI\Model\User|null - */ public function getByPrimaryKey($value, $useConnection = 'read') { return $this->getById($value, $useConnection); } - /** - * Get a User by Id. - * @param mixed $value. - * @param string $useConnection Connection to use (read / write) - * @throws \b8\Exception\HttpException - * @return \PHPCI\Model\User|null; - */ public function getById($value, $useConnection = 'read') { if (is_null($value)) { @@ -57,13 +44,6 @@ class UserStoreBase extends Store return null; } - /** - * Get a User by Email. - * @param mixed $value. - * @param string $useConnection Connection to use (read / write) - * @throws \b8\Exception\HttpException - * @return \PHPCI\Model\User|null; - */ public function getByEmail($value, $useConnection = 'read') { if (is_null($value)) { From a4339fc1b6dd7663dc01f16c6f9e05a332856ebe Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 12 Feb 2015 11:38:00 +0000 Subject: [PATCH 078/329] Fixing two particularly dodgy queries in BuildStore. --- PHPCI/Store/BuildStore.php | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/PHPCI/Store/BuildStore.php b/PHPCI/Store/BuildStore.php index 39ff7c78..96631e1e 100644 --- a/PHPCI/Store/BuildStore.php +++ b/PHPCI/Store/BuildStore.php @@ -30,13 +30,12 @@ class BuildStore extends BuildStoreBase */ public function getLatestBuilds($projectId = null, $limit = 5) { - $where = ''; - if (!is_null($projectId)) { - $where = ' WHERE `project_id` = :pid '; + $query = 'SELECT * FROM build WHERE `project_id` = :pid ORDER BY id DESC LIMIT :limit'; + } else { + $query = 'SELECT * FROM build ORDER BY id DESC LIMIT :limit'; } - $query = 'SELECT * FROM build '.$where.' ORDER BY id DESC LIMIT :limit'; $stmt = Database::getConnection('read')->prepare($query); if (!is_null($projectId)) { @@ -120,17 +119,26 @@ class BuildStore extends BuildStoreBase */ public function getMeta($key, $projectId, $buildId = null, $branch = null, $numResults = 1) { - $select = '`bm`.`build_id`, `bm`.`meta_key`, `bm`.`meta_value`'; - $and = $numResults > 1 ? ' AND (`bm`.`build_id` <= :buildId) ' : ' AND (`bm`.`build_id` = :buildId) '; - $where = '`bm`.`meta_key` = :key AND `bm`.`project_id` = :projectId ' . $and; - $from = ' `build_meta` AS `bm`'; + $query = 'SELECT bm.build_id, bm.meta_key, bm.meta_value + FROM build_meta AS bm + LEFT JOIN build b ON b.id = bm.build_id + WHERE bm.meta_key = :key + AND bm.project_id = :projectId'; - if ($branch !== null) { - $where .= ' AND `b`.`branch` = :branch AND `b`.`id`= `bm`.`build_id` '; - $from .= ', `build` AS `b`'; + // If we're getting comparative meta data, include previous builds + // otherwise just include the specified build ID: + if ($numResults > 1) { + $query .= ' AND bm.build_id <= :buildId '; + } else { + $query .= ' AND bm.build_id = :buildId '; } - $query = 'SELECT '.$select.' FROM '.$from.' WHERE '.$where.' ORDER BY `bm`.id DESC LIMIT :numResults'; + // Include specific branch information if required: + if (!is_null($branch)) { + $query .= ' AND b.branch = :branch '; + } + + $query .= ' ORDER BY bm.id DESC LIMIT :numResults'; $stmt = Database::getConnection('read')->prepare($query); $stmt->bindValue(':key', $key, \PDO::PARAM_STR); From e5cbccecb5bf8a0a5171390c033bf01a7858eeaf Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 12 Feb 2015 11:41:58 +0000 Subject: [PATCH 079/329] Getting rid of debug code --- public/index.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/public/index.php b/public/index.php index 94191794..4d68e76b 100644 --- a/public/index.php +++ b/public/index.php @@ -9,9 +9,6 @@ session_start(); -error_reporting(E_ALL); -ini_set('display_errors', 'on'); - require_once('../bootstrap.php'); $fc = new PHPCI\Application($config, new b8\Http\Request()); From 36e3c622c83e33413236a45e0b334abc4223365f Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 12 Feb 2015 11:42:09 +0000 Subject: [PATCH 080/329] Fixing missing use statements --- PHPCI/Controller.php | 1 + PHPCI/Logging/BuildDBLogHandler.php | 1 + PHPCI/Plugin/Atoum.php | 1 + 3 files changed, 3 insertions(+) diff --git a/PHPCI/Controller.php b/PHPCI/Controller.php index 5fd22594..4f941251 100644 --- a/PHPCI/Controller.php +++ b/PHPCI/Controller.php @@ -10,6 +10,7 @@ namespace PHPCI; use b8\Config; +use b8\Exception\HttpException\ForbiddenException; use b8\Http\Request; use b8\Http\Response; use b8\View; diff --git a/PHPCI/Logging/BuildDBLogHandler.php b/PHPCI/Logging/BuildDBLogHandler.php index bda8cfce..91664b25 100644 --- a/PHPCI/Logging/BuildDBLogHandler.php +++ b/PHPCI/Logging/BuildDBLogHandler.php @@ -12,6 +12,7 @@ namespace PHPCI\Logging; use b8\Store\Factory; use Monolog\Handler\AbstractProcessingHandler; use PHPCI\Model\Build; +use Psr\Log\LogLevel; /** * Class BuildDBLogHandler writes the build log to the database. diff --git a/PHPCI/Plugin/Atoum.php b/PHPCI/Plugin/Atoum.php index 8abc871e..877f009b 100644 --- a/PHPCI/Plugin/Atoum.php +++ b/PHPCI/Plugin/Atoum.php @@ -10,6 +10,7 @@ namespace PHPCI\Plugin; use PHPCI\Builder; +use PHPCI\Helper\Lang; use PHPCI\Model\Build; /** From 489f71b8c2d96e6304e11882aa55a823c70d4cfd Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 12 Feb 2015 12:37:56 +0000 Subject: [PATCH 081/329] Cleaning up unnecessary use of 'die' and 'exit' --- PHPCI/Command/InstallCommand.php | 10 ++- PHPCI/Command/PollCommand.php | 2 +- PHPCI/Command/UpdateCommand.php | 10 ++- PHPCI/Controller.php | 4 ++ PHPCI/Controller/BuildController.php | 37 +++++++---- PHPCI/Controller/BuildStatusController.php | 9 ++- PHPCI/Controller/HomeController.php | 6 +- PHPCI/Controller/PluginController.php | 23 ++++--- PHPCI/Controller/ProjectController.php | 35 ++++++---- PHPCI/Controller/SessionController.php | 16 +++-- PHPCI/Controller/SettingsController.php | 51 +++++++++------ PHPCI/Controller/UserController.php | 15 +++-- PHPCI/Controller/WebhookController.php | 76 ++++++++++++---------- 13 files changed, 188 insertions(+), 106 deletions(-) diff --git a/PHPCI/Command/InstallCommand.php b/PHPCI/Command/InstallCommand.php index 3dab656b..77b6b962 100644 --- a/PHPCI/Command/InstallCommand.php +++ b/PHPCI/Command/InstallCommand.php @@ -59,7 +59,9 @@ class InstallCommand extends Command { $this->configFilePath = $input->getOption('config-path'); - $this->verifyNotInstalled($output); + if (!$this->verifyNotInstalled($output)) { + return; + } $output->writeln(''); $output->writeln('******************'); @@ -346,7 +348,6 @@ class InstallCommand extends Command } catch (\Exception $ex) { $output->writeln(''.Lang::get('failed_to_create').''); $output->writeln('' . $ex->getMessage() . ''); - die; } } @@ -361,6 +362,7 @@ class InstallCommand extends Command /** * @param OutputInterface $output + * @return bool */ protected function verifyNotInstalled(OutputInterface $output) { @@ -370,8 +372,10 @@ class InstallCommand extends Command if (!empty($content)) { $output->writeln(''.Lang::get('config_exists').''); $output->writeln(''.Lang::get('update_instead').''); - die; + return false; } } + + return true; } } diff --git a/PHPCI/Command/PollCommand.php b/PHPCI/Command/PollCommand.php index 07b628b8..42a07067 100644 --- a/PHPCI/Command/PollCommand.php +++ b/PHPCI/Command/PollCommand.php @@ -60,7 +60,7 @@ class PollCommand extends Command if (!$token) { $this->logger->error(Lang::get('no_token')); - exit(); + return; } $buildStore = Factory::getStore('Build'); diff --git a/PHPCI/Command/UpdateCommand.php b/PHPCI/Command/UpdateCommand.php index 32ce09b2..1ca1341d 100644 --- a/PHPCI/Command/UpdateCommand.php +++ b/PHPCI/Command/UpdateCommand.php @@ -49,7 +49,9 @@ class UpdateCommand extends Command */ protected function execute(InputInterface $input, OutputInterface $output) { - $this->verifyInstalled($output); + if (!$this->verifyInstalled($output)) { + return; + } $output->write(Lang::get('updating_phpci')); @@ -63,14 +65,16 @@ class UpdateCommand extends Command if (!file_exists(PHPCI_DIR . 'PHPCI/config.yml')) { $output->writeln(''.Lang::get('not_installed').''); $output->writeln(''.Lang::get('install_instead').''); - die; + return false; } $content = file_get_contents(PHPCI_DIR . 'PHPCI/config.yml'); if (empty($content)) { $output->writeln(''.Lang::get('not_installed').''); $output->writeln(''.Lang::get('install_instead').''); - die; + return false; } + + return true; } } diff --git a/PHPCI/Controller.php b/PHPCI/Controller.php index 4f941251..68b1c845 100644 --- a/PHPCI/Controller.php +++ b/PHPCI/Controller.php @@ -92,6 +92,10 @@ class Controller extends \b8\Controller $this->setView($action); $response = parent::handleAction($action, $actionParams); + if ($response instanceof Response) { + return $response; + } + if (is_string($response)) { $this->controllerView->content = $response; } elseif (isset($this->view)) { diff --git a/PHPCI/Controller/BuildController.php b/PHPCI/Controller/BuildController.php index d552f5b3..bf898d2d 100644 --- a/PHPCI/Controller/BuildController.php +++ b/PHPCI/Controller/BuildController.php @@ -11,6 +11,7 @@ namespace PHPCI\Controller; use b8; use b8\Exception\HttpException\NotFoundException; +use b8\Http\Response\JsonResponse; use PHPCI\BuildFactory; use PHPCI\Helper\Lang; use PHPCI\Model\Build; @@ -61,7 +62,7 @@ class BuildController extends \PHPCI\Controller $this->view->plugins = $this->getUiPlugins(); $this->view->build = $build; - $this->view->data = $this->getBuildData($build); + $this->view->data = json_encode($this->getBuildData($build)); $this->layout->title = Lang::get('build_n', $buildId); $this->layout->subtitle = $build->getProjectTitle(); @@ -107,7 +108,17 @@ class BuildController extends \PHPCI\Controller */ public function data($buildId) { - die($this->getBuildData(BuildFactory::getBuildById($buildId))); + $response = new JsonResponse(); + $build = BuildFactory::getBuildById($buildId); + + if (!$build) { + $response->setResponseCode(404); + $response->setContent(array()); + return $response; + } + + $response->setContent($this->getBuildData($build)); + return $response; } /** @@ -124,7 +135,9 @@ class BuildController extends \PHPCI\Controller $data = $this->buildStore->getMeta($key, $build->getProjectId(), $buildId, $build->getBranch(), $numBuilds); } - die(json_encode($data)); + $response = new JsonResponse(); + $response->setContent($data); + return $response; } /** @@ -139,7 +152,7 @@ class BuildController extends \PHPCI\Controller $data['started'] = !is_null($build->getStarted()) ? $build->getStarted()->format('Y-m-d H:i:s') : null; $data['finished'] = !is_null($build->getFinished()) ? $build->getFinished()->format('Y-m-d H:i:s') : null; - return json_encode($data); + return $data; } /** @@ -155,8 +168,9 @@ class BuildController extends \PHPCI\Controller $build = $this->buildService->createDuplicateBuild($copy); - header('Location: '.PHPCI_URL.'build/view/' . $build->getId()); - exit; + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', PHPCI_URL.'build/view/' . $build->getId()); + return $response; } /** @@ -174,8 +188,9 @@ class BuildController extends \PHPCI\Controller $this->buildService->deleteBuild($build); - header('Location: '.PHPCI_URL.'project/view/' . $build->getProjectId()); - exit; + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', PHPCI_URL.'project/view/' . $build->getProjectId()); + return $response; } /** @@ -200,9 +215,9 @@ class BuildController extends \PHPCI\Controller 'running' => $this->formatBuilds($this->buildStore->getByStatus(Build::STATUS_RUNNING)), ); - if ($this->request->isAjax()) { - die(json_encode($rtn)); - } + $response = new JsonResponse(); + $response->setContent($rtn); + return $response; } /** diff --git a/PHPCI/Controller/BuildStatusController.php b/PHPCI/Controller/BuildStatusController.php index 71ad1e11..13b68cd8 100644 --- a/PHPCI/Controller/BuildStatusController.php +++ b/PHPCI/Controller/BuildStatusController.php @@ -53,7 +53,7 @@ class BuildStatusController extends \PHPCI\Controller $status = 'passing'; if (!$project->getAllowPublicStatus()) { - die(); + return null; } if (isset($project) && $project instanceof Project) { @@ -76,6 +76,13 @@ class BuildStatusController extends \PHPCI\Controller public function image($projectId) { $status = $this->getStatus($projectId); + + if (is_null($status)) { + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', '/'); + return $response; + } + $color = ($status == 'passing') ? 'green' : 'red'; header('Content-Type: image/svg+xml'); diff --git a/PHPCI/Controller/HomeController.php b/PHPCI/Controller/HomeController.php index 20d4cead..c48f40ee 100644 --- a/PHPCI/Controller/HomeController.php +++ b/PHPCI/Controller/HomeController.php @@ -68,7 +68,8 @@ class HomeController extends \PHPCI\Controller */ public function latest() { - die($this->getLatestBuildsHtml()); + $this->response->setContent($this->getLatestBuildsHtml()); + return $this->response; } /** @@ -77,7 +78,8 @@ class HomeController extends \PHPCI\Controller public function summary() { $projects = $this->projectStore->getWhere(array(), 50, 0, array(), array('title' => 'ASC')); - die($this->getSummaryHtml($projects)); + $this->response->setContent($this->getSummaryHtml($projects)); + return $this->response; } /** diff --git a/PHPCI/Controller/PluginController.php b/PHPCI/Controller/PluginController.php index ac829f7f..a090798e 100644 --- a/PHPCI/Controller/PluginController.php +++ b/PHPCI/Controller/PluginController.php @@ -82,16 +82,18 @@ class PluginController extends \PHPCI\Controller $package = $this->getParam('package', null); $json = $this->getComposerJson(); + $response = new b8\Http\Response\RedirectResponse(); + if (!in_array($package, $this->required)) { unset($json['require'][$package]); $this->setComposerJson($json); - header('Location: ' . PHPCI_URL . 'plugin?r=' . $package); - die; + $response->setHeader('Location', PHPCI_URL . 'plugin?r=' . $package); + return $response; } - header('Location: ' . PHPCI_URL); - die; + $response->setHeader('Location', PHPCI_URL); + return $response; } /** @@ -108,8 +110,9 @@ class PluginController extends \PHPCI\Controller $json['require'][$package] = $version; $this->setComposerJson($json); - header('Location: ' . PHPCI_URL . 'plugin?w=' . $package); - die; + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', PHPCI_URL . 'plugin?w=' . $package); + return $response; } /** @@ -181,7 +184,9 @@ class PluginController extends \PHPCI\Controller $http->setHeaders(array('User-Agent: PHPCI/1.0 (+https://www.phptesting.org)')); $res = $http->get('https://packagist.org/search.json', array('q' => $searchQuery)); - die(json_encode($res['body'])); + $response = new b8\Http\Response\JsonResponse(); + $response->setContent($res['body']); + return $response; } /** @@ -194,6 +199,8 @@ class PluginController extends \PHPCI\Controller $http->setHeaders(array('User-Agent: PHPCI/1.0 (+https://www.phptesting.org)')); $res = $http->get('https://packagist.org/packages/'.$name.'.json'); - die(json_encode($res['body'])); + $response = new b8\Http\Response\JsonResponse(); + $response->setContent($res['body']); + return $response; } } diff --git a/PHPCI/Controller/ProjectController.php b/PHPCI/Controller/ProjectController.php index 01900aa0..5a914adf 100644 --- a/PHPCI/Controller/ProjectController.php +++ b/PHPCI/Controller/ProjectController.php @@ -81,8 +81,9 @@ class ProjectController extends \PHPCI\Controller $pages = $builds[1] == 0 ? 1 : ceil($builds[1] / $per_page); if ($page > $pages) { - header('Location: '.PHPCI_URL.'project/view/'.$projectId); - die; + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', PHPCI_URL.'project/view/'.$projectId); + return $response; } $this->view->builds = $builds[0]; @@ -118,8 +119,9 @@ class ProjectController extends \PHPCI\Controller $email = $_SESSION['phpci_user']->getEmail(); $build = $this->buildService->createBuild($project, null, urldecode($branch), $email); - header('Location: '.PHPCI_URL.'build/view/' . $build->getId()); - exit; + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', PHPCI_URL.'build/view/' . $build->getId()); + return $response; } /** @@ -132,8 +134,9 @@ class ProjectController extends \PHPCI\Controller $project = $this->projectStore->getById($projectId); $this->projectService->deleteProject($project); - header('Location: '.PHPCI_URL); - exit; + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', PHPCI_URL); + return $response; } /** @@ -143,7 +146,9 @@ class ProjectController extends \PHPCI\Controller { $branch = $this->getParam('branch', ''); $builds = $this->getLatestBuildsHtml($projectId, urldecode($branch)); - die($builds[0]); + + $this->response->setContent($builds[0]); + return $this->response; } /** @@ -220,8 +225,10 @@ class ProjectController extends \PHPCI\Controller ); $project = $this->projectService->createProject($title, $type, $reference, $options); - header('Location: '.PHPCI_URL.'project/view/' . $project->getId()); - die; + + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', PHPCI_URL.'project/view/' . $project->getId()); + return $response; } } @@ -282,8 +289,9 @@ class ProjectController extends \PHPCI\Controller $project = $this->projectService->updateProject($project, $title, $type, $reference, $options); - header('Location: '.PHPCI_URL.'project/view/' . $project->getId()); - die; + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', PHPCI_URL.'project/view/' . $project->getId()); + return $response; } /** @@ -366,7 +374,10 @@ class ProjectController extends \PHPCI\Controller protected function githubRepositories() { $github = new Github(); - die(json_encode($github->getRepositories())); + + $response = new b8\Http\Response\JsonResponse(); + $response->setContent($github->getRepositories()); + return $response; } /** diff --git a/PHPCI/Controller/SessionController.php b/PHPCI/Controller/SessionController.php index c5f8b962..540b043f 100644 --- a/PHPCI/Controller/SessionController.php +++ b/PHPCI/Controller/SessionController.php @@ -47,8 +47,9 @@ class SessionController extends \PHPCI\Controller if ($user && password_verify($this->getParam('password', ''), $user->getHash())) { $_SESSION['phpci_user_id'] = $user->getId(); - header('Location: ' . $this->getLoginRedirect()); - die; + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', $this->getLoginRedirect()); + return $response; } else { $isLoginFailure = true; } @@ -92,8 +93,10 @@ class SessionController extends \PHPCI\Controller unset($_SESSION['phpci_user_id']); session_destroy(); - header('Location: ' . PHPCI_URL); - die; + + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', PHPCI_URL); + return $response; } /** @@ -151,8 +154,9 @@ class SessionController extends \PHPCI\Controller $_SESSION['phpci_user'] = $this->userStore->save($user); $_SESSION['phpci_user_id'] = $user->getId(); - header('Location: ' . PHPCI_URL); - die; + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', PHPCI_URL); + return $response; } $this->view->id = $userId; diff --git a/PHPCI/Controller/SettingsController.php b/PHPCI/Controller/SettingsController.php index 72df7c95..ba9b0141 100644 --- a/PHPCI/Controller/SettingsController.php +++ b/PHPCI/Controller/SettingsController.php @@ -102,13 +102,15 @@ class SettingsController extends Controller $this->settings['phpci']['github']['secret'] = $this->getParam('githubsecret', ''); $error = $this->storeSettings(); + $response = new b8\Http\Response\RedirectResponse(); + if ($error) { - header('Location: ' . PHPCI_URL . 'settings?saved=2'); + $response->setHeader('Location', PHPCI_URL . 'settings?saved=2'); } else { - header('Location: ' . PHPCI_URL . 'settings?saved=1'); + $response->setHeader('Location', PHPCI_URL . 'settings?saved=1'); } - die; + return $response; } /** @@ -123,13 +125,15 @@ class SettingsController extends Controller $error = $this->storeSettings(); + $response = new b8\Http\Response\RedirectResponse(); + if ($error) { - header('Location: ' . PHPCI_URL . 'settings?saved=2'); + $response->setHeader('Location', PHPCI_URL . 'settings?saved=2'); } else { - header('Location: ' . PHPCI_URL . 'settings?saved=1'); + $response->setHeader('Location', PHPCI_URL . 'settings?saved=1'); } - die; + return $response; } /** @@ -143,13 +147,15 @@ class SettingsController extends Controller $error = $this->storeSettings(); + $response = new b8\Http\Response\RedirectResponse(); + if ($error) { - header('Location: ' . PHPCI_URL . 'settings?saved=2'); + $response->setHeader('Location', PHPCI_URL . 'settings?saved=2'); } else { - header('Location: ' . PHPCI_URL . 'settings?saved=1'); + $response->setHeader('Location', PHPCI_URL . 'settings?saved=1'); } - die; + return $response; } /** @@ -162,13 +168,15 @@ class SettingsController extends Controller $this->settings['phpci']['basic'] = $this->getParams(); $error = $this->storeSettings(); + $response = new b8\Http\Response\RedirectResponse(); + if ($error) { - header('Location: ' . PHPCI_URL . 'settings?saved=2'); + $response->setHeader('Location', PHPCI_URL . 'settings?saved=2'); } else { - header('Location: ' . PHPCI_URL . 'settings?saved=1'); + $response->setHeader('Location', PHPCI_URL . 'settings?saved=1'); } - die; + return $response; } /** @@ -183,13 +191,15 @@ class SettingsController extends Controller $error = $this->storeSettings(); + $response = new b8\Http\Response\RedirectResponse(); + if ($error) { - header('Location: ' . PHPCI_URL . 'settings?saved=2'); + $response->setHeader('Location', PHPCI_URL . 'settings?saved=2'); } else { - header('Location: ' . PHPCI_URL . 'settings?saved=1'); + $response->setHeader('Location', PHPCI_URL . 'settings?saved=1'); } - die; + return $response; } /** @@ -212,14 +222,15 @@ class SettingsController extends Controller $this->settings['phpci']['github']['token'] = $resp['access_token']; $this->storeSettings(); - header('Location: ' . PHPCI_URL . 'settings?linked=1'); - die; + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', PHPCI_URL . 'settings?linked=1'); + return $response; } } - - header('Location: ' . PHPCI_URL . 'settings?linked=2'); - die; + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', PHPCI_URL . 'settings?linked=2'); + return $response; } /** diff --git a/PHPCI/Controller/UserController.php b/PHPCI/Controller/UserController.php index 7710c503..48092ad5 100644 --- a/PHPCI/Controller/UserController.php +++ b/PHPCI/Controller/UserController.php @@ -175,8 +175,9 @@ class UserController extends Controller $this->userService->createUser($name, $email, $password, $isAdmin); - header('Location: '.PHPCI_URL.'user'); - die; + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', PHPCI_URL . 'user'); + return $response; } /** @@ -215,8 +216,9 @@ class UserController extends Controller $this->userService->updateUser($user, $name, $email, $password, $isAdmin); - header('Location: '.PHPCI_URL.'user'); - die; + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', PHPCI_URL . 'user'); + return $response; } /** @@ -288,7 +290,8 @@ class UserController extends Controller $this->userService->deleteUser($user); - header('Location: '.PHPCI_URL.'user'); - die; + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', PHPCI_URL . 'user'); + return $response; } } diff --git a/PHPCI/Controller/WebhookController.php b/PHPCI/Controller/WebhookController.php index a7652971..5ce4849b 100644 --- a/PHPCI/Controller/WebhookController.php +++ b/PHPCI/Controller/WebhookController.php @@ -55,6 +55,9 @@ class WebhookController extends \PHPCI\Controller */ public function bitbucket($project) { + $response = new b8\Http\Response\JsonResponse(); + $response->setContent(array('status' => 'ok')); + $payload = json_decode($this->getParam('payload'), true); foreach ($payload['commits'] as $commit) { @@ -65,13 +68,13 @@ class WebhookController extends \PHPCI\Controller $this->createBuild($project, $commit['raw_node'], $commit['branch'], $email, $commit['message']); } catch (\Exception $ex) { - header('HTTP/1.1 500 Internal Server Error'); - header('Ex: ' . $ex->getMessage()); - die('FAIL'); + $response->setResponseCode(500); + $response->setContent(array('status' => 'failed', 'error' => $ex->getMessage())); + break; } } - die('OK'); + return $response; } /** @@ -81,6 +84,9 @@ class WebhookController extends \PHPCI\Controller */ public function git($project) { + $response = new b8\Http\Response\JsonResponse(); + $response->setContent(array('status' => 'ok')); + $branch = $this->getParam('branch'); $commit = $this->getParam('commit'); $commitMessage = $this->getParam('message'); @@ -105,12 +111,11 @@ class WebhookController extends \PHPCI\Controller $this->createBuild($project, $commit, $branch, $committer, $commitMessage); } catch (\Exception $ex) { - header('HTTP/1.1 400 Bad Request'); - header('Ex: ' . $ex->getMessage()); - die('FAIL'); + $response->setResponseCode(500); + $response->setContent(array('status' => 'failed', 'error' => $ex->getMessage())); } - die('OK'); + return $response; } /** @@ -118,6 +123,9 @@ class WebhookController extends \PHPCI\Controller */ public function github($project) { + $response = new b8\Http\Response\JsonResponse(); + $response->setContent(array('status' => 'ok')); + switch ($_SERVER['CONTENT_TYPE']) { case 'application/json': $payload = json_decode(file_get_contents('php://input'), true); @@ -128,22 +136,22 @@ class WebhookController extends \PHPCI\Controller break; default: - header('HTTP/1.1 400 Bad Request'); - die('Request content type not supported'); + $response->setResponseCode(401); + $response->setContent(array('status' => 'failed', 'error' => 'Content type not supported.')); + return $response; } // Handle Pull Request web hooks: if (array_key_exists('pull_request', $payload)) { - return $this->githubPullRequest($project, $payload); + return $this->githubPullRequest($project, $payload, $response); } // Handle Push web hooks: if (array_key_exists('commits', $payload)) { - return $this->githubCommitRequest($project, $payload); + return $this->githubCommitRequest($project, $payload, $response); } - header('HTTP/1.1 200 OK'); - die('This request type is not supported, this is not an error.'); + return $response; } /** @@ -151,12 +159,12 @@ class WebhookController extends \PHPCI\Controller * @param $project * @param array $payload */ - protected function githubCommitRequest($project, array $payload) + protected function githubCommitRequest($project, array $payload, b8\Http\Response\JsonResponse $response) { // Github sends a payload when you close a pull request with a // non-existant commit. We don't want this. if (array_key_exists('after', $payload) && $payload['after'] === '0000000000000000000000000000000000000000') { - die('OK'); + return $response; } try { @@ -182,12 +190,12 @@ class WebhookController extends \PHPCI\Controller } } catch (\Exception $ex) { - header('HTTP/1.1 500 Internal Server Error'); - header('Ex: ' . $ex->getMessage()); - die('FAIL'); + $response->setResponseCode(500); + $response->setContent(array('status' => 'failed', 'error' => $ex->getMessage())); + } - die('OK'); + return $response; } /** @@ -195,11 +203,11 @@ class WebhookController extends \PHPCI\Controller * @param $projectId * @param array $payload */ - protected function githubPullRequest($projectId, array $payload) + protected function githubPullRequest($projectId, array $payload, b8\Http\Response\JsonResponse $response) { // We only want to know about open pull requests: if (!in_array($payload['action'], array('opened', 'synchronize', 'reopened'))) { - die('OK'); + return $response; } try { @@ -217,9 +225,10 @@ class WebhookController extends \PHPCI\Controller // Check we got a success response: if (!$response['success']) { - header('HTTP/1.1 500 Internal Server Error'); - header('Ex: Could not get commits, failed API request.'); - die('FAIL'); + $message = 'Could not get commits, failed API request.'; + $response->setResponseCode(500); + $response->setContent(array('status' => 'failed', 'error' => $message)); + return $response; } foreach ($response['body'] as $commit) { @@ -238,12 +247,11 @@ class WebhookController extends \PHPCI\Controller $this->createBuild($projectId, $commit['sha'], $branch, $committer, $message, $extra); } } catch (\Exception $ex) { - header('HTTP/1.1 500 Internal Server Error'); - header('Ex: ' . $ex->getMessage()); - die('FAIL'); + $response->setResponseCode(500); + $response->setContent(array('status' => 'failed', 'error' => $ex->getMessage())); } - die('OK'); + return $response; } /** @@ -251,6 +259,9 @@ class WebhookController extends \PHPCI\Controller */ public function gitlab($project) { + $response = new b8\Http\Response\JsonResponse(); + $response->setContent(array('status' => 'ok')); + $payloadString = file_get_contents("php://input"); $payload = json_decode($payloadString, true); @@ -282,12 +293,11 @@ class WebhookController extends \PHPCI\Controller } } catch (\Exception $ex) { - header('HTTP/1.1 500 Internal Server Error'); - header('Ex: ' . $ex->getMessage()); - die('FAIL'); + $response->setResponseCode(500); + $response->setContent(array('status' => 'failed', 'error' => $ex->getMessage())); } - die('OK'); + return $response; } /** From c9864058616cf560662ea48752d679368d2e6d39 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 12 Feb 2015 12:38:22 +0000 Subject: [PATCH 082/329] Removing the now-unnecessary JSON.parse() call --- public/assets/js/phpci.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/assets/js/phpci.js b/public/assets/js/phpci.js index 7e0b0f8b..a2d5954c 100644 --- a/public/assets/js/phpci.js +++ b/public/assets/js/phpci.js @@ -33,7 +33,7 @@ var PHPCI = { url: PHPCI_URL + 'build/latest', success: function (data) { - $(window).trigger('builds-updated', [JSON.parse(data)]); + $(window).trigger('builds-updated', [data]); }, error: PHPCI.handleFailedAjax From 54ab93373ddd44b5f53a3ee987a9578f39bb5fd2 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 12 Feb 2015 13:17:42 +0000 Subject: [PATCH 083/329] Fixing some bugs related to the last round of changes. --- PHPCI/Controller/ProjectController.php | 1 + PHPCI/View/Home/index.phtml | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/PHPCI/Controller/ProjectController.php b/PHPCI/Controller/ProjectController.php index 5a914adf..e1aaa6cb 100644 --- a/PHPCI/Controller/ProjectController.php +++ b/PHPCI/Controller/ProjectController.php @@ -147,6 +147,7 @@ class ProjectController extends \PHPCI\Controller $branch = $this->getParam('branch', ''); $builds = $this->getLatestBuildsHtml($projectId, urldecode($branch)); + $this->response->disableLayout(); $this->response->setContent($builds[0]); return $this->response; } diff --git a/PHPCI/View/Home/index.phtml b/PHPCI/View/Home/index.phtml index 6e27bb79..251bb9ee 100644 --- a/PHPCI/View/Home/index.phtml +++ b/PHPCI/View/Home/index.phtml @@ -17,7 +17,6 @@ getStatus()) { case \PHPCI\Model\Build::STATUS_NEW: $updated = $build->getCreated(); @@ -44,6 +43,10 @@ break; } + if (!$updated) { + $updated = $build->getCreated(); + } + if ($updated->format('Y-m-d') != $last->format('Y-m-d')): $last = $updated; ?>
  • From 320b0efa7175b0271f1323553ab208615626843e Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 12 Feb 2015 13:17:50 +0000 Subject: [PATCH 084/329] Fixing run-builds. --- PHPCI/Command/RunCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/Command/RunCommand.php b/PHPCI/Command/RunCommand.php index e0f37b6b..63e38b2d 100644 --- a/PHPCI/Command/RunCommand.php +++ b/PHPCI/Command/RunCommand.php @@ -47,7 +47,7 @@ class RunCommand extends Command /** * @var int */ - protected $maxBuilds = null; + protected $maxBuilds = 100; /** * @var bool From dea615bf26338b3a197fe8912303f12c7e4ec256 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 12 Feb 2015 13:20:08 +0000 Subject: [PATCH 085/329] Cleanup --- PHPCI/Controller/HomeController.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PHPCI/Controller/HomeController.php b/PHPCI/Controller/HomeController.php index c48f40ee..65abf79a 100644 --- a/PHPCI/Controller/HomeController.php +++ b/PHPCI/Controller/HomeController.php @@ -68,6 +68,7 @@ class HomeController extends \PHPCI\Controller */ public function latest() { + $this->response->disableLayout(); $this->response->setContent($this->getLatestBuildsHtml()); return $this->response; } @@ -77,6 +78,7 @@ class HomeController extends \PHPCI\Controller */ public function summary() { + $this->response->disableLayout(); $projects = $this->projectStore->getWhere(array(), 50, 0, array(), array('title' => 'ASC')); $this->response->setContent($this->getSummaryHtml($projects)); return $this->response; From dcbad55df8c04e9c0d1ab36feba7c543e3eeddfa Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 12 Feb 2015 13:30:32 +0000 Subject: [PATCH 086/329] Fixing FIXMEs --- PHPCI/Controller/BuildStatusController.php | 9 ++++++--- PHPCI/Helper/Github.php | 2 +- PHPCI/Plugin/PhpTalLint.php | 2 -- PHPCI/Plugin/SlackNotify.php | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/PHPCI/Controller/BuildStatusController.php b/PHPCI/Controller/BuildStatusController.php index 13b68cd8..06f9400b 100644 --- a/PHPCI/Controller/BuildStatusController.php +++ b/PHPCI/Controller/BuildStatusController.php @@ -84,9 +84,12 @@ class BuildStatusController extends \PHPCI\Controller } $color = ($status == 'passing') ? 'green' : 'red'; - - header('Content-Type: image/svg+xml'); - die(file_get_contents('http://img.shields.io/badge/build-' . $status . '-' . $color . '.svg')); + $image = file_get_contents('http://img.shields.io/badge/build-' . $status . '-' . $color . '.svg'); + + $this->response->disableLayout(); + $this->response->setHeader('Content-Type', 'image/svg+xml'); + $this->response->setContent($image); + return $this->response; } /** diff --git a/PHPCI/Helper/Github.php b/PHPCI/Helper/Github.php index b83b06c3..a791601e 100644 --- a/PHPCI/Helper/Github.php +++ b/PHPCI/Helper/Github.php @@ -79,7 +79,7 @@ class Github $token = Config::getInstance()->get('phpci.github.token'); if (!$token) { - die(json_encode(null)); + return null; } $cache = Cache::getCache(Cache::TYPE_APC); diff --git a/PHPCI/Plugin/PhpTalLint.php b/PHPCI/Plugin/PhpTalLint.php index 01e70b3e..146b0479 100644 --- a/PHPCI/Plugin/PhpTalLint.php +++ b/PHPCI/Plugin/PhpTalLint.php @@ -202,7 +202,6 @@ class PhpTalLint implements PHPCI\Plugin list($suffixes, $tales) = $this->getFlags(); - // FIXME: Find a way to clean this up $lint = dirname(__FILE__) . '/../../vendor/phptal/phptal/tools/phptal_lint.php'; $cmd = '/usr/bin/env php ' . $lint . ' %s %s "%s"'; @@ -210,7 +209,6 @@ class PhpTalLint implements PHPCI\Plugin $output = $this->phpci->getLastOutput(); - // FIXME: This is very messy, clean it up if (preg_match('/Found (.+?) (error|warning)/i', $output, $matches)) { $rows = explode(PHP_EOL, $output); diff --git a/PHPCI/Plugin/SlackNotify.php b/PHPCI/Plugin/SlackNotify.php index 1e1e3961..65a190fd 100644 --- a/PHPCI/Plugin/SlackNotify.php +++ b/PHPCI/Plugin/SlackNotify.php @@ -120,7 +120,7 @@ class SlackNotify implements \PHPCI\Plugin $success = true; - $message->send(''); // FIXME: Handle errors + $message->send(''); return $success; } From 12f8d376bd1037324bb7802f8140b9c37f90f4a4 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 12 Feb 2015 13:34:15 +0000 Subject: [PATCH 087/329] Adding a default robots.txt file. --- public/robots.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 public/robots.txt diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..25781b7d --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: From 6576974584cf9204302ccdff448637e43cf42bff Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 12 Feb 2015 13:42:07 +0000 Subject: [PATCH 088/329] Cleaning up permissions on a few files. --- PHPCI/Plugin/Mysql.php | 0 PHPCI/Plugin/PhpCodeSniffer.php | 0 PHPCI/Plugin/PhpDocblockChecker.php | 0 PHPCI/Plugin/PhpMessDetector.php | 0 PHPCI/build/.gitignore | 0 bootstrap.php | 0 public/assets/js/plugins/input-mask/phone-codes/readme.txt | 0 7 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 PHPCI/Plugin/Mysql.php mode change 100755 => 100644 PHPCI/Plugin/PhpCodeSniffer.php mode change 100755 => 100644 PHPCI/Plugin/PhpDocblockChecker.php mode change 100755 => 100644 PHPCI/Plugin/PhpMessDetector.php mode change 100755 => 100644 PHPCI/build/.gitignore mode change 100755 => 100644 bootstrap.php mode change 100755 => 100644 public/assets/js/plugins/input-mask/phone-codes/readme.txt diff --git a/PHPCI/Plugin/Mysql.php b/PHPCI/Plugin/Mysql.php old mode 100755 new mode 100644 diff --git a/PHPCI/Plugin/PhpCodeSniffer.php b/PHPCI/Plugin/PhpCodeSniffer.php old mode 100755 new mode 100644 diff --git a/PHPCI/Plugin/PhpDocblockChecker.php b/PHPCI/Plugin/PhpDocblockChecker.php old mode 100755 new mode 100644 diff --git a/PHPCI/Plugin/PhpMessDetector.php b/PHPCI/Plugin/PhpMessDetector.php old mode 100755 new mode 100644 diff --git a/PHPCI/build/.gitignore b/PHPCI/build/.gitignore old mode 100755 new mode 100644 diff --git a/bootstrap.php b/bootstrap.php old mode 100755 new mode 100644 diff --git a/public/assets/js/plugins/input-mask/phone-codes/readme.txt b/public/assets/js/plugins/input-mask/phone-codes/readme.txt old mode 100755 new mode 100644 From 5ca9d4606e995d432a9390f6aee4d0ca31a2fd69 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 12 Feb 2015 13:44:16 +0000 Subject: [PATCH 089/329] Fixes --- public/robots.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/robots.txt b/public/robots.txt index 25781b7d..0ad279c7 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,2 +1,2 @@ User-Agent: * -Disallow: +Disallow: From c20ca7c8ff546d8733dff9ff66053fe72150ba92 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 12 Feb 2015 14:11:58 +0000 Subject: [PATCH 090/329] Lots of cleanup. --- LICENSE.md | 2 +- PHPCI/Application.php | 3 +-- PHPCI/Command/CreateAdminCommand.php | 5 +---- PHPCI/Command/DaemonCommand.php | 5 ----- PHPCI/Command/DaemoniseCommand.php | 4 +--- PHPCI/Command/GenerateCommand.php | 2 -- PHPCI/Command/InstallCommand.php | 4 ++-- PHPCI/Command/PollCommand.php | 2 -- PHPCI/Command/RunCommand.php | 5 +---- PHPCI/Command/UpdateCommand.php | 3 --- PHPCI/Controller/PluginController.php | 1 - PHPCI/Controller/ProjectController.php | 7 ++----- PHPCI/Controller/SettingsController.php | 1 - PHPCI/Controller/UserController.php | 2 -- PHPCI/Controller/WebhookController.php | 1 - PHPCI/Helper/Email.php | 2 +- .../20140513153133_change_build_keys_migration.php | 2 +- PHPCI/Migrations/20140611170618_choose_branch.php | 2 +- PHPCI/Migrations/20150203105015_fix_column_types.php | 2 +- PHPCI/Plugin.php | 2 -- PHPCI/Plugin/Campfire.php | 1 - PHPCI/Plugin/CopyBuild.php | 2 +- PHPCI/Plugin/HipchatNotify.php | 8 +++----- PHPCI/Plugin/Util/ComposerPluginInformation.php | 4 +++- PHPCI/Plugin/Util/FilesPluginInformation.php | 6 +++--- PHPCI/Store/BuildStore.php | 1 - PHPCI/build/.gitignore | 2 +- daemon/.gitignore | 2 +- loggerconfig.php.example | 2 +- pluginconfig.php.example | 2 +- 30 files changed, 27 insertions(+), 60 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 1df52b9c..4c4f6952 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -6,4 +6,4 @@ Redistribution and use in source and binary forms, with or without modification, - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/PHPCI/Application.php b/PHPCI/Application.php index a8790804..ee1f6e15 100644 --- a/PHPCI/Application.php +++ b/PHPCI/Application.php @@ -14,7 +14,6 @@ use b8\Exception\HttpException; use b8\Http\Response; use b8\Http\Response\RedirectResponse; use b8\View; -use PHPCI\Model\Build; /** * PHPCI Front Controller @@ -58,7 +57,7 @@ class Application extends b8\Application $routeHandler = function (&$route, Response &$response) use (&$request, $validateSession, $skipAuth) { $skipValidation = in_array($route['controller'], array('session', 'webhook', 'build-status')); - if (!$skipValidation && !$validateSession() && !$skipAuth()) { + if (!$skipValidation && !$validateSession() && (!is_callable($skipAuth) || !$skipAuth())) { if ($request->isAjax()) { $response->setResponseCode(401); $response->setContent(''); diff --git a/PHPCI/Command/CreateAdminCommand.php b/PHPCI/Command/CreateAdminCommand.php index 81f23c72..3b69afd3 100644 --- a/PHPCI/Command/CreateAdminCommand.php +++ b/PHPCI/Command/CreateAdminCommand.php @@ -12,12 +12,9 @@ namespace PHPCI\Command; use PHPCI\Helper\Lang; use PHPCI\Service\UserService; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use b8\Store\Factory; -use PHPCI\Builder; /** * Create admin command - creates an admin user @@ -77,7 +74,7 @@ class CreateAdminCommand extends Command if (!$emptyOk && empty($rtn)) { $rtn = $this->ask($question, $emptyOk, $validationFilter); - } elseif ($validationFilter != null && ! empty($rtn)) { + } elseif (!is_null($validationFilter) && ! empty($rtn)) { if (! $this -> controlFormat($rtn, $validationFilter, $statusMessage)) { print $statusMessage; $rtn = $this->ask($question, $emptyOk, $validationFilter); diff --git a/PHPCI/Command/DaemonCommand.php b/PHPCI/Command/DaemonCommand.php index 79925fbd..e51f31f1 100644 --- a/PHPCI/Command/DaemonCommand.php +++ b/PHPCI/Command/DaemonCommand.php @@ -12,13 +12,8 @@ namespace PHPCI\Command; use Monolog\Logger; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use b8\Store\Factory; -use PHPCI\Builder; -use PHPCI\BuildFactory; /** * Daemon that loops and call the run-command. diff --git a/PHPCI/Command/DaemoniseCommand.php b/PHPCI/Command/DaemoniseCommand.php index 0bcd42f6..eadd334e 100644 --- a/PHPCI/Command/DaemoniseCommand.php +++ b/PHPCI/Command/DaemoniseCommand.php @@ -12,9 +12,7 @@ namespace PHPCI\Command; use Monolog\Logger; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArgvInput; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** @@ -76,7 +74,7 @@ class DaemoniseCommand extends Command $this->sleep = 0; $runner = new RunCommand($this->logger); $runner->setMaxBuilds(1); - $runner->setIsDaemon(true); + $runner->setDaemon(true); $emptyInput = new ArgvInput(array()); diff --git a/PHPCI/Command/GenerateCommand.php b/PHPCI/Command/GenerateCommand.php index 07307686..ddd227d9 100644 --- a/PHPCI/Command/GenerateCommand.php +++ b/PHPCI/Command/GenerateCommand.php @@ -10,9 +10,7 @@ namespace PHPCI\Command; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use b8\Database; use b8\Database\CodeGenerator; diff --git a/PHPCI/Command/InstallCommand.php b/PHPCI/Command/InstallCommand.php index 77b6b962..2917303d 100644 --- a/PHPCI/Command/InstallCommand.php +++ b/PHPCI/Command/InstallCommand.php @@ -17,11 +17,9 @@ use b8\Database; use b8\Store\Factory; use PHPCI\Helper\Lang; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Helper\DialogHelper; use PHPCI\Service\UserService; /** @@ -298,6 +296,8 @@ class InstallCommand extends Command ) ); + unset($pdo); + return true; } catch (Exception $ex) { diff --git a/PHPCI/Command/PollCommand.php b/PHPCI/Command/PollCommand.php index 42a07067..a8729515 100644 --- a/PHPCI/Command/PollCommand.php +++ b/PHPCI/Command/PollCommand.php @@ -14,9 +14,7 @@ use b8\HttpClient; use Monolog\Logger; use PHPCI\Helper\Lang; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Yaml\Parser; use PHPCI\Model\Build; diff --git a/PHPCI/Command/RunCommand.php b/PHPCI/Command/RunCommand.php index 63e38b2d..0cbd8e6a 100644 --- a/PHPCI/Command/RunCommand.php +++ b/PHPCI/Command/RunCommand.php @@ -15,11 +15,8 @@ use PHPCI\Helper\Lang; use PHPCI\Logging\BuildDBLogHandler; use PHPCI\Logging\LoggedBuildContextTidier; use PHPCI\Logging\OutputLogHandler; -use Psr\Log\LoggerAwareInterface; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use b8\Store\Factory; use PHPCI\Builder; @@ -143,7 +140,7 @@ class RunCommand extends Command $this->maxBuilds = (int)$numBuilds; } - public function setIsDaemon($fromDaemon) + public function setDaemon($fromDaemon) { $this->isFromDaemon = (bool)$fromDaemon; } diff --git a/PHPCI/Command/UpdateCommand.php b/PHPCI/Command/UpdateCommand.php index 1ca1341d..8170675a 100644 --- a/PHPCI/Command/UpdateCommand.php +++ b/PHPCI/Command/UpdateCommand.php @@ -12,10 +12,7 @@ namespace PHPCI\Command; use Monolog\Logger; use PHPCI\Helper\Lang; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** diff --git a/PHPCI/Controller/PluginController.php b/PHPCI/Controller/PluginController.php index a090798e..20675e46 100644 --- a/PHPCI/Controller/PluginController.php +++ b/PHPCI/Controller/PluginController.php @@ -11,7 +11,6 @@ namespace PHPCI\Controller; use b8; use PHPCI\Helper\Lang; -use PHPCI\Model\Build; use PHPCI\Plugin\Util\ComposerPluginInformation; use PHPCI\Plugin\Util\FilesPluginInformation; use PHPCI\Plugin\Util\PluginInformationCollection; diff --git a/PHPCI/Controller/ProjectController.php b/PHPCI/Controller/ProjectController.php index e1aaa6cb..a626100b 100644 --- a/PHPCI/Controller/ProjectController.php +++ b/PHPCI/Controller/ProjectController.php @@ -10,17 +10,14 @@ namespace PHPCI\Controller; use b8; -use b8\Controller; use b8\Form; -use b8\Exception\HttpException\ForbiddenException; use b8\Exception\HttpException\NotFoundException; use b8\Store; +use PHPCI; use PHPCI\BuildFactory; use PHPCI\Helper\Github; use PHPCI\Helper\Lang; use PHPCI\Helper\SshKey; -use PHPCI\Model\Build; -use PHPCI\Model\Project; use PHPCI\Service\BuildService; use PHPCI\Service\ProjectService; @@ -30,7 +27,7 @@ use PHPCI\Service\ProjectService; * @package PHPCI * @subpackage Web */ -class ProjectController extends \PHPCI\Controller +class ProjectController extends Controller { /** * @var \PHPCI\Store\ProjectStore diff --git a/PHPCI/Controller/SettingsController.php b/PHPCI/Controller/SettingsController.php index ba9b0141..3c5ab5f4 100644 --- a/PHPCI/Controller/SettingsController.php +++ b/PHPCI/Controller/SettingsController.php @@ -14,7 +14,6 @@ use b8\Form; use b8\HttpClient; use PHPCI\Controller; use PHPCI\Helper\Lang; -use PHPCI\Model\Build; use Symfony\Component\Yaml\Dumper; use Symfony\Component\Yaml\Parser; diff --git a/PHPCI/Controller/UserController.php b/PHPCI/Controller/UserController.php index 48092ad5..8d09ecd0 100644 --- a/PHPCI/Controller/UserController.php +++ b/PHPCI/Controller/UserController.php @@ -10,12 +10,10 @@ namespace PHPCI\Controller; use b8; -use b8\Exception\HttpException\ForbiddenException; use b8\Exception\HttpException\NotFoundException; use b8\Form; use PHPCI\Controller; use PHPCI\Helper\Lang; -use PHPCI\Model\User; use PHPCI\Service\UserService; /** diff --git a/PHPCI/Controller/WebhookController.php b/PHPCI/Controller/WebhookController.php index 5ce4849b..cef7dc53 100644 --- a/PHPCI/Controller/WebhookController.php +++ b/PHPCI/Controller/WebhookController.php @@ -12,7 +12,6 @@ namespace PHPCI\Controller; use b8; use b8\Store; use PHPCI\BuildFactory; -use PHPCI\Model\Build; use PHPCI\Service\BuildService; /** diff --git a/PHPCI/Helper/Email.php b/PHPCI/Helper/Email.php index 3ca15e94..cb588dd1 100644 --- a/PHPCI/Helper/Email.php +++ b/PHPCI/Helper/Email.php @@ -90,7 +90,7 @@ class Email * @param bool $isHtml * @return $this */ - public function setIsHtml($isHtml = false) + public function setHtml($isHtml = false) { $this->isHtml = $isHtml; diff --git a/PHPCI/Migrations/20140513153133_change_build_keys_migration.php b/PHPCI/Migrations/20140513153133_change_build_keys_migration.php index b26a9388..7fd253cc 100644 --- a/PHPCI/Migrations/20140513153133_change_build_keys_migration.php +++ b/PHPCI/Migrations/20140513153133_change_build_keys_migration.php @@ -23,4 +23,4 @@ class ChangeBuildKeysMigration extends AbstractMigration $project->renameColumn('ssh_private_key', 'git_key'); $project->renameColumn('ssh_public_key', 'public_key'); } -} \ No newline at end of file +} diff --git a/PHPCI/Migrations/20140611170618_choose_branch.php b/PHPCI/Migrations/20140611170618_choose_branch.php index 5945c241..26b4ed4c 100644 --- a/PHPCI/Migrations/20140611170618_choose_branch.php +++ b/PHPCI/Migrations/20140611170618_choose_branch.php @@ -37,4 +37,4 @@ class ChooseBranch extends AbstractMigration $project = $this->table('project'); $project->removeColumn('branch')->save(); } -} \ No newline at end of file +} diff --git a/PHPCI/Migrations/20150203105015_fix_column_types.php b/PHPCI/Migrations/20150203105015_fix_column_types.php index 9a693594..f9bcf68a 100644 --- a/PHPCI/Migrations/20150203105015_fix_column_types.php +++ b/PHPCI/Migrations/20150203105015_fix_column_types.php @@ -25,4 +25,4 @@ class FixColumnTypes extends AbstractMigration 'limit' => MysqlAdapter::TEXT_MEDIUM, )); } -} \ No newline at end of file +} diff --git a/PHPCI/Plugin.php b/PHPCI/Plugin.php index 67914b7d..ac2960cd 100644 --- a/PHPCI/Plugin.php +++ b/PHPCI/Plugin.php @@ -9,8 +9,6 @@ namespace PHPCI; -use PHPCI\Model\Build; - /** * PHPCI Plugin Interface - Used by all build plugins. * @author Dan Cryer diff --git a/PHPCI/Plugin/Campfire.php b/PHPCI/Plugin/Campfire.php index b111786e..325e6e40 100644 --- a/PHPCI/Plugin/Campfire.php +++ b/PHPCI/Plugin/Campfire.php @@ -148,7 +148,6 @@ class Campfire implements \PHPCI\Plugin return json_decode($output); } // Simple 200 OK response (such as for joining a room) - // TODO: check for other result codes here return true; } } diff --git a/PHPCI/Plugin/CopyBuild.php b/PHPCI/Plugin/CopyBuild.php index a80819fb..7f447291 100644 --- a/PHPCI/Plugin/CopyBuild.php +++ b/PHPCI/Plugin/CopyBuild.php @@ -74,7 +74,7 @@ class CopyBuild implements \PHPCI\Plugin */ protected function wipeExistingDirectory() { - if ($this->wipe == true && $this->directory != '/' && is_dir($this->directory)) { + if ($this->wipe === true && $this->directory != '/' && is_dir($this->directory)) { $cmd = 'rm -Rf "%s*"'; $success = $this->phpci->executeCommand($cmd, $this->directory); diff --git a/PHPCI/Plugin/HipchatNotify.php b/PHPCI/Plugin/HipchatNotify.php index 0cc1bf70..a39b94fe 100644 --- a/PHPCI/Plugin/HipchatNotify.php +++ b/PHPCI/Plugin/HipchatNotify.php @@ -21,11 +21,9 @@ use PHPCI\Model\Build; */ class HipchatNotify implements \PHPCI\Plugin { - private $authToken; - private $userAgent; - private $cookie; - private $color; - private $notify; + protected $authToken; + protected $color; + protected $notify; /** * Set up the plugin, configure options, etc. diff --git a/PHPCI/Plugin/Util/ComposerPluginInformation.php b/PHPCI/Plugin/Util/ComposerPluginInformation.php index 1f022b4a..d8dcc91f 100644 --- a/PHPCI/Plugin/Util/ComposerPluginInformation.php +++ b/PHPCI/Plugin/Util/ComposerPluginInformation.php @@ -2,6 +2,8 @@ namespace PHPCI\Plugin\Util; +use PHPCI\Plugin; + /** * Class ComposerPluginInformation * @package PHPCI\Plugin\Util @@ -63,7 +65,7 @@ class ComposerPluginInformation implements InstalledPluginInformation public function getPluginClasses() { return array_map( - function ($plugin) { + function (Plugin $plugin) { return $plugin->class; }, $this->getInstalledPlugins() diff --git a/PHPCI/Plugin/Util/FilesPluginInformation.php b/PHPCI/Plugin/Util/FilesPluginInformation.php index 6bad028f..33b3e4b3 100644 --- a/PHPCI/Plugin/Util/FilesPluginInformation.php +++ b/PHPCI/Plugin/Util/FilesPluginInformation.php @@ -2,6 +2,8 @@ namespace PHPCI\Plugin\Util; +use PHPCI\Plugin; + /** * Class FilesPluginInformation * @package PHPCI\Plugin\Util @@ -66,7 +68,7 @@ class FilesPluginInformation implements InstalledPluginInformation public function getPluginClasses() { return array_map( - function ($plugin) { + function (Plugin $plugin) { return $plugin->class; }, $this->getInstalledPlugins() @@ -114,8 +116,6 @@ class FilesPluginInformation implements InstalledPluginInformation */ protected function getFullClassFromFile(\SplFileInfo $fileInfo) { - //TODO: Something less horrible than a regular expression - // on the contents of a file $contents = file_get_contents($fileInfo->getRealPath()); $matches = array(); diff --git a/PHPCI/Store/BuildStore.php b/PHPCI/Store/BuildStore.php index 96631e1e..3a4c0ddc 100644 --- a/PHPCI/Store/BuildStore.php +++ b/PHPCI/Store/BuildStore.php @@ -10,7 +10,6 @@ namespace PHPCI\Store; use b8\Database; -use PHPCI\BuildFactory; use PHPCI\Model\Build; use PHPCI\Store\Base\BuildStoreBase; diff --git a/PHPCI/build/.gitignore b/PHPCI/build/.gitignore index c96a04f0..d6b7ef32 100644 --- a/PHPCI/build/.gitignore +++ b/PHPCI/build/.gitignore @@ -1,2 +1,2 @@ * -!.gitignore \ No newline at end of file +!.gitignore diff --git a/daemon/.gitignore b/daemon/.gitignore index c96a04f0..d6b7ef32 100644 --- a/daemon/.gitignore +++ b/daemon/.gitignore @@ -1,2 +1,2 @@ * -!.gitignore \ No newline at end of file +!.gitignore diff --git a/loggerconfig.php.example b/loggerconfig.php.example index c306673b..d5abc96e 100644 --- a/loggerconfig.php.example +++ b/loggerconfig.php.example @@ -13,4 +13,4 @@ return array( new \Monolog\Handler\RotatingFileHandler(__DIR__ . DIRECTORY_SEPARATOR . 'everything',3, \Monolog\Logger::DEBUG), ); }, -); \ No newline at end of file +); diff --git a/pluginconfig.php.example b/pluginconfig.php.example index 4e4279b9..5c182401 100644 --- a/pluginconfig.php.example +++ b/pluginconfig.php.example @@ -19,4 +19,4 @@ return function (PHPCI\Plugin\Util\Factory $factory) { // The resource will only be given when the type hint is: PHPCI\Plugin\Util\Factory::TYPE_ARRAY ); -}; \ No newline at end of file +}; From 44c489dd222599501df2bc85601caccc6e86a423 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 12 Feb 2015 14:15:19 +0000 Subject: [PATCH 091/329] Lots of cleanup. --- PHPCI/Command/InstallCommand.php | 1 - PHPCI/Controller/ProjectController.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/PHPCI/Command/InstallCommand.php b/PHPCI/Command/InstallCommand.php index 2917303d..d262b9f0 100644 --- a/PHPCI/Command/InstallCommand.php +++ b/PHPCI/Command/InstallCommand.php @@ -13,7 +13,6 @@ use Exception; use PDO; use b8\Config; -use b8\Database; use b8\Store\Factory; use PHPCI\Helper\Lang; use Symfony\Component\Console\Command\Command; diff --git a/PHPCI/Controller/ProjectController.php b/PHPCI/Controller/ProjectController.php index a626100b..b263f06e 100644 --- a/PHPCI/Controller/ProjectController.php +++ b/PHPCI/Controller/ProjectController.php @@ -27,7 +27,7 @@ use PHPCI\Service\ProjectService; * @package PHPCI * @subpackage Web */ -class ProjectController extends Controller +class ProjectController extends PHPCI\Controller { /** * @var \PHPCI\Store\ProjectStore From 55b10948efc87039c9704962ef549c7771b0855a Mon Sep 17 00:00:00 2001 From: Igor Timoshenko Date: Wed, 4 Feb 2015 14:21:21 +0000 Subject: [PATCH 092/329] Added Codeception UI plugin --- PHPCI/Languages/lang.da.php | 2 + PHPCI/Languages/lang.de.php | 2 + PHPCI/Languages/lang.el.php | 2 + PHPCI/Languages/lang.en.php | 2 + PHPCI/Languages/lang.fr.php | 2 + PHPCI/Languages/lang.it.php | 4 +- PHPCI/Languages/lang.nl.php | 2 + PHPCI/Languages/lang.pl.php | 2 + PHPCI/Languages/lang.ru.php | 2 + PHPCI/Languages/lang.uk.php | 4 +- PHPCI/Plugin/Codeception.php | 90 +++++++++++++++---- public/assets/js/build-plugins/codeception.js | 75 ++++++++++++++++ public/assets/js/build-plugins/warnings.js | 3 +- 13 files changed, 170 insertions(+), 22 deletions(-) create mode 100644 public/assets/js/build-plugins/codeception.js diff --git a/PHPCI/Languages/lang.da.php b/PHPCI/Languages/lang.da.php index 12ed31e2..5010f28e 100644 --- a/PHPCI/Languages/lang.da.php +++ b/PHPCI/Languages/lang.da.php @@ -166,6 +166,7 @@ Services sektionen under dit Bitbucket-repository.', 'lines_of_code' => 'Kode-linjer', 'build_log' => 'Build-log', 'quality_trend' => 'Kvalitets-trend', + 'codeception_errors' => 'Codeception-fejl', 'phpmd_warnings' => 'PHPMD-advarsler', 'phpcs_warnings' => 'PHPCS-advarsler', 'phpcs_errors' => 'PHPCS-fejl', @@ -174,6 +175,7 @@ Services sektionen under dit Bitbucket-repository.', 'phpdoccheck_warnings' => 'Manglende Docblocks', 'issues' => 'Sager', + 'codeception' => 'Codeception', 'phpcpd' => 'PHP Copy/Paste Detector', 'phpcs' => 'PHP Code Sniffer', 'phpdoccheck' => 'Manglende Docblocks', diff --git a/PHPCI/Languages/lang.de.php b/PHPCI/Languages/lang.de.php index d89261c8..c40ef23d 100644 --- a/PHPCI/Languages/lang.de.php +++ b/PHPCI/Languages/lang.de.php @@ -164,6 +164,7 @@ generiert. Um es zu verwenden, fügen Sie einfach den folgenden Public Key im Ab 'lines_of_code' => 'Anzahl Codezeilen', 'build_log' => 'Buildprotokoll', 'quality_trend' => 'Qualitätstrend', + 'codeception_errors' => 'Codeception Errors', 'phpmd_warnings' => 'PHPMD Warnings', 'phpcs_warnings' => 'PHPCS Warnings', 'phpcs_errors' => 'PHPCS Errors', @@ -172,6 +173,7 @@ generiert. Um es zu verwenden, fügen Sie einfach den folgenden Public Key im Ab 'phpdoccheck_warnings' => 'Fehlende Docblocks', 'issues' => 'Probleme', + 'codeception' => 'Codeception', 'phpcpd' => 'PHP Copy/Paste Detector', 'phpcs' => 'PHP Code Sniffer', 'phpdoccheck' => 'Fehlende Docblocks', diff --git a/PHPCI/Languages/lang.el.php b/PHPCI/Languages/lang.el.php index 2089cd04..c611a87d 100644 --- a/PHPCI/Languages/lang.el.php +++ b/PHPCI/Languages/lang.el.php @@ -166,6 +166,7 @@ Services του Bitbucket αποθετηρίου σας.', 'lines_of_code' => 'Γραμμές Κώδικα', 'build_log' => 'Αρχείο καταγραφής κατασκευών', 'quality_trend' => 'Ποιότητα τρέντ', + 'codeception_errors' => 'Λάθη Codeception', 'phpmd_warnings' => 'Προειδοποιήσεις PHPMD', 'phpcs_warnings' => 'Προειδοποιήσεις PHPCS ', 'phpcs_errors' => 'Λάθη PHPCS', @@ -174,6 +175,7 @@ Services του Bitbucket αποθετηρίου σας.', 'phpdoccheck_warnings' => 'Χαμένα Docblocks', 'issues' => 'Θέματα', + 'codeception' => 'Codeception', 'phpcpd' => 'PHP Ανιχνευτής Αντιγραφής/Επικόλλησης', 'phpcs' => 'Sniffer Κώδικα PHP', 'phpdoccheck' => 'Χαμένα Docblocks', diff --git a/PHPCI/Languages/lang.en.php b/PHPCI/Languages/lang.en.php index 41526624..9718e442 100644 --- a/PHPCI/Languages/lang.en.php +++ b/PHPCI/Languages/lang.en.php @@ -167,6 +167,7 @@ PHPCI', 'lines_of_code' => 'Lines of Code', 'build_log' => 'Build Log', 'quality_trend' => 'Quality Trend', + 'codeception_errors' => 'Codeception Errors', 'phpmd_warnings' => 'PHPMD Warnings', 'phpcs_warnings' => 'PHPCS Warnings', 'phpcs_errors' => 'PHPCS Errors', @@ -175,6 +176,7 @@ PHPCI', 'phpdoccheck_warnings' => 'Missing Docblocks', 'issues' => 'Issues', + 'codeception' => 'Codeception', 'phpcpd' => 'PHP Copy/Paste Detector', 'phpcs' => 'PHP Code Sniffer', 'phpdoccheck' => 'Missing Docblocks', diff --git a/PHPCI/Languages/lang.fr.php b/PHPCI/Languages/lang.fr.php index 9e7cd339..45cc3958 100644 --- a/PHPCI/Languages/lang.fr.php +++ b/PHPCI/Languages/lang.fr.php @@ -167,6 +167,7 @@ PHPCI', 'lines_of_code' => 'Lignes de code', 'build_log' => 'Log du build', 'quality_trend' => 'Tendance de la qualité', + 'codeception_errors' => 'Erreurs Codeception', 'phpmd_warnings' => 'Alertes PHPMD', 'phpcs_warnings' => 'Alertes PHPCS', 'phpcs_errors' => 'Erreurs PHPCS', @@ -175,6 +176,7 @@ PHPCI', 'phpdoccheck_warnings' => 'Blocs de documentation manquants', 'issues' => 'Tickets', + 'codeception' => 'Codeception', 'phpcpd' => 'PHP Copy/Paste Detector', 'phpcs' => 'PHP Code Sniffer', 'phpdoccheck' => 'Missing Docblocks', diff --git a/PHPCI/Languages/lang.it.php b/PHPCI/Languages/lang.it.php index e4b531e0..c5c4e01b 100644 --- a/PHPCI/Languages/lang.it.php +++ b/PHPCI/Languages/lang.it.php @@ -11,7 +11,7 @@ $strings = array( 'language_name' => 'Italiano', 'language' => 'Lingua', - + // Log in: 'log_in_to_phpci' => 'Accedi a PHPCI', 'login_error' => 'Indirizzo email o password errati', @@ -169,6 +169,7 @@ PHPCI', 'lines_of_code' => 'Linee di codice', 'build_log' => 'Log della build', 'quality_trend' => 'Trend della qualità', + 'codeception_errors' => 'Errori di Codeception', 'phpmd_warnings' => 'Avvisi di PHPMD', 'phpcs_warnings' => 'Avvisi di PHPCS', 'phpcs_errors' => 'Errori di PHPCS', @@ -177,6 +178,7 @@ PHPCI', 'phpdoccheck_warnings' => 'Docblocks mancanti', 'issues' => 'Segnalazioni', + 'codeception' => 'Codeception', 'phpcpd' => 'PHP Copy/Paste Detector', 'phpcs' => 'PHP Code Sniffer', 'phpdoccheck' => 'Docblocks mancanti', diff --git a/PHPCI/Languages/lang.nl.php b/PHPCI/Languages/lang.nl.php index e03e709f..f38272c4 100644 --- a/PHPCI/Languages/lang.nl.php +++ b/PHPCI/Languages/lang.nl.php @@ -167,6 +167,7 @@ Services sectie van je Bitbucket repository toegevoegd worden.', 'lines_of_code' => 'Lijnen code', 'build_log' => 'Build Log', 'quality_trend' => 'Kwaliteitstrend', + 'codeception_errors' => 'Codeception Fouten', 'phpmd_warnings' => 'PHPMD Waarschuwingen', 'phpcs_warnings' => 'PHPCS Waarschuwingen', 'phpcs_errors' => 'PHPCS Fouten', @@ -175,6 +176,7 @@ Services sectie van je Bitbucket repository toegevoegd worden.', 'phpdoccheck_warnings' => 'Ontbrekende Docblocks', 'issues' => 'Problemen', + 'codeception' => 'Codeception', 'phpcpd' => 'PHP Copy/Paste Detector', 'phpcs' => 'PHP Code Sniffer', 'phpdoccheck' => 'Ontbrekende Docblocks', diff --git a/PHPCI/Languages/lang.pl.php b/PHPCI/Languages/lang.pl.php index a614d29d..4aafc9cf 100644 --- a/PHPCI/Languages/lang.pl.php +++ b/PHPCI/Languages/lang.pl.php @@ -167,6 +167,7 @@ Services repozytoria Bitbucket.', 'lines_of_code' => 'Linie Kodu', 'build_log' => 'Log Budowania', 'quality_trend' => 'Trend Jakości', + 'codeception_errors' => 'Błędy Codeception', 'phpmd_warnings' => 'Alerty PHPMD', 'phpcs_warnings' => 'Alerty PHPCS', 'phpcs_errors' => 'Błędy PHPCS', @@ -175,6 +176,7 @@ Services repozytoria Bitbucket.', 'phpdoccheck_warnings' => 'Brakuje sekcji DocBlock', 'issues' => 'Problemy', + 'codeception' => 'Codeception', 'phpcpd' => 'PHP Copy/Paste Detector', 'phpcs' => 'PHP Code Sniffer', 'phpdoccheck' => 'Brakuje sekcji DocBlock', diff --git a/PHPCI/Languages/lang.ru.php b/PHPCI/Languages/lang.ru.php index dfef84d9..00b2783e 100644 --- a/PHPCI/Languages/lang.ru.php +++ b/PHPCI/Languages/lang.ru.php @@ -162,6 +162,7 @@ PHPCI', 'lines_of_code' => 'Строк кода', 'build_log' => 'Лог сборки', 'quality_trend' => 'Тенденция качества', + 'codeception_errors' => 'Ошибки Codeception', 'phpmd_warnings' => 'Предупреждения PHPMD', 'phpcs_warnings' => 'Предупреждения PHPCS', 'phpcs_errors' => 'Ошибки PHPCS', @@ -170,6 +171,7 @@ PHPCI', 'phpdoccheck_warnings' => 'Пропущенные Docblocks', 'issues' => 'Проблемы', + 'codeception' => 'Codeception', 'phpcpd' => 'PHP Copy/Paste Detector', 'phpcs' => 'PHP Code Sniffer', 'phpdoccheck' => 'Missing Docblocks', diff --git a/PHPCI/Languages/lang.uk.php b/PHPCI/Languages/lang.uk.php index 24f9cbb2..f51bd885 100644 --- a/PHPCI/Languages/lang.uk.php +++ b/PHPCI/Languages/lang.uk.php @@ -167,14 +167,16 @@ PHPCI', 'lines_of_code' => 'Рядки коду', 'build_log' => 'Лог збірки', 'quality_trend' => 'Тенденція якості', + 'codeception_errors' => 'Помилки Codeception', 'phpmd_warnings' => 'Попередження PHPMD', 'phpcs_warnings' => 'Попередження PHPCS', 'phpcs_errors' => 'Помилки PHPCS', 'phplint_errors' => 'Помилки Lint', - 'phpunit_errors' => 'Помилки PHPCS', + 'phpunit_errors' => 'Помилки PHPUnit', 'phpdoccheck_warnings' => 'Відсутні Docblocks', 'issues' => 'Проблеми', + 'codeception' => 'Codeception', 'phpcpd' => 'PHP Copy/Paste Detector', 'phpcs' => 'PHP Code Sniffer', 'phpdoccheck' => 'Відсутні Docblocks', diff --git a/PHPCI/Plugin/Codeception.php b/PHPCI/Plugin/Codeception.php index 2f9e66fd..7d7fd81e 100644 --- a/PHPCI/Plugin/Codeception.php +++ b/PHPCI/Plugin/Codeception.php @@ -9,37 +9,50 @@ namespace PHPCI\Plugin; +use PHPCI; use PHPCI\Builder; use PHPCI\Helper\Lang; use PHPCI\Model\Build; +use PHPCI\Plugin\Util\TapParser; /** - * Codeception Plugin - Enables full acceptance, unit, and functional testing. + * Codeception Plugin - Enables full acceptance, unit, and functional testing + * * @author Don Gilbert + * @author Igor Timoshenko * @package PHPCI * @subpackage Plugins */ -class Codeception implements \PHPCI\Plugin +class Codeception implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin { /** * @var string */ protected $args = ''; + /** + * @var Build + */ + protected $build; + /** * @var Builder */ protected $phpci; - protected $build; + /** + * @var string|string[] The path (or array of paths) of an yml config for Codeception + */ + protected $configFile; /** - * @var string|string[] $xmlConfigFile The path (or array of paths) of an xml config for PHPUnit + * @var string The path where the reports and logs are stored */ - protected $xmlConfigFile; + protected $logPath = 'tests/_output'; /** * Set up the plugin, configure options, etc. + * * @param Builder $phpci * @param Build $build * @param array $options @@ -50,49 +63,86 @@ class Codeception implements \PHPCI\Plugin $this->build = $build; if (isset($options['config'])) { - $this->xmlConfigFile = $options['config']; + $this->configFile = $options['config']; } + if (isset($options['args'])) { $this->args = (string) $options['args']; } + + if (isset($options['log_path'])) { + $this->logPath = $options['log_path']; + } } /** - * Runs Codeception tests, optionally using specified config file(s). + * {@inheritDoc} */ public function execute() { $success = true; - // Run any config files first. This can be either a single value or an array. - if ($this->xmlConfigFile !== null) { - $success &= $this->runConfigFile($this->xmlConfigFile); + $this->phpci->logExecOutput(false); + + // Run any config files first. This can be either a single value or an array + if ($this->configFile !== null) { + $success &= $this->runConfigFile($this->configFile); } + $tapString = file_get_contents( + $this->phpci->buildPath . $this->logPath . DIRECTORY_SEPARATOR . 'report.tap.log' + ); + + try { + $tapParser = new TapParser($tapString); + $output = $tapParser->parse(); + } catch (\Exception $ex) { + $this->phpci->logFailure($tapString); + + throw $ex; + } + + $failures = $tapParser->getTotalFailures(); + + $this->build->storeMeta('codeception-errors', $failures); + $this->build->storeMeta('codeception-data', $output); + + $this->phpci->logExecOutput(true); + return $success; } /** - * Run tests from a Codeception config file. - * @param $configPath + * {@inheritDoc} + */ + public static function canExecute($stage, Builder $builder, Build $build) + { + return $stage === 'test'; + } + + /** + * Run tests from a Codeception config file + * + * @param string $configPath * @return bool|mixed */ protected function runConfigFile($configPath) { if (is_array($configPath)) { - return $this->recurseArg($configPath, array($this, "runConfigFile")); + return $this->recurseArg($configPath, array($this, 'runConfigFile')); } else { - $codecept = $this->phpci->findBinary('codecept'); if (!$codecept) { $this->phpci->logFailure(Lang::get('could_not_find', 'codecept')); + return false; } - $cmd = 'cd "%s" && ' . $codecept . ' run -c "%s" '. $this->args; + $cmd = 'cd "%s" && ' . $codecept . ' run -c "%s" --tap ' . $this->args; + if (IS_WIN) { - $cmd = 'cd /d "%s" && ' . $codecept . ' run -c "%s" '. $this->args; + $cmd = 'cd /d "%s" && ' . $codecept . ' run -c "%s" --tap ' . $this->args; } $configPath = $this->phpci->buildPath . $configPath; @@ -103,16 +153,18 @@ class Codeception implements \PHPCI\Plugin } /** - * @param $array - * @param $callable + * @param array $array + * @param \Callback $callable * @return bool|mixed */ - protected function recurseArg($array, $callable) + protected function recurseArg(array $array, $callable) { $success = true; + foreach ($array as $subItem) { $success &= call_user_func($callable, $subItem); } + return $success; } } diff --git a/public/assets/js/build-plugins/codeception.js b/public/assets/js/build-plugins/codeception.js new file mode 100644 index 00000000..def42330 --- /dev/null +++ b/public/assets/js/build-plugins/codeception.js @@ -0,0 +1,75 @@ +var codeceptionPlugin = ActiveBuild.UiPlugin.extend({ + id: 'build-codeception-errors', + css: 'col-lg-6 col-md-12 col-sm-12 col-xs-12', + title: Lang.get('codeception'), + lastData: null, + displayOnUpdate: false, + box: true, + rendered: false, + + register: function() { + var self = this; + var query = ActiveBuild.registerQuery('codeception-data', -1, {key: 'codeception-data'}) + + $(window).on('codeception-data', function(data) { + self.onUpdate(data); + }); + + $(window).on('build-updated', function() { + if (!self.rendered) { + self.displayOnUpdate = true; + query(); + } + }); + }, + + render: function() { + + return $('' + + '' + + '' + + ' ' + + '' + + '
    '+Lang.get('test')+'
    '); + }, + + onUpdate: function(e) { + if (!e.queryData) { + $('#build-codeception-errors').hide(); + return; + } + + this.rendered = true; + this.lastData = e.queryData; + + var tests = this.lastData[0].meta_value; + var tbody = $('#codeception-data tbody'); + tbody.empty(); + + if (tests.length == 0) { + $('#build-codeception-errors').hide(); + return; + } + + for (var i in tests) { + + var row = $('' + + ''+tests[i].suite+'' + + '::'+tests[i].test+'
    ' + + ''+(tests[i].message || '')+'' + + ''); + + if (!tests[i].pass) { + row.addClass('danger'); + } else { + row.addClass('success'); + } + + tbody.append(row); + } + + $('#build-codeception-errors').show(); + } +}); + +ActiveBuild.registerPlugin(new codeceptionPlugin()); diff --git a/public/assets/js/build-plugins/warnings.js b/public/assets/js/build-plugins/warnings.js index a2a31dc3..7e8ae0b1 100644 --- a/public/assets/js/build-plugins/warnings.js +++ b/public/assets/js/build-plugins/warnings.js @@ -3,6 +3,7 @@ var warningsPlugin = ActiveBuild.UiPlugin.extend({ css: 'col-lg-6 col-md-6 col-sm-12 col-xs-12', title: Lang.get('quality_trend'), keys: { + 'codeception-errors': Lang.get('codeception_errors'), 'phpmd-warnings': Lang.get('phpmd_warnings'), 'phpcs-warnings': Lang.get('phpcs_warnings'), 'phpcs-errors': Lang.get('phpcs_errors'), @@ -24,7 +25,7 @@ var warningsPlugin = ActiveBuild.UiPlugin.extend({ queries.push(ActiveBuild.registerQuery(key, -1, {num_builds: 10, key: key})); } - $(window).on('phpmd-warnings phpcs-warnings phptallint-warnings phptallint-errors phpcs-errors phplint-errors phpunit-errors phpdoccheck-warnings', function(data) { + $(window).on('codeception-errors phpmd-warnings phpcs-warnings phptallint-warnings phptallint-errors phpcs-errors phplint-errors phpunit-errors phpdoccheck-warnings', function(data) { self.onUpdate(data); }); From 691f2423f78e6122be0e651d0c87066999ad06fc Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Mon, 16 Feb 2015 11:20:18 +0000 Subject: [PATCH 093/329] Fixing base store docblocks. --- PHPCI/Store/Base/BuildMetaStoreBase.php | 29 +++++++++++++++++++++++++ PHPCI/Store/Base/BuildStoreBase.php | 29 +++++++++++++++++++++++++ PHPCI/Store/Base/ProjectStoreBase.php | 21 ++++++++++++++++++ PHPCI/Store/Base/UserStoreBase.php | 20 +++++++++++++++++ 4 files changed, 99 insertions(+) diff --git a/PHPCI/Store/Base/BuildMetaStoreBase.php b/PHPCI/Store/Base/BuildMetaStoreBase.php index 6c4cfc15..7a3d4159 100644 --- a/PHPCI/Store/Base/BuildMetaStoreBase.php +++ b/PHPCI/Store/Base/BuildMetaStoreBase.php @@ -20,11 +20,24 @@ class BuildMetaStoreBase extends Store protected $modelName = '\PHPCI\Model\BuildMeta'; protected $primaryKey = 'id'; + /** + * Returns a BuildMeta model by primary key. + * @param mixed $value + * @param string $useConnection + * @return \@appNamespace\Model\BuildMeta|null + */ public function getByPrimaryKey($value, $useConnection = 'read') { return $this->getById($value, $useConnection); } + /** + * Returns a BuildMeta model by Id. + * @param mixed $value + * @param string $useConnection + * @throws HttpException + * @return \@appNamespace\Model\BuildMeta|null + */ public function getById($value, $useConnection = 'read') { if (is_null($value)) { @@ -44,6 +57,14 @@ class BuildMetaStoreBase extends Store return null; } + /** + * Returns an array of BuildMeta models by ProjectId. + * @param mixed $value + * @param int $limit + * @param string $useConnection + * @throws HttpException + * @return array + */ public function getByProjectId($value, $limit = 1000, $useConnection = 'read') { if (is_null($value)) { @@ -72,6 +93,14 @@ class BuildMetaStoreBase extends Store } } + /** + * Returns an array of BuildMeta models by BuildId. + * @param mixed $value + * @param int $limit + * @param string $useConnection + * @throws HttpException + * @return array + */ public function getByBuildId($value, $limit = 1000, $useConnection = 'read') { if (is_null($value)) { diff --git a/PHPCI/Store/Base/BuildStoreBase.php b/PHPCI/Store/Base/BuildStoreBase.php index 89d3a82f..0560d72b 100644 --- a/PHPCI/Store/Base/BuildStoreBase.php +++ b/PHPCI/Store/Base/BuildStoreBase.php @@ -20,11 +20,24 @@ class BuildStoreBase extends Store protected $modelName = '\PHPCI\Model\Build'; protected $primaryKey = 'id'; + /** + * Returns a Build model by primary key. + * @param mixed $value + * @param string $useConnection + * @return \@appNamespace\Model\Build|null + */ public function getByPrimaryKey($value, $useConnection = 'read') { return $this->getById($value, $useConnection); } + /** + * Returns a Build model by Id. + * @param mixed $value + * @param string $useConnection + * @throws HttpException + * @return \@appNamespace\Model\Build|null + */ public function getById($value, $useConnection = 'read') { if (is_null($value)) { @@ -44,6 +57,14 @@ class BuildStoreBase extends Store return null; } + /** + * Returns an array of Build models by ProjectId. + * @param mixed $value + * @param int $limit + * @param string $useConnection + * @throws HttpException + * @return array + */ public function getByProjectId($value, $limit = 1000, $useConnection = 'read') { if (is_null($value)) { @@ -72,6 +93,14 @@ class BuildStoreBase extends Store } } + /** + * Returns an array of Build models by Status. + * @param mixed $value + * @param int $limit + * @param string $useConnection + * @throws HttpException + * @return array + */ public function getByStatus($value, $limit = 1000, $useConnection = 'read') { if (is_null($value)) { diff --git a/PHPCI/Store/Base/ProjectStoreBase.php b/PHPCI/Store/Base/ProjectStoreBase.php index e5a01cdd..562afba2 100644 --- a/PHPCI/Store/Base/ProjectStoreBase.php +++ b/PHPCI/Store/Base/ProjectStoreBase.php @@ -20,11 +20,24 @@ class ProjectStoreBase extends Store protected $modelName = '\PHPCI\Model\Project'; protected $primaryKey = 'id'; + /** + * Returns a Project model by primary key. + * @param mixed $value + * @param string $useConnection + * @return \@appNamespace\Model\Project|null + */ public function getByPrimaryKey($value, $useConnection = 'read') { return $this->getById($value, $useConnection); } + /** + * Returns a Project model by Id. + * @param mixed $value + * @param string $useConnection + * @throws HttpException + * @return \@appNamespace\Model\Project|null + */ public function getById($value, $useConnection = 'read') { if (is_null($value)) { @@ -44,6 +57,14 @@ class ProjectStoreBase extends Store return null; } + /** + * Returns an array of Project models by Title. + * @param mixed $value + * @param int $limit + * @param string $useConnection + * @throws HttpException + * @return array + */ public function getByTitle($value, $limit = 1000, $useConnection = 'read') { if (is_null($value)) { diff --git a/PHPCI/Store/Base/UserStoreBase.php b/PHPCI/Store/Base/UserStoreBase.php index fd903d8e..d91271d0 100644 --- a/PHPCI/Store/Base/UserStoreBase.php +++ b/PHPCI/Store/Base/UserStoreBase.php @@ -20,11 +20,24 @@ class UserStoreBase extends Store protected $modelName = '\PHPCI\Model\User'; protected $primaryKey = 'id'; + /** + * Returns a User model by primary key. + * @param mixed $value + * @param string $useConnection + * @return \@appNamespace\Model\User|null + */ public function getByPrimaryKey($value, $useConnection = 'read') { return $this->getById($value, $useConnection); } + /** + * Returns a User model by Id. + * @param mixed $value + * @param string $useConnection + * @throws HttpException + * @return \@appNamespace\Model\User|null + */ public function getById($value, $useConnection = 'read') { if (is_null($value)) { @@ -44,6 +57,13 @@ class UserStoreBase extends Store return null; } + /** + * Returns a User model by Email. + * @param mixed $value + * @param string $useConnection + * @throws HttpException + * @return \@appNamespace\Model\User|null + */ public function getByEmail($value, $useConnection = 'read') { if (is_null($value)) { From 0fc18503b22fb8077447a37c23ed34552eeb1705 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Mon, 16 Feb 2015 11:54:52 +0000 Subject: [PATCH 094/329] Fixing install command tests. --- Tests/PHPCI/Command/InstallCommandTest.php | 209 ++++++++++++--------- 1 file changed, 120 insertions(+), 89 deletions(-) diff --git a/Tests/PHPCI/Command/InstallCommandTest.php b/Tests/PHPCI/Command/InstallCommandTest.php index 29c22303..4dd0f133 100644 --- a/Tests/PHPCI/Command/InstallCommandTest.php +++ b/Tests/PHPCI/Command/InstallCommandTest.php @@ -3,7 +3,6 @@ namespace PHPCI\Plugin\Tests\Command; use Symfony\Component\Console\Application; -use PHPCI\Command\InstallCommand; use Prophecy\PhpUnit\ProphecyTestCase; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Console\Helper\HelperSet; @@ -12,17 +11,43 @@ class InstallCommandTest extends ProphecyTestCase { protected $config; protected $admin; - protected $command; - protected $dialog; protected $application; public function setup() { parent::setup(); + $this->application = new Application(); + $this->application->setHelperSet(new HelperSet()); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockBuilder + */ + protected function getDialogHelperMock() + { + // We check that there's no interaction with user. + $dialog = $this->getMockBuilder('Symfony\\Component\\Console\\Helper\\DialogHelper') + ->setMethods(array( + 'ask', + 'askConfirmation', + 'askAndValidate', + 'askHiddenResponse', + 'askHiddenResponseAndValidate', + )) + ->getMock(); + + return $dialog; + } + + /** + * @return \PHPUnit_Framework_MockObject_MockBuilder + */ + protected function getInstallCommandMock() + { // Current command, we need to mock all method that interact with // Database & File system. - $this->command = $this->getMockBuilder('PHPCI\\Command\\InstallCommand') + $command = $this->getMockBuilder('PHPCI\\Command\\InstallCommand') ->setMethods(array( 'reloadConfig', 'verifyNotInstalled', @@ -33,38 +58,27 @@ class InstallCommandTest extends ProphecyTestCase )) ->getMock(); - $this->command->expects($this->once())->method('verifyDatabaseDetails')->willReturn(true); - $this->command->expects($this->once())->method('setupDatabase')->willReturn(true); - $this->command->expects($this->once())->method('createAdminUser')->will( + $command->expects($this->once())->method('verifyNotInstalled')->willReturn(true); + $command->expects($this->once())->method('verifyDatabaseDetails')->willReturn(true); + $command->expects($this->once())->method('setupDatabase')->willReturn(true); + $command->expects($this->once())->method('createAdminUser')->will( $this->returnCallback(function ($adm) {// use (&$admin) { $this->admin = $adm; }) ); - $this->command->expects($this->once())->method('writeConfigFile')->will( + $command->expects($this->once())->method('writeConfigFile')->will( $this->returnCallback(function ($cfg) { //use (&$config) { $this->config = $cfg; }) ); - // We check that there's no interaction with user. - $this->dialog = $this->getMockBuilder('Symfony\\Component\\Console\\Helper\\DialogHelper') - ->setMethods(array( - 'ask', - 'askConfirmation', - 'askAndValidate', - 'askHiddenResponse', - 'askHiddenResponseAndValidate', - )) - ->getMock(); - - $this->application = new Application(); - $this->application->setHelperSet(new HelperSet()); + return $command; } - protected function getCommandTester() + protected function getCommandTester($dialog) { - $this->application->getHelperSet()->set($this->dialog, 'dialog'); - $this->application->add($this->command); + $this->application->getHelperSet()->set($dialog, 'dialog'); + $this->application->add($this->getInstallCommandMock()); $command = $this->application->find('phpci:install'); $commandTester = new CommandTester($command); @@ -91,39 +105,42 @@ class InstallCommandTest extends ProphecyTestCase return $config; } - protected function executeWithoutParam($param = null) + protected function executeWithoutParam($param = null, $dialog) { // Clean result variables. $this->admin = array(); $this->config = array(); // Get tester and execute with extracted parameters. - $commandTester = $this->getCommandTester(); + $commandTester = $this->getCommandTester($dialog); $parameters = $this->getConfig($param); $commandTester->execute($parameters); } - public function testAutomticInstallation() + public function testAutomaticInstallation() { - $this->dialog->expects($this->never())->method('ask'); - $this->dialog->expects($this->never())->method('askConfirmation'); - $this->dialog->expects($this->never())->method('askAndValidate'); - $this->dialog->expects($this->never())->method('askHiddenResponse'); - $this->dialog->expects($this->never())->method('askHiddenResponseAndValidate'); + $dialog = $this->getDialogHelperMock(); + $dialog->expects($this->never())->method('ask'); + $dialog->expects($this->never())->method('askConfirmation'); + $dialog->expects($this->never())->method('askAndValidate'); + $dialog->expects($this->never())->method('askHiddenResponse'); + $dialog->expects($this->never())->method('askHiddenResponseAndValidate'); - $this->executeWithoutParam(); + $this->executeWithoutParam(null, $dialog); } public function testDatabaseHostnameConfig() { - // We specified an input value for hostname. - $this->dialog->expects($this->once())->method('ask')->willReturn('testedvalue'); - $this->dialog->expects($this->never())->method('askConfirmation'); - $this->dialog->expects($this->never())->method('askAndValidate'); - $this->dialog->expects($this->never())->method('askHiddenResponse'); - $this->dialog->expects($this->never())->method('askHiddenResponseAndValidate'); + $dialog = $this->getDialogHelperMock(); - $this->executeWithoutParam('--db-host'); + // We specified an input value for hostname. + $dialog->expects($this->once())->method('ask')->willReturn('testedvalue'); + $dialog->expects($this->never())->method('askConfirmation'); + $dialog->expects($this->never())->method('askAndValidate'); + $dialog->expects($this->never())->method('askHiddenResponse'); + $dialog->expects($this->never())->method('askHiddenResponseAndValidate'); + + $this->executeWithoutParam('--db-host', $dialog); // Check that specified arguments are correctly loaded. $this->assertEquals('testedvalue', $this->config['b8']['database']['servers']['read']); @@ -132,14 +149,16 @@ class InstallCommandTest extends ProphecyTestCase public function testDatabaseNameConfig() { - // We specified an input value for hostname. - $this->dialog->expects($this->once())->method('ask')->willReturn('testedvalue'); - $this->dialog->expects($this->never())->method('askConfirmation'); - $this->dialog->expects($this->never())->method('askAndValidate'); - $this->dialog->expects($this->never())->method('askHiddenResponse'); - $this->dialog->expects($this->never())->method('askHiddenResponseAndValidate'); + $dialog = $this->getDialogHelperMock(); - $this->executeWithoutParam('--db-name'); + // We specified an input value for hostname. + $dialog->expects($this->once())->method('ask')->willReturn('testedvalue'); + $dialog->expects($this->never())->method('askConfirmation'); + $dialog->expects($this->never())->method('askAndValidate'); + $dialog->expects($this->never())->method('askHiddenResponse'); + $dialog->expects($this->never())->method('askHiddenResponseAndValidate'); + + $this->executeWithoutParam('--db-name', $dialog); // Check that specified arguments are correctly loaded. $this->assertEquals('testedvalue', $this->config['b8']['database']['name']); @@ -147,14 +166,16 @@ class InstallCommandTest extends ProphecyTestCase public function testDatabaseUserameConfig() { - // We specified an input value for hostname. - $this->dialog->expects($this->once())->method('ask')->willReturn('testedvalue'); - $this->dialog->expects($this->never())->method('askConfirmation'); - $this->dialog->expects($this->never())->method('askAndValidate'); - $this->dialog->expects($this->never())->method('askHiddenResponse'); - $this->dialog->expects($this->never())->method('askHiddenResponseAndValidate'); + $dialog = $this->getDialogHelperMock(); - $this->executeWithoutParam('--db-user'); + // We specified an input value for hostname. + $dialog->expects($this->once())->method('ask')->willReturn('testedvalue'); + $dialog->expects($this->never())->method('askConfirmation'); + $dialog->expects($this->never())->method('askAndValidate'); + $dialog->expects($this->never())->method('askHiddenResponse'); + $dialog->expects($this->never())->method('askHiddenResponseAndValidate'); + + $this->executeWithoutParam('--db-user', $dialog); // Check that specified arguments are correctly loaded. $this->assertEquals('testedvalue', $this->config['b8']['database']['username']); @@ -162,14 +183,16 @@ class InstallCommandTest extends ProphecyTestCase public function testDatabasePasswordConfig() { - // We specified an input value for hostname. - $this->dialog->expects($this->never())->method('ask'); - $this->dialog->expects($this->never())->method('askConfirmation'); - $this->dialog->expects($this->never())->method('askAndValidate'); - $this->dialog->expects($this->once())->method('askHiddenResponse')->willReturn('testedvalue'); - $this->dialog->expects($this->never())->method('askHiddenResponseAndValidate'); + $dialog = $this->getDialogHelperMock(); - $this->executeWithoutParam('--db-pass'); + // We specified an input value for hostname. + $dialog->expects($this->never())->method('ask'); + $dialog->expects($this->never())->method('askConfirmation'); + $dialog->expects($this->never())->method('askAndValidate'); + $dialog->expects($this->once())->method('askHiddenResponse')->willReturn('testedvalue'); + $dialog->expects($this->never())->method('askHiddenResponseAndValidate'); + + $this->executeWithoutParam('--db-pass', $dialog); // Check that specified arguments are correctly loaded. $this->assertEquals('testedvalue', $this->config['b8']['database']['password']); @@ -177,14 +200,16 @@ class InstallCommandTest extends ProphecyTestCase public function testPhpciUrlConfig() { - // We specified an input value for hostname. - $this->dialog->expects($this->never())->method('ask'); - $this->dialog->expects($this->never())->method('askConfirmation'); - $this->dialog->expects($this->once())->method('askAndValidate')->willReturn('http://testedvalue.com'); - $this->dialog->expects($this->never())->method('askHiddenResponse'); - $this->dialog->expects($this->never())->method('askHiddenResponseAndValidate'); + $dialog = $this->getDialogHelperMock(); - $this->executeWithoutParam('--url'); + // We specified an input value for hostname. + $dialog->expects($this->never())->method('ask'); + $dialog->expects($this->never())->method('askConfirmation'); + $dialog->expects($this->once())->method('askAndValidate')->willReturn('http://testedvalue.com'); + $dialog->expects($this->never())->method('askHiddenResponse'); + $dialog->expects($this->never())->method('askHiddenResponseAndValidate'); + + $this->executeWithoutParam('--url', $dialog); // Check that specified arguments are correctly loaded. $this->assertEquals('http://testedvalue.com', $this->config['phpci']['url']); @@ -192,14 +217,16 @@ class InstallCommandTest extends ProphecyTestCase public function testAdminEmailConfig() { - // We specified an input value for hostname. - $this->dialog->expects($this->never())->method('ask'); - $this->dialog->expects($this->never())->method('askConfirmation'); - $this->dialog->expects($this->once())->method('askAndValidate')->willReturn('test@phpci.com'); - $this->dialog->expects($this->never())->method('askHiddenResponse'); - $this->dialog->expects($this->never())->method('askHiddenResponseAndValidate'); + $dialog = $this->getDialogHelperMock(); - $this->executeWithoutParam('--admin-mail'); + // We specified an input value for hostname. + $dialog->expects($this->never())->method('ask'); + $dialog->expects($this->never())->method('askConfirmation'); + $dialog->expects($this->once())->method('askAndValidate')->willReturn('test@phpci.com'); + $dialog->expects($this->never())->method('askHiddenResponse'); + $dialog->expects($this->never())->method('askHiddenResponseAndValidate'); + + $this->executeWithoutParam('--admin-mail', $dialog); // Check that specified arguments are correctly loaded. $this->assertEquals('test@phpci.com', $this->admin['mail']); @@ -207,14 +234,16 @@ class InstallCommandTest extends ProphecyTestCase public function testAdminUserameConfig() { - // Define expectation for dialog. - $this->dialog->expects($this->once())->method('ask')->willReturn('testedvalue'); - $this->dialog->expects($this->never())->method('askConfirmation'); - $this->dialog->expects($this->never())->method('askAndValidate'); - $this->dialog->expects($this->never())->method('askHiddenResponse'); - $this->dialog->expects($this->never())->method('askHiddenResponseAndValidate'); + $dialog = $this->getDialogHelperMock(); - $this->executeWithoutParam('--admin-name'); + // Define expectation for dialog. + $dialog->expects($this->once())->method('ask')->willReturn('testedvalue'); + $dialog->expects($this->never())->method('askConfirmation'); + $dialog->expects($this->never())->method('askAndValidate'); + $dialog->expects($this->never())->method('askHiddenResponse'); + $dialog->expects($this->never())->method('askHiddenResponseAndValidate'); + + $this->executeWithoutParam('--admin-name', $dialog); // Check that specified arguments are correctly loaded. $this->assertEquals('testedvalue', $this->admin['name']); @@ -222,14 +251,16 @@ class InstallCommandTest extends ProphecyTestCase public function testAdminPasswordConfig() { - // We specified an input value for hostname. - $this->dialog->expects($this->never())->method('ask'); - $this->dialog->expects($this->never())->method('askConfirmation'); - $this->dialog->expects($this->never())->method('askAndValidate'); - $this->dialog->expects($this->once())->method('askHiddenResponse')->willReturn('testedvalue'); - $this->dialog->expects($this->never())->method('askHiddenResponseAndValidate'); + $dialog = $this->getDialogHelperMock(); - $this->executeWithoutParam('--admin-pass'); + // We specified an input value for hostname. + $dialog->expects($this->never())->method('ask'); + $dialog->expects($this->never())->method('askConfirmation'); + $dialog->expects($this->never())->method('askAndValidate'); + $dialog->expects($this->once())->method('askHiddenResponse')->willReturn('testedvalue'); + $dialog->expects($this->never())->method('askHiddenResponseAndValidate'); + + $this->executeWithoutParam('--admin-pass', $dialog); // Check that specified arguments are correctly loaded. $this->assertEquals('testedvalue', $this->admin['pass']); From 4d142b61b6956c61be13bc67e8aace76c48e1001 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Mon, 16 Feb 2015 11:58:15 +0000 Subject: [PATCH 095/329] Fixing other tests --- PHPCI/Plugin/Util/FilesPluginInformation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/Plugin/Util/FilesPluginInformation.php b/PHPCI/Plugin/Util/FilesPluginInformation.php index 33b3e4b3..2593ae22 100644 --- a/PHPCI/Plugin/Util/FilesPluginInformation.php +++ b/PHPCI/Plugin/Util/FilesPluginInformation.php @@ -68,7 +68,7 @@ class FilesPluginInformation implements InstalledPluginInformation public function getPluginClasses() { return array_map( - function (Plugin $plugin) { + function (\stdClass $plugin) { return $plugin->class; }, $this->getInstalledPlugins() From f192185e263de56bb99fec299eb5e4f644de2649 Mon Sep 17 00:00:00 2001 From: Marco Vito Moscaritolo Date: Tue, 11 Nov 2014 22:58:23 +0100 Subject: [PATCH 096/329] Refactor on mail sending to use Email helper and specific tempalte for successfull build. --- PHPCI/Plugin/Email.php | 111 ++++++++++++++------------------- PHPCI/View/Email/success.phtml | 15 +++++ 2 files changed, 61 insertions(+), 65 deletions(-) create mode 100644 PHPCI/View/Email/success.phtml diff --git a/PHPCI/Plugin/Email.php b/PHPCI/Plugin/Email.php index 7c5171d6..85720d7f 100644 --- a/PHPCI/Plugin/Email.php +++ b/PHPCI/Plugin/Email.php @@ -13,6 +13,7 @@ use b8\View; use PHPCI\Builder; use PHPCI\Helper\Lang; use PHPCI\Model\Build; +use PHPCI\Helper\Email as EmailHelper; /** * Email Plugin - Provides simple email capability to PHPCI. @@ -27,21 +28,16 @@ class Email implements \PHPCI\Plugin */ protected $phpci; + /** + * @var \PHPCI\Model\Build + */ + protected $build; + /** * @var array */ protected $options; - /** - * @var \Swift_Mailer - */ - protected $mailer; - - /** - * @var string - */ - protected $fromAddress; - /** * Set up the plugin, configure options, etc. * @param Builder $phpci @@ -52,25 +48,16 @@ class Email implements \PHPCI\Plugin public function __construct( Builder $phpci, Build $build, - \Swift_Mailer $mailer, array $options = array() ) { $this->phpci = $phpci; $this->build = $build; $this->options = $options; - - $phpCiSettings = $phpci->getSystemConfig('phpci'); - - $this->fromAddress = isset($phpCiSettings['email_settings']['from_address']) - ? $phpCiSettings['email_settings']['from_address'] - : "notifications-ci@phptesting.org"; - - $this->mailer = $mailer; } /** - * Connects to MySQL and runs a specified set of queries. - */ + * Send a notificaiton mail. + */ public function execute() { $addresses = $this->getEmailAddresses(); @@ -81,79 +68,73 @@ class Email implements \PHPCI\Plugin return false; } - $subjectTemplate = "PHPCI - %s - %s"; + $buildStatus = $this->build->isSuccessful() ? "Failing Build" : "Passing Build"; $projectName = $this->phpci->getBuildProjectTitle(); - $logText = $this->build->getLog(); + $mailTemplate = $this->build->isSuccessful() ? 'Email/success' : 'Email/failed'; - if ($this->build->isSuccessful()) { - $sendFailures = $this->sendSeparateEmails( - $addresses, - sprintf($subjectTemplate, $projectName, Lang::get('passing_build')), - sprintf(Lang::get('log_output')."
    %s
    ", $logText) - ); - } else { - $view = new View('Email/failed'); - $view->build = $this->build; - $view->project = $this->build->getProject(); + $view = new View($mailTemplate); + $view->build = $this->build; + $view->project = $this->build->getProject(); + $body = $view->render(); - $emailHtml = $view->render(); - - $sendFailures = $this->sendSeparateEmails( - $addresses, - sprintf($subjectTemplate, $projectName, Lang::get('failing_build')), - $emailHtml - ); - } + $sendFailures = $this->sendSeparateEmails( + $addresses, + sprintf("PHPCI - %s - %s", $projectName, $buildStatus), + $body + ); // This is a success if we've not failed to send anything. + $this->phpci->log(sprintf("%d emails sent", (count($addresses) - $sendFailures))); + $this->phpci->log(sprintf("%d emails failed to send", $sendFailures)); - $this->phpci->log(Lang::get('n_emails_sent', (count($addresses) - count($sendFailures)))); - $this->phpci->log(Lang::get('n_emails_failed', count($sendFailures))); - - return (count($sendFailures) == 0); + return ($sendFailures === 0); } /** - * @param string[]|string $toAddresses Array or single address to send to + * @param string $toAddress Single address to send to * @param string[] $ccList * @param string $subject Email subject * @param string $body Email body * @return array Array of failed addresses */ - public function sendEmail($toAddresses, $ccList, $subject, $body) + public function sendEmail($toAddress, $ccList, $subject, $body) { - $message = \Swift_Message::newInstance($subject) - ->setFrom($this->fromAddress) - ->setTo($toAddresses) - ->setBody($body) - ->setContentType("text/html"); + $email = new EmailHelper(); + + $email->setEmailTo($toAddress, $toAddress); + $email->setSubject($subject); + $email->setBody($body); + $email->setIsHtml(true); if (is_array($ccList) && count($ccList)) { - $message->setCc($ccList); + foreach ($ccList as $address) { + $message->addCc($address, $address); + } } - $failedAddresses = array(); - $this->mailer->send($message, $failedAddresses); - - return $failedAddresses; + return $email->send(); } /** - * Send out build status emails. + * Send an email to a list of specified subjects. + * * @param array $toAddresses - * @param $subject - * @param $body - * @return array + * List of destinatary of message. + * @param string $subject + * Mail subject + * @param string $body + * Mail body + * + * @return int number of failed messages */ public function sendSeparateEmails(array $toAddresses, $subject, $body) { - $failures = array(); + $failures = 0; $ccList = $this->getCcAddresses(); foreach ($toAddresses as $address) { - $newFailures = $this->sendEmail($address, $ccList, $subject, $body); - foreach ($newFailures as $failure) { - $failures[] = $failure; + if (!$this->sendEmail($address, $ccList, $subject, $body)) { + $failures++; } } return $failures; diff --git a/PHPCI/View/Email/success.phtml b/PHPCI/View/Email/success.phtml new file mode 100644 index 00000000..a6dfccea --- /dev/null +++ b/PHPCI/View/Email/success.phtml @@ -0,0 +1,15 @@ +
    +
    +
    + getTitle(); ?> - Build #getId(); ?> +
    + +
    +

    Your commit getCommitId(); ?> genrate a successfull build in project getTitle(); ?>.

    + +

    getCommitMessage(); ?>

    +
    getLog(); ?>
    +

    You can review your commit and the build log.

    +
    +
    +
    From 501ca58729e63adcd6f408f1f912f551f5e185af Mon Sep 17 00:00:00 2001 From: Marco Vito Moscaritolo Date: Tue, 11 Nov 2014 22:58:57 +0100 Subject: [PATCH 097/329] Remove unrequire parameters on constructur and fixed error on phpdoc. --- Tests/PHPCI/Plugin/EmailTest.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Tests/PHPCI/Plugin/EmailTest.php b/Tests/PHPCI/Plugin/EmailTest.php index 0fa2d8f0..b53f4a96 100644 --- a/Tests/PHPCI/Plugin/EmailTest.php +++ b/Tests/PHPCI/Plugin/EmailTest.php @@ -30,12 +30,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase protected $mockCiBuilder; /** - * @var \PHPUnit_Framework_MockObject_MockObject $mockMailer - */ - protected $mockMailer; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject $mockMailer + * @var \PHPUnit_Framework_MockObject_MockObject $mockBuild */ protected $mockBuild; @@ -111,7 +106,6 @@ class EmailTest extends \PHPUnit_Framework_TestCase $this->testedEmailPlugin = new EmailPlugin( $this->mockCiBuilder, $this->mockBuild, - $this->mockMailer, $arrOptions ); } @@ -322,4 +316,4 @@ class EmailTest extends \PHPUnit_Framework_TestCase $actualMail->getSubject() ); } -} \ No newline at end of file +} From 33ca150efba7915956ea183950476355ec7e920a Mon Sep 17 00:00:00 2001 From: Marco Vito Moscaritolo Date: Tue, 11 Nov 2014 23:05:35 +0100 Subject: [PATCH 098/329] Added project mocking to allow using mail template on successfull build. --- Tests/PHPCI/Plugin/EmailTest.php | 68 ++++++++++---------------------- 1 file changed, 20 insertions(+), 48 deletions(-) diff --git a/Tests/PHPCI/Plugin/EmailTest.php b/Tests/PHPCI/Plugin/EmailTest.php index b53f4a96..855736f2 100644 --- a/Tests/PHPCI/Plugin/EmailTest.php +++ b/Tests/PHPCI/Plugin/EmailTest.php @@ -34,11 +34,28 @@ class EmailTest extends \PHPUnit_Framework_TestCase */ protected $mockBuild; + /** + * @var \PHPUnit_Framework_MockObject_MockObject $mockProject + */ + protected $mockProject; + public function setUp() { + $this->mockProject = $this->getMock( + '\PHPCI\Model\Project', + array('getTitle'), + array(), + "mockProject", + false + ); + + $this->mockProject->expects($this->any()) + ->method('getTitle') + ->will($this->returnValue("Test project")); + $this->mockBuild = $this->getMock( '\PHPCI\Model\Build', - array('getLog', 'getStatus'), + array('getLog', 'getStatus', 'getProject'), array(), "mockBuild", false @@ -53,8 +70,8 @@ class EmailTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue(\PHPCI\Model\Build::STATUS_SUCCESS)); $this->mockBuild->expects($this->any()) - ->method('getCommitterEmail') - ->will($this->returnValue("committer@test.com")); + ->method('getProject') + ->will($this->returnValue($this->mockProject)); $this->mockCiBuilder = $this->getMock( '\PHPCI\Builder', @@ -149,53 +166,8 @@ class EmailTest extends \PHPUnit_Framework_TestCase ); $this->assertEquals($expectedReturn, $returnValue); - } - /** - * @covers PHPUnit::execute - */ - public function testExecute_UniqueRecipientsFromWithCommitter() - { - $this->loadEmailPluginWithOptions( - array( - 'addresses' => array('test-receiver@example.com', 'test-receiver2@example.com') - ) - ); - $actualMails = []; - $this->catchMailPassedToSend($actualMails); - - $returnValue = $this->testedEmailPlugin->execute(); - $this->assertTrue($returnValue); - - $this->assertCount(2, $actualMails); - - $actualTos = array(key($actualMails[0]->getTo()), key($actualMails[1]->getTo())); - $this->assertContains('test-receiver@example.com', $actualTos); - $this->assertContains('test-receiver2@example.com', $actualTos); - } - - /** - * @covers PHPUnit::execute - */ - public function testExecute_UniqueRecipientsWithCommiter() - { - $this->loadEmailPluginWithOptions( - array( - 'commiter' => true, - 'addresses' => array('test-receiver@example.com', 'committer@test.com') - ) - ); - - $actualMails = []; - $this->catchMailPassedToSend($actualMails); - - $returnValue = $this->testedEmailPlugin->execute(); - $this->assertTrue($returnValue); - - $actualTos = array(key($actualMails[0]->getTo()), key($actualMails[1]->getTo())); - $this->assertContains('test-receiver@example.com', $actualTos); - $this->assertContains('committer@test.com', $actualTos); } /** From f16395e45b12c5f9c015f060fdf86f0579c75606 Mon Sep 17 00:00:00 2001 From: Marco Vito Moscaritolo Date: Wed, 12 Nov 2014 00:20:17 +0100 Subject: [PATCH 099/329] Use more consistent project name loader to compose mail. --- PHPCI/Plugin/Email.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PHPCI/Plugin/Email.php b/PHPCI/Plugin/Email.php index 85720d7f..406caed6 100644 --- a/PHPCI/Plugin/Email.php +++ b/PHPCI/Plugin/Email.php @@ -68,8 +68,8 @@ class Email implements \PHPCI\Plugin return false; } - $buildStatus = $this->build->isSuccessful() ? "Failing Build" : "Passing Build"; - $projectName = $this->phpci->getBuildProjectTitle(); + $buildStatus = $this->build->isSuccessful() ? "Passing Build" : "Failing Build"; + $projectName = $this->build->getProject()->getTitle(); $mailTemplate = $this->build->isSuccessful() ? 'Email/success' : 'Email/failed'; $view = new View($mailTemplate); From 64bd64e07bb55f28ae4bf63efa400a4a3eea97ff Mon Sep 17 00:00:00 2001 From: Marco Vito Moscaritolo Date: Wed, 12 Nov 2014 00:20:55 +0100 Subject: [PATCH 100/329] Added more test case to validate subject, body, status code and mails on messages. --- Tests/PHPCI/Plugin/EmailTest.php | 364 +++++++++++++++++++------------ 1 file changed, 220 insertions(+), 144 deletions(-) diff --git a/Tests/PHPCI/Plugin/EmailTest.php b/Tests/PHPCI/Plugin/EmailTest.php index 855736f2..e311d508 100644 --- a/Tests/PHPCI/Plugin/EmailTest.php +++ b/Tests/PHPCI/Plugin/EmailTest.php @@ -10,6 +10,7 @@ namespace PHPCI\Plugin\Tests; use PHPCI\Plugin\Email as EmailPlugin; +use PHPCI\Model\Build; /** @@ -39,8 +40,26 @@ class EmailTest extends \PHPUnit_Framework_TestCase */ protected $mockProject; + /** + * @var int buildStatus + */ + protected $buildStatus; + + /** + * @var array $message; + */ + protected $message; + + /** + * @var bool $mailDelivered + */ + protected $mailDelivered; + public function setUp() { + $this->message = array(); + $this->mailDelivered = true; + $this->mockProject = $this->getMock( '\PHPCI\Model\Project', array('getTitle'), @@ -51,11 +70,11 @@ class EmailTest extends \PHPUnit_Framework_TestCase $this->mockProject->expects($this->any()) ->method('getTitle') - ->will($this->returnValue("Test project")); + ->will($this->returnValue("Test-Project")); $this->mockBuild = $this->getMock( '\PHPCI\Model\Build', - array('getLog', 'getStatus', 'getProject'), + array('getLog', 'getStatus', 'getProject', 'getCommitterEmail'), array(), "mockBuild", false @@ -67,17 +86,22 @@ class EmailTest extends \PHPUnit_Framework_TestCase $this->mockBuild->expects($this->any()) ->method('getStatus') - ->will($this->returnValue(\PHPCI\Model\Build::STATUS_SUCCESS)); + ->will($this->returnCallback(function () { + return $this->buildStatus; + })); $this->mockBuild->expects($this->any()) ->method('getProject') ->will($this->returnValue($this->mockProject)); + $this->mockBuild->expects($this->any()) + ->method('getCommitterEmail') + ->will($this->returnValue('committer-email@example.com')); + $this->mockCiBuilder = $this->getMock( '\PHPCI\Builder', array( 'getSystemConfig', - 'getBuildProjectTitle', 'getBuild', 'log' ), @@ -100,30 +124,41 @@ class EmailTest extends \PHPUnit_Framework_TestCase ) ) ); - $this->mockCiBuilder->expects($this->any()) - ->method('getBuildProjectTitle') - ->will($this->returnValue('Test-Project')); - $this->mockCiBuilder->expects($this->any()) - ->method('getBuild') - ->will($this->returnValue($this->mockBuild)); - - $this->mockMailer = $this->getMock( - '\Swift_Mailer', - array('send'), - array(), - "mockMailer", - false - ); - - $this->loadEmailPluginWithOptions(); } - protected function loadEmailPluginWithOptions($arrOptions = array()) + protected function loadEmailPluginWithOptions($arrOptions = array(), $buildStatus = null, $mailDelivered = true) { - $this->testedEmailPlugin = new EmailPlugin( - $this->mockCiBuilder, - $this->mockBuild, - $arrOptions + $this->mailDelivered = $mailDelivered; + + if (is_null($buildStatus)) { + $this->buildStatus = Build::STATUS_SUCCESS; + } else { + $this->buildStatus = $buildStatus; + } + + // Reset current message. + $this->message = array(); + + $this->testedEmailPlugin = $this->getMock( + '\PHPCI\Plugin\Email', + array('sendEmail'), + array( + $this->mockCiBuilder, + $this->mockBuild, + $arrOptions + ) + ); + + $this->testedEmailPlugin->expects($this->any()) + ->method('sendEmail') + ->will($this->returnCallback(function ($to, $cc, $subject, $body) { + $this->message['to'][] = $to; + $this->message['cc'] = $cc; + $this->message['subject'] = $subject; + $this->message['body'] = $body; + + return $this->mailDelivered; + }) ); } @@ -132,7 +167,10 @@ class EmailTest extends \PHPUnit_Framework_TestCase */ public function testExecute_ReturnsFalseWithoutArgs() { + $this->loadEmailPluginWithOptions(); + $returnValue = $this->testedEmailPlugin->execute(); + // As no addresses will have been mailed as non are configured. $expectedReturn = false; @@ -147,145 +185,183 @@ class EmailTest extends \PHPUnit_Framework_TestCase $this->loadEmailPluginWithOptions( array( 'addresses' => array('test-receiver@example.com') - ) + ), + Build::STATUS_SUCCESS ); - /** @var \Swift_Message $actualMail */ - $actualMail = null; - $this->catchMailPassedToSend($actualMail); - $returnValue = $this->testedEmailPlugin->execute(); - $expectedReturn = true; - $this->assertSystemMail( - 'test-receiver@example.com', - 'test-from-address@example.com', - "Log Output:
    Build Log
    ", - "PHPCI - Test-Project - Passing Build", - $actualMail + $this->assertContains('test-receiver@example.com', $this->message['to']); + } + + /** + * @covers PHPUnit::execute + */ + public function testExecute_BuildsDefaultEmails() + { + $this->loadEmailPluginWithOptions( + array( + 'default_mailto_address' => 'default-mailto-address@example.com' + ), + Build::STATUS_SUCCESS ); - $this->assertEquals($expectedReturn, $returnValue); - + $returnValue = $this->testedEmailPlugin->execute(); + $this->assertContains('default-mailto-address@example.com', $this->message['to']); } /** - * @covers PHPUnit::sendEmail + * @covers PHPUnit::execute */ - public function testSendEmail_CallsMailerSend() + public function testExecute_CcDefaultEmails() { - $this->mockMailer->expects($this->once()) - ->method('send'); - $this->testedEmailPlugin->sendEmail("test@email.com", array(), "hello", "body"); - } - - /** - * @covers PHPUnit::sendEmail - */ - public function testSendEmail_BuildsAMessageObject() - { - $subject = "Test mail"; - $body = "Message Body"; - $toAddress = "test@example.com"; - - $this->mockMailer->expects($this->once()) - ->method('send') - ->with($this->isInstanceOf('\Swift_Message'), $this->anything()); - $this->testedEmailPlugin->sendEmail($toAddress, array(), $subject, $body); - } - - /** - * @covers PHPUnit::sendEmail - */ - public function testSendEmail_BuildsExpectedMessage() - { - $subject = "Test mail"; - $body = "Message Body"; - $toAddress = "test@example.com"; - $expectedMessage = \Swift_Message::newInstance($subject) - ->setFrom('test-from-address@example.com') - ->setTo($toAddress) - ->setBody($body); - - /** @var \Swift_Message $actualMail */ - $actualMail = null; - $this->catchMailPassedToSend($actualMail); - - $this->testedEmailPlugin->sendEmail($toAddress, array(), $subject, $body); - - $this->assertSystemMail( - $toAddress, - 'test-from-address@example.com', - $body, - $subject, - $actualMail + $this->loadEmailPluginWithOptions( + array( + 'default_mailto_address' => 'default-mailto-address@example.com', + 'cc' => array( + 'cc-email-1@example.com', + 'cc-email-2@example.com', + 'cc-email-3@example.com', + ), + ), + Build::STATUS_SUCCESS ); - } - /** - * @param \Swift_Message $actualMail passed by ref and populated with - * the message object the mock mailer - * receives. - */ - protected function catchMailPassedToSend(&$actualMail) - { - $this->mockMailer->expects(is_array($actualMail) ? $this->atLeast(1) : $this->once()) - ->method('send') - ->will( - $this->returnCallback( - function ($passedMail) use (&$actualMail) { - if(is_array($actualMail)) { - $actualMail[] = $passedMail; - } else { - $actualMail = $passedMail; - } - return array(); - } - ) - ); - } - - /** - * Asserts that the actual mail object is populated as expected. - * - * @param string $expectedToAddress - * @param $expectedFromAddress - * @param string $expectedBody - * @param string $expectedSubject - * @param \Swift_Message $actualMail - */ - protected function assertSystemMail( - $expectedToAddress, - $expectedFromAddress, - $expectedBody, - $expectedSubject, - $actualMail - ) { - if (!($actualMail instanceof \Swift_Message)) { - $type = is_object($actualMail) ? get_class($actualMail) : gettype( - $actualMail - ); - throw new \Exception("Expected Swift_Message got " . $type); - } - $this->assertEquals( - array($expectedFromAddress => null), - $actualMail->getFrom() - ); + $this->testedEmailPlugin->execute(); $this->assertEquals( - array($expectedToAddress => null), - $actualMail->getTo() + array( + 'cc-email-1@example.com', + 'cc-email-2@example.com', + 'cc-email-3@example.com', + ), + $this->message['cc'] + ); + } + + /** + * @covers PHPUnit::execute + */ + public function testExecute_BuildsCommitterEmails() + { + $this->loadEmailPluginWithOptions( + array( + 'committer' => true + ), + Build::STATUS_SUCCESS ); - $this->assertEquals( - $expectedBody, - $actualMail->getBody() + $this->testedEmailPlugin->execute(); + + $this->assertContains('committer-email@example.com', $this->message['to']); + } + + /** + * @covers PHPUnit::execute + */ + public function testExecute_MailSuccessfulBuildHaveProjectName() + { + $this->loadEmailPluginWithOptions( + array( + 'addresses' => array('test-receiver@example.com') + ), + Build::STATUS_SUCCESS ); - $this->assertEquals( - $expectedSubject, - $actualMail->getSubject() + $returnValue = $this->testedEmailPlugin->execute(); + + $this->assertContains('Test-Project', $this->message['subject']); + $this->assertContains('Test-Project', $this->message['body']); + } + + /** + * @covers PHPUnit::execute + */ + public function testExecute_MailFailingBuildHaveProjectName() + { + $this->loadEmailPluginWithOptions( + array( + 'addresses' => array('test-receiver@example.com') + ), + Build::STATUS_FAILED ); + + $returnValue = $this->testedEmailPlugin->execute(); + + $this->assertContains('Test-Project', $this->message['subject']); + $this->assertContains('Test-Project', $this->message['body']); + } + + /** + * @covers PHPUnit::execute + */ + public function testExecute_MailSuccessfulBuildHaveStatus() + { + $this->loadEmailPluginWithOptions( + array( + 'addresses' => array('test-receiver@example.com') + ), + Build::STATUS_SUCCESS + ); + + $returnValue = $this->testedEmailPlugin->execute(); + + $this->assertContains('Passing', $this->message['subject']); + $this->assertContains('successfull', $this->message['body']); + } + + /** + * @covers PHPUnit::execute + */ + public function testExecute_MailFailingBuildHaveStatus() + { + $this->loadEmailPluginWithOptions( + array( + 'addresses' => array('test-receiver@example.com') + ), + Build::STATUS_FAILED + ); + + $returnValue = $this->testedEmailPlugin->execute(); + + $this->assertContains('Failing', $this->message['subject']); + $this->assertContains('failed', $this->message['body']); + } + + /** + * @covers PHPUnit::execute + */ + public function testExecute_MailDeliverySuccess() + { + $this->loadEmailPluginWithOptions( + array( + 'addresses' => array('test-receiver@example.com') + ), + Build::STATUS_FAILED, + true + ); + + $returnValue = $this->testedEmailPlugin->execute(); + + $this->assertEquals(true, $returnValue); + } + + /** + * @covers PHPUnit::execute + */ + public function testExecute_MailDeliveryFail() + { + $this->loadEmailPluginWithOptions( + array( + 'addresses' => array('test-receiver@example.com') + ), + Build::STATUS_FAILED, + false + ); + + $returnValue = $this->testedEmailPlugin->execute(); + + $this->assertEquals(false, $returnValue); } } From 2aff5ab89670bb14fe0a42e2cee99c6fb6894c87 Mon Sep 17 00:00:00 2001 From: Marco Vito Moscaritolo Date: Wed, 12 Nov 2014 13:37:52 +0100 Subject: [PATCH 101/329] Fixed CS. --- Tests/PHPCI/Plugin/EmailTest.php | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/Tests/PHPCI/Plugin/EmailTest.php b/Tests/PHPCI/Plugin/EmailTest.php index e311d508..791123e4 100644 --- a/Tests/PHPCI/Plugin/EmailTest.php +++ b/Tests/PHPCI/Plugin/EmailTest.php @@ -12,7 +12,6 @@ namespace PHPCI\Plugin\Tests; use PHPCI\Plugin\Email as EmailPlugin; use PHPCI\Model\Build; - /** * Unit test for the PHPUnit plugin. * @author meadsteve @@ -158,14 +157,13 @@ class EmailTest extends \PHPUnit_Framework_TestCase $this->message['body'] = $body; return $this->mailDelivered; - }) - ); + })); } /** * @covers PHPUnit::execute */ - public function testExecute_ReturnsFalseWithoutArgs() + public function testReturnsFalseWithoutArgs() { $this->loadEmailPluginWithOptions(); @@ -180,7 +178,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase /** * @covers PHPUnit::execute */ - public function testExecute_BuildsBasicEmails() + public function testBuildsBasicEmails() { $this->loadEmailPluginWithOptions( array( @@ -197,7 +195,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase /** * @covers PHPUnit::execute */ - public function testExecute_BuildsDefaultEmails() + public function testBuildsDefaultEmails() { $this->loadEmailPluginWithOptions( array( @@ -214,7 +212,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase /** * @covers PHPUnit::execute */ - public function testExecute_CcDefaultEmails() + public function testCcDefaultEmails() { $this->loadEmailPluginWithOptions( array( @@ -243,7 +241,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase /** * @covers PHPUnit::execute */ - public function testExecute_BuildsCommitterEmails() + public function testBuildsCommitterEmails() { $this->loadEmailPluginWithOptions( array( @@ -260,7 +258,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase /** * @covers PHPUnit::execute */ - public function testExecute_MailSuccessfulBuildHaveProjectName() + public function testMailSuccessfulBuildHaveProjectName() { $this->loadEmailPluginWithOptions( array( @@ -278,7 +276,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase /** * @covers PHPUnit::execute */ - public function testExecute_MailFailingBuildHaveProjectName() + public function testMailFailingBuildHaveProjectName() { $this->loadEmailPluginWithOptions( array( @@ -296,7 +294,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase /** * @covers PHPUnit::execute */ - public function testExecute_MailSuccessfulBuildHaveStatus() + public function testMailSuccessfulBuildHaveStatus() { $this->loadEmailPluginWithOptions( array( @@ -314,7 +312,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase /** * @covers PHPUnit::execute */ - public function testExecute_MailFailingBuildHaveStatus() + public function testMailFailingBuildHaveStatus() { $this->loadEmailPluginWithOptions( array( @@ -332,7 +330,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase /** * @covers PHPUnit::execute */ - public function testExecute_MailDeliverySuccess() + public function testMailDeliverySuccess() { $this->loadEmailPluginWithOptions( array( @@ -350,7 +348,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase /** * @covers PHPUnit::execute */ - public function testExecute_MailDeliveryFail() + public function testMailDeliveryFail() { $this->loadEmailPluginWithOptions( array( From 2d194de6d0267e3312407506723fdf033f31a2d2 Mon Sep 17 00:00:00 2001 From: Marco Vito Moscaritolo Date: Wed, 12 Nov 2014 13:44:56 +0100 Subject: [PATCH 102/329] Fixed code in CC mails. --- PHPCI/Plugin/Email.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/Plugin/Email.php b/PHPCI/Plugin/Email.php index 406caed6..fa2a4e85 100644 --- a/PHPCI/Plugin/Email.php +++ b/PHPCI/Plugin/Email.php @@ -108,7 +108,7 @@ class Email implements \PHPCI\Plugin if (is_array($ccList) && count($ccList)) { foreach ($ccList as $address) { - $message->addCc($address, $address); + $email->addCc($address, $address); } } From 74b0513dd5fd88e297dc8d5ea2c687b69c9a4c87 Mon Sep 17 00:00:00 2001 From: Marco Vito Moscaritolo Date: Wed, 12 Nov 2014 13:51:01 +0100 Subject: [PATCH 103/329] Remove unused code. --- Tests/PHPCI/Plugin/EmailTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/PHPCI/Plugin/EmailTest.php b/Tests/PHPCI/Plugin/EmailTest.php index 791123e4..183d17e5 100644 --- a/Tests/PHPCI/Plugin/EmailTest.php +++ b/Tests/PHPCI/Plugin/EmailTest.php @@ -187,7 +187,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase Build::STATUS_SUCCESS ); - $returnValue = $this->testedEmailPlugin->execute(); + $this->testedEmailPlugin->execute(); $this->assertContains('test-receiver@example.com', $this->message['to']); } @@ -204,7 +204,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase Build::STATUS_SUCCESS ); - $returnValue = $this->testedEmailPlugin->execute(); + $this->testedEmailPlugin->execute(); $this->assertContains('default-mailto-address@example.com', $this->message['to']); } @@ -267,7 +267,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase Build::STATUS_SUCCESS ); - $returnValue = $this->testedEmailPlugin->execute(); + $this->testedEmailPlugin->execute(); $this->assertContains('Test-Project', $this->message['subject']); $this->assertContains('Test-Project', $this->message['body']); @@ -285,7 +285,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase Build::STATUS_FAILED ); - $returnValue = $this->testedEmailPlugin->execute(); + $this->testedEmailPlugin->execute(); $this->assertContains('Test-Project', $this->message['subject']); $this->assertContains('Test-Project', $this->message['body']); @@ -303,7 +303,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase Build::STATUS_SUCCESS ); - $returnValue = $this->testedEmailPlugin->execute(); + $this->testedEmailPlugin->execute(); $this->assertContains('Passing', $this->message['subject']); $this->assertContains('successfull', $this->message['body']); @@ -321,7 +321,7 @@ class EmailTest extends \PHPUnit_Framework_TestCase Build::STATUS_FAILED ); - $returnValue = $this->testedEmailPlugin->execute(); + $this->testedEmailPlugin->execute(); $this->assertContains('Failing', $this->message['subject']); $this->assertContains('failed', $this->message['body']); From 5b754fe07c757bfeefbfb348b71961f7e56ba20e Mon Sep 17 00:00:00 2001 From: Marco Vito Moscaritolo Date: Thu, 25 Dec 2014 21:49:53 +0100 Subject: [PATCH 104/329] Fixed config path for tests. --- Tests/bootstrap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/bootstrap.php b/Tests/bootstrap.php index a8b71034..f7cbd630 100644 --- a/Tests/bootstrap.php +++ b/Tests/bootstrap.php @@ -17,7 +17,7 @@ require_once(dirname(__DIR__) . '/vendor/autoload.php'); $conf = array(); $conf['b8']['app']['namespace'] = 'PHPCI'; $conf['b8']['app']['default_controller'] = 'Home'; -$conf['b8']['view']['path'] = dirname(__DIR__) . '/PHPCI/View/'; +$conf['b8']['view']['path'] = dirname(__DIR__) . '/../PHPCI/View/'; // If the PHPCI config file is not where we expect it, try looking in // env for an alternative config path. From 885f947da804b4324b436eb8cf123d54b720e490 Mon Sep 17 00:00:00 2001 From: Marco Vito Moscaritolo Date: Thu, 25 Dec 2014 22:20:11 +0100 Subject: [PATCH 105/329] Update loader config for test. --- Tests/bootstrap.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/bootstrap.php b/Tests/bootstrap.php index f7cbd630..601c7d7b 100644 --- a/Tests/bootstrap.php +++ b/Tests/bootstrap.php @@ -13,12 +13,6 @@ date_default_timezone_set(@date_default_timezone_get()); // Load Composer autoloader: require_once(dirname(__DIR__) . '/vendor/autoload.php'); -// Load configuration if present: -$conf = array(); -$conf['b8']['app']['namespace'] = 'PHPCI'; -$conf['b8']['app']['default_controller'] = 'Home'; -$conf['b8']['view']['path'] = dirname(__DIR__) . '/../PHPCI/View/'; - // If the PHPCI config file is not where we expect it, try looking in // env for an alternative config path. $configFile = dirname(__FILE__) . '/../PHPCI/config.yml'; @@ -31,6 +25,12 @@ if (!file_exists($configFile)) { } } +// Load configuration if present: +$conf = array(); +$conf['b8']['app']['namespace'] = 'PHPCI'; +$conf['b8']['app']['default_controller'] = 'Home'; +$conf['b8']['view']['path'] = dirname(__DIR__) . '/PHPCI/View/'; + $config = new b8\Config($conf); if (file_exists($configFile)) { From cbd98b6b46a637b48712d2d1f92973b2b013c3b8 Mon Sep 17 00:00:00 2001 From: Marco Vito Moscaritolo Date: Thu, 5 Feb 2015 14:27:26 +0100 Subject: [PATCH 106/329] Fixed typo. --- PHPCI/Plugin/Email.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/Plugin/Email.php b/PHPCI/Plugin/Email.php index fa2a4e85..02987e9b 100644 --- a/PHPCI/Plugin/Email.php +++ b/PHPCI/Plugin/Email.php @@ -56,7 +56,7 @@ class Email implements \PHPCI\Plugin } /** - * Send a notificaiton mail. + * Send a notification mail. */ public function execute() { From 2255767dc1e14b9d5fc3f7f2862a66e721dd95b3 Mon Sep 17 00:00:00 2001 From: Marco Vito Moscaritolo Date: Fri, 6 Feb 2015 19:09:37 +0100 Subject: [PATCH 107/329] Fixed test that use genreated mail to see information. --- Tests/PHPCI/Plugin/EmailTest.php | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Tests/PHPCI/Plugin/EmailTest.php b/Tests/PHPCI/Plugin/EmailTest.php index 183d17e5..965b23c4 100644 --- a/Tests/PHPCI/Plugin/EmailTest.php +++ b/Tests/PHPCI/Plugin/EmailTest.php @@ -209,6 +209,45 @@ class EmailTest extends \PHPUnit_Framework_TestCase $this->assertContains('default-mailto-address@example.com', $this->message['to']); } + /** + * @covers PHPUnit::execute + */ + public function testExecute_UniqueRecipientsFromWithCommitter() + { + $this->loadEmailPluginWithOptions( + array( + 'addresses' => array('test-receiver@example.com', 'test-receiver2@example.com') + ) + ); + + $returnValue = $this->testedEmailPlugin->execute(); + $this->assertTrue($returnValue); + + $this->assertCount(2, $this->message['to']); + + $this->assertContains('test-receiver@example.com', $this->message['to']); + $this->assertContains('test-receiver2@example.com', $this->message['to']); + } + + /** + * @covers PHPUnit::execute + */ + public function testExecute_UniqueRecipientsWithCommiter() + { + $this->loadEmailPluginWithOptions( + array( + 'commiter' => true, + 'addresses' => array('test-receiver@example.com', 'committer@test.com') + ) + ); + + $returnValue = $this->testedEmailPlugin->execute(); + $this->assertTrue($returnValue); + + $this->assertContains('test-receiver@example.com', $this->message['to']); + $this->assertContains('committer@test.com', $this->message['to']); + } + /** * @covers PHPUnit::execute */ From 7c4a9261660598355a34647bd161fe3e4a55cb3a Mon Sep 17 00:00:00 2001 From: Tobias van Beek Date: Mon, 16 Feb 2015 15:00:37 +0100 Subject: [PATCH 108/329] Render the public key on the project edit page. CLose #722 done by @leewillis77 --- PHPCI/Controller/ProjectController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/Controller/ProjectController.php b/PHPCI/Controller/ProjectController.php index b263f06e..8457a7bb 100644 --- a/PHPCI/Controller/ProjectController.php +++ b/PHPCI/Controller/ProjectController.php @@ -268,7 +268,7 @@ class ProjectController extends PHPCI\Controller $view->type = 'edit'; $view->project = $project; $view->form = $form; - $view->key = null; + $view->key = $values['pubkey']; return $view->render(); } From 33f2ec172d594da2f49da8802c02dd01a9d3c43a Mon Sep 17 00:00:00 2001 From: Daniel Wolkenhauer Date: Mon, 16 Feb 2015 18:51:05 +0100 Subject: [PATCH 109/329] Fatal error: Call to undefined method PHPCI\Helper\Email::setIsHtml() in ../Email.php on line 107 --- PHPCI/Plugin/Email.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/Plugin/Email.php b/PHPCI/Plugin/Email.php index 02987e9b..9e361d91 100644 --- a/PHPCI/Plugin/Email.php +++ b/PHPCI/Plugin/Email.php @@ -104,7 +104,7 @@ class Email implements \PHPCI\Plugin $email->setEmailTo($toAddress, $toAddress); $email->setSubject($subject); $email->setBody($body); - $email->setIsHtml(true); + $email->setHtml(true); if (is_array($ccList) && count($ccList)) { foreach ($ccList as $address) { From 8f5d855193e6b67cd042ac61c8d9553359d6b8d5 Mon Sep 17 00:00:00 2001 From: James Inman Date: Wed, 18 Feb 2015 14:07:26 +0000 Subject: [PATCH 110/329] Adding Technical Debt plugin. --- PHPCI/Languages/lang.en.php | 1 + PHPCI/Plugin/TechnicalDebt.php | 183 ++++++++++++++++++ .../assets/js/build-plugins/technical_debt.js | 79 ++++++++ 3 files changed, 263 insertions(+) create mode 100755 PHPCI/Plugin/TechnicalDebt.php create mode 100755 public/assets/js/build-plugins/technical_debt.js diff --git a/PHPCI/Languages/lang.en.php b/PHPCI/Languages/lang.en.php index 9718e442..5f21d3dc 100644 --- a/PHPCI/Languages/lang.en.php +++ b/PHPCI/Languages/lang.en.php @@ -183,6 +183,7 @@ PHPCI', 'phpmd' => 'PHP Mess Detector', 'phpspec' => 'PHP Spec', 'phpunit' => 'PHP Unit', + 'technical_debt' => 'Technical Debt', 'file' => 'File', 'line' => 'Line', diff --git a/PHPCI/Plugin/TechnicalDebt.php b/PHPCI/Plugin/TechnicalDebt.php new file mode 100755 index 00000000..4d82dd25 --- /dev/null +++ b/PHPCI/Plugin/TechnicalDebt.php @@ -0,0 +1,183 @@ + +* @package PHPCI +* @subpackage Plugins +*/ +class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin +{ + /** + * @var \PHPCI\Builder + */ + protected $phpci; + + /** + * @var array + */ + protected $suffixes; + + /** + * @var string + */ + protected $directory; + + /** + * @var int + */ + protected $allowed_errors; + + /** + * @var int + */ + protected $allowed_warnings; + + /** + * @var string, based on the assumption the root may not hold the code to be + * tested, extends the base path + */ + protected $path; + + /** + * @var array - paths to ignore + */ + protected $ignore; + + /** + * @var array - terms to search for + */ + protected $searches; + + public static function canExecute($stage, Builder $builder, Build $build) + { + if ($stage == 'test') { + return true; + } + + return false; + } + + /** + * @param \PHPCI\Builder $phpci + * @param \PHPCI\Model\Build $build + * @param array $options + */ + public function __construct(Builder $phpci, Build $build, array $options = array()) + { + $this->phpci = $phpci; + $this->build = $build; + $this->suffixes = array('php'); + $this->directory = $phpci->buildPath; + $this->path = ''; + $this->ignore = $this->phpci->ignore; + $this->allowed_warnings = 0; + $this->allowed_errors = 0; + $this->searches = array('TODO', 'FIXME', 'TO DO', 'FIX ME'); + + if (isset($options['searches']) && is_array($options['searches'])) { + $this->searches = $options['searches']; + } + + if (isset($options['zero_config']) && $options['zero_config']) { + $this->allowed_warnings = -1; + $this->allowed_errors = -1; + } + } + + protected function setOptions($options) + { + foreach (array('directory', 'path', 'ignore', 'allowed_warnings', 'allowed_errors') as $key) { + if (array_key_exists($key, $options)) { + $this->{$key} = $options[$key]; + } + } + } + + /** + * Runs in a specified directory, to a specified standard. + */ + public function execute() + { + $this->phpci->logExecOutput(false); + + $ignores = $this->ignore; + $ignores[] = 'phpci.yml'; + + $dirIterator = new \RecursiveDirectoryIterator($this->directory); + $iterator = new \RecursiveIteratorIterator($dirIterator, \RecursiveIteratorIterator::SELF_FIRST); + $files = []; + + foreach ($iterator as $file) { + $filePath = $file->getRealPath(); + $skipFile = false; + foreach ($ignores as $ignore) { + if (stripos($filePath, $ignore) !== false) { + $skipFile = true; + break; + } + } + + // Ignore hidden files, else .git, .sass_cache, etc. all get looped over + if (stripos($filePath, '/.') !== false) { + $skipFile = true; + } + + if ($skipFile == false) { + $files[] = $file->getRealPath(); + } + } + + $files = array_filter(array_unique($files)); + $errorCount = 0; + $data = array(); + + foreach ($files as $file) { + foreach ($this->searches as $search) { + $fileContent = file_get_contents($file); + $allLines = explode(PHP_EOL, $fileContent); + $beforeString = strstr($fileContent, $search, true); + + if (false !== $beforeString) { + $lines = explode(PHP_EOL, $beforeString); + $lineNumber = count($lines); + $content = trim($allLines[$lineNumber - 1]); + + $errorCount++; + $this->phpci->log("Found $search on line $lineNumber of $file:\n$content"); + $data[] = array( + 'file' => str_replace($this->directory, '', $file), + 'line' => $lineNumber, + 'message' => $content + ); + } + } + } + + $this->phpci->log("Found $errorCount instances of " . implode(', ', $this->searches)); + + $this->build->storeMeta('technical_debt-warnings', $errorCount); + $this->build->storeMeta('technical_debt-data', $data); + + if ($this->allowed_errors != -1 && $errorCount > $this->allowed_errors) { + $success = false; + } + + return $success; + } +} + diff --git a/public/assets/js/build-plugins/technical_debt.js b/public/assets/js/build-plugins/technical_debt.js new file mode 100755 index 00000000..a187401d --- /dev/null +++ b/public/assets/js/build-plugins/technical_debt.js @@ -0,0 +1,79 @@ +var TechnicalDebtPlugin = ActiveBuild.UiPlugin.extend({ + id: 'build-technical_debt', + css: 'col-lg-6 col-md-12 col-sm-12 col-xs-12', + title: Lang.get('technical_debt'), + lastData: null, + box: true, + rendered: false, + + register: function() { + var self = this; + var query = ActiveBuild.registerQuery('technical_debt-data', -1, {key: 'technical_debt-data'}) + + $(window).on('technical_debt-data', function(data) { + self.onUpdate(data); + }); + + $(window).on('build-updated', function() { + if (!self.rendered) { + query(); + } + }); + }, + + render: function() { + return $('
    ' + + '' + + '' + + ' ' + + ' ' + + ' ' + + '' + + '
    '+Lang.get('file')+''+Lang.get('line')+''+Lang.get('message')+'
    '); + }, + + onUpdate: function(e) { + if (!e.queryData) { + $('#build-technical_debt').hide(); + return; + } + + this.rendered = true; + this.lastData = e.queryData; + + var errors = this.lastData[0].meta_value; + var tbody = $('#technical_debt-data tbody'); + tbody.empty(); + + if (errors.length == 0) { + $('#build-technical_debt').hide(); + return; + } + + for (var i in errors) { + var file = errors[i].file; + + if (ActiveBuild.fileLinkTemplate) { + var fileLink = ActiveBuild.fileLinkTemplate.replace('{FILE}', file); + fileLink = fileLink.replace('{LINE}', errors[i].line); + + file = '' + file + ''; + } + + var row = $('' + + ''+file+'' + + ''+errors[i].line+'' + + ''+errors[i].message+''); + + if (errors[i].type == 'ERROR') { + row.addClass('danger'); + } + + tbody.append(row); + } + + $('#build-technical_debt').show(); + } +}); + +ActiveBuild.registerPlugin(new TechnicalDebtPlugin()); From 2a5ac8ccbcd478de08f036c5ababd4099c5b49e8 Mon Sep 17 00:00:00 2001 From: James Inman Date: Wed, 18 Feb 2015 14:15:59 +0000 Subject: [PATCH 111/329] PHPCS/DocBlock fixes to Technical Debt plugin. --- PHPCI/Plugin/TechnicalDebt.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/PHPCI/Plugin/TechnicalDebt.php b/PHPCI/Plugin/TechnicalDebt.php index 4d82dd25..408f40fb 100755 --- a/PHPCI/Plugin/TechnicalDebt.php +++ b/PHPCI/Plugin/TechnicalDebt.php @@ -63,6 +63,15 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin */ protected $searches; + + /** + * Check if this plugin can be executed. + * + * @param $stage + * @param Builder $builder + * @param Build $build + * @return bool + */ public static function canExecute($stage, Builder $builder, Build $build) { if ($stage == 'test') { @@ -99,6 +108,10 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin } } + /** + * Handle this plugin's options. + * @param $options + */ protected function setOptions($options) { foreach (array('directory', 'path', 'ignore', 'allowed_warnings', 'allowed_errors') as $key) { @@ -123,7 +136,7 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin $files = []; foreach ($iterator as $file) { - $filePath = $file->getRealPath(); + $filePath = $file->getRealPath(); $skipFile = false; foreach ($ignores as $ignore) { if (stripos($filePath, $ignore) !== false) { @@ -168,7 +181,7 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin } } - $this->phpci->log("Found $errorCount instances of " . implode(', ', $this->searches)); + $this->phpci->log("Found $errorCount instances of " . implode(', ', $this->searches)); $this->build->storeMeta('technical_debt-warnings', $errorCount); $this->build->storeMeta('technical_debt-data', $data); @@ -180,4 +193,3 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin return $success; } } - From d6f72b0b7e66c74ae70db440d379a3000637279d Mon Sep 17 00:00:00 2001 From: James Inman Date: Wed, 18 Feb 2015 14:26:21 +0000 Subject: [PATCH 112/329] PHPCS/PHPMD fixes for Technical Debt plugin. --- PHPCI/Plugin/TechnicalDebt.php | 40 +++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/PHPCI/Plugin/TechnicalDebt.php b/PHPCI/Plugin/TechnicalDebt.php index 408f40fb..71ff0052 100755 --- a/PHPCI/Plugin/TechnicalDebt.php +++ b/PHPCI/Plugin/TechnicalDebt.php @@ -122,21 +122,42 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin } /** - * Runs in a specified directory, to a specified standard. + * Runs the plugin */ public function execute() { $this->phpci->logExecOutput(false); - $ignores = $this->ignore; - $ignores[] = 'phpci.yml'; + list($errorCount, $data) = $this->getErrorList(); + $this->phpci->log("Found $errorCount instances of " . implode(', ', $this->searches)); + + $this->build->storeMeta('technical_debt-warnings', $errorCount); + $this->build->storeMeta('technical_debt-data', $data); + + if ($this->allowed_errors != -1 && $errorCount > $this->allowed_errors) { + $success = false; + } + + return $success; + } + + /** + * Gets the number and list of errors returned from the search + * + * @return array + */ + public function getErrorList() + { $dirIterator = new \RecursiveDirectoryIterator($this->directory); $iterator = new \RecursiveIteratorIterator($dirIterator, \RecursiveIteratorIterator::SELF_FIRST); $files = []; + $ignores = $this->ignore; + $ignores[] = 'phpci.yml'; + foreach ($iterator as $file) { - $filePath = $file->getRealPath(); + $filePath = $file->getRealPath(); $skipFile = false; foreach ($ignores as $ignore) { if (stripos($filePath, $ignore) !== false) { @@ -180,16 +201,5 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin } } } - - $this->phpci->log("Found $errorCount instances of " . implode(', ', $this->searches)); - - $this->build->storeMeta('technical_debt-warnings', $errorCount); - $this->build->storeMeta('technical_debt-data', $data); - - if ($this->allowed_errors != -1 && $errorCount > $this->allowed_errors) { - $success = false; - } - - return $success; } } From 5bb68507d836bf94be3c7ebd8a87f5411311224f Mon Sep 17 00:00:00 2001 From: James Inman Date: Wed, 18 Feb 2015 14:56:04 +0000 Subject: [PATCH 113/329] Fixing Technical Debt error message. --- PHPCI/Plugin/TechnicalDebt.php | 1 + 1 file changed, 1 insertion(+) diff --git a/PHPCI/Plugin/TechnicalDebt.php b/PHPCI/Plugin/TechnicalDebt.php index 71ff0052..c10e7d29 100755 --- a/PHPCI/Plugin/TechnicalDebt.php +++ b/PHPCI/Plugin/TechnicalDebt.php @@ -126,6 +126,7 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin */ public function execute() { + $success = true; $this->phpci->logExecOutput(false); list($errorCount, $data) = $this->getErrorList(); From bf6ac530a67e0385032ed653338c34625fec82b8 Mon Sep 17 00:00:00 2001 From: Jon Gotlin Date: Thu, 4 Dec 2014 12:31:21 +0100 Subject: [PATCH 114/329] Create admin command cleanup --- .gitignore | 1 + PHPCI/Command/CreateAdminCommand.php | 129 ++++++------------ PHPCI/Command/InstallCommand.php | 6 +- .../PHPCI/Command/CreateAdminCommandTest.php | 78 +++++++++++ console | 5 +- 5 files changed, 129 insertions(+), 90 deletions(-) create mode 100644 Tests/PHPCI/Command/CreateAdminCommandTest.php diff --git a/.gitignore b/.gitignore index 79784a60..63e81c5b 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ PHPCI/Model/Base/MigrationBase.php PHPCI/Store/MigrationStore.php PHPCI/Store/Base/MigrationStoreBase.php local_vars.php +Tests/PHPCI/config.yml diff --git a/PHPCI/Command/CreateAdminCommand.php b/PHPCI/Command/CreateAdminCommand.php index 3b69afd3..265f03b4 100644 --- a/PHPCI/Command/CreateAdminCommand.php +++ b/PHPCI/Command/CreateAdminCommand.php @@ -9,21 +9,36 @@ namespace PHPCI\Command; -use PHPCI\Helper\Lang; use PHPCI\Service\UserService; +use PHPCI\Helper\Lang; +use PHPCI\Store\UserStore; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use b8\Store\Factory; /** -* Create admin command - creates an admin user -* @author Wogan May (@woganmay) -* @package PHPCI -* @subpackage Console -*/ + * Create admin command - creates an admin user + * @author Wogan May (@woganmay) + * @package PHPCI + * @subpackage Console + */ class CreateAdminCommand extends Command { + /** + * @var UserStore + */ + protected $userStore; + + /** + * @param UserStore $userStore + */ + public function __construct(UserStore $userStore) + { + parent::__construct(); + + $this->userStore = $userStore; + } + protected function configure() { $this @@ -32,92 +47,36 @@ class CreateAdminCommand extends Command } /** - * Creates an admin user in the existing PHPCI database - */ + * Creates an admin user in the existing PHPCI database + * + * {@inheritDoc} + */ protected function execute(InputInterface $input, OutputInterface $output) { - $userStore = Factory::getStore('User'); - $userService = new UserService($userStore); + $userService = new UserService($this->userStore); - require(PHPCI_DIR . 'bootstrap.php'); + /** @var $dialog \Symfony\Component\Console\Helper\DialogHelper */ + $dialog = $this->getHelperSet()->get('dialog'); - // Try to create a user account: - $adminEmail = $this->ask(Lang::get('enter_email'), true, FILTER_VALIDATE_EMAIL); + // Function to validate mail address. + $mailValidator = function ($answer) { + if (!filter_var($answer, FILTER_VALIDATE_EMAIL)) { + throw new \InvalidArgumentException(Lang::get('must_be_valid_email')); + } - if (empty($adminEmail)) { - return; - } + return $answer; + }; - $adminPass = $this->ask(Lang::get('enter_pass')); - $adminName = $this->ask(Lang::get('enter_name')); + $adminEmail = $dialog->askAndValidate($output, Lang::get('enter_email'), $mailValidator, false); + $adminName = $dialog->ask($output, Lang::get('enter_name')); + $adminPass = $dialog->askHiddenResponse($output, Lang::get('enter_password')); try { - $userService->createUser($adminName, $adminEmail, $adminPass, 1); - print Lang::get('user_created') . PHP_EOL; - } catch (\Exception $ex) { - print Lang::get('failed_to_create') . PHP_EOL; - print $ex->getMessage(); - print PHP_EOL; + $userService->createUser($adminName, $adminEmail, $adminPass, true); + $output->writeln(Lang::get('user_created')); + } catch (\Exception $e) { + $output->writeln(sprintf('%s', Lang::get('failed_to_create'))); + $output->writeln(sprintf('%s', $e->getMessage())); } } - - protected function ask($question, $emptyOk = false, $validationFilter = null) - { - print $question . ' '; - - $rtn = ''; - $stdin = fopen('php://stdin', 'r'); - $rtn = fgets($stdin); - fclose($stdin); - - $rtn = trim($rtn); - - if (!$emptyOk && empty($rtn)) { - $rtn = $this->ask($question, $emptyOk, $validationFilter); - } elseif (!is_null($validationFilter) && ! empty($rtn)) { - if (! $this -> controlFormat($rtn, $validationFilter, $statusMessage)) { - print $statusMessage; - $rtn = $this->ask($question, $emptyOk, $validationFilter); - } - } - - return $rtn; - } - protected function controlFormat($valueToInspect, $filter, &$statusMessage) - { - $filters = !(is_array($filter))? array($filter) : $filter; - $statusMessage = ''; - $status = true; - $options = array(); - - foreach ($filters as $filter) { - if (! is_int($filter)) { - $regexp = $filter; - $filter = FILTER_VALIDATE_REGEXP; - $options = array( - 'options' => array( - 'regexp' => $regexp, - ) - ); - } - if (! filter_var($valueToInspect, $filter, $options)) { - $status = false; - - switch ($filter) - { - case FILTER_VALIDATE_URL: - $statusMessage = Lang::get('must_be_valid_url') . PHP_EOL; - break; - case FILTER_VALIDATE_EMAIL: - $statusMessage = Lang::get('must_be_valid_email') . PHP_EOL; - break; - case FILTER_VALIDATE_REGEXP: - $statusMessage = Lang::get('incorrect_format') . PHP_EOL; - break; - } - } - } - - return $status; - } } diff --git a/PHPCI/Command/InstallCommand.php b/PHPCI/Command/InstallCommand.php index d262b9f0..04e0f2dd 100644 --- a/PHPCI/Command/InstallCommand.php +++ b/PHPCI/Command/InstallCommand.php @@ -95,7 +95,7 @@ class InstallCommand extends Command $this->writeConfigFile($conf); $this->setupDatabase($output); - $admin = $this->getAdminInforamtion($input, $output); + $admin = $this->getAdminInformation($input, $output); $this->createAdminUser($admin, $output); } @@ -160,7 +160,7 @@ class InstallCommand extends Command * @param OutputInterface $output * @return array */ - protected function getAdminInforamtion(InputInterface $input, OutputInterface $output) + protected function getAdminInformation(InputInterface $input, OutputInterface $output) { $admin = array(); @@ -172,7 +172,7 @@ class InstallCommand extends Command // Function to validate mail address. $mailValidator = function ($answer) { if (!filter_var($answer, FILTER_VALIDATE_EMAIL)) { - throw new Exception(Lang::get('must_be_valid_email')); + throw new \InvalidArgumentException(Lang::get('must_be_valid_email')); } return $answer; diff --git a/Tests/PHPCI/Command/CreateAdminCommandTest.php b/Tests/PHPCI/Command/CreateAdminCommandTest.php new file mode 100644 index 00000000..738d8f8a --- /dev/null +++ b/Tests/PHPCI/Command/CreateAdminCommandTest.php @@ -0,0 +1,78 @@ +command = $this->getMockBuilder('PHPCI\\Command\\CreateAdminCommand') + ->setConstructorArgs([$this->getMock('PHPCI\\Store\\UserStore')]) + ->setMethods(['reloadConfig']) + ->getMock() + ; + + $this->dialog = $this->getMockBuilder('Symfony\\Component\\Console\\Helper\\DialogHelper') + ->setMethods([ + 'ask', + 'askAndValidate', + 'askHiddenResponse', + ]) + ->getMock() + ; + + $this->application = new Application(); + } + + /** + * @return CommandTester + */ + protected function getCommandTester() + { + $this->application->getHelperSet()->set($this->dialog, 'dialog'); + $this->application->add($this->command); + $command = $this->application->find('phpci:create-admin'); + $commandTester = new CommandTester($command); + + return $commandTester; + } + + public function testExecute() + { + $this->dialog->expects($this->at(0))->method('askAndValidate')->will($this->returnValue('test@example.com')); + $this->dialog->expects($this->at(1))->method('ask')->will($this->returnValue('A name')); + $this->dialog->expects($this->at(2))->method('askHiddenResponse')->will($this->returnValue('foobar123')); + + $commandTester = $this->getCommandTester(); + $commandTester->execute([]); + + $this->assertEquals('User account created!' . PHP_EOL, $commandTester->getDisplay()); + } +} diff --git a/console b/console index 43fc2b8f..782cc2d0 100755 --- a/console +++ b/console @@ -20,6 +20,7 @@ use PHPCI\Command\DaemonCommand; use PHPCI\Command\PollCommand; use PHPCI\Command\CreateAdminCommand; use Symfony\Component\Console\Application; +use b8\Store\Factory; $application = new Application(); @@ -29,6 +30,6 @@ $application->add(new UpdateCommand($loggerConfig->getFor('UpdateCommand'))); $application->add(new GenerateCommand); $application->add(new DaemonCommand($loggerConfig->getFor('DaemonCommand'))); $application->add(new PollCommand($loggerConfig->getFor('PollCommand'))); -$application->add(new CreateAdminCommand); +$application->add(new CreateAdminCommand(Factory::getStore('User'))); -$application->run(); \ No newline at end of file +$application->run(); From 6c8df9a8cee8fe5a5c85cd6a9b937f11cdfd92dc Mon Sep 17 00:00:00 2001 From: James Inman Date: Thu, 19 Feb 2015 13:37:35 +0000 Subject: [PATCH 115/329] Fix missing return statement in Technical Debt. --- PHPCI/Plugin/TechnicalDebt.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PHPCI/Plugin/TechnicalDebt.php b/PHPCI/Plugin/TechnicalDebt.php index c10e7d29..fed76566 100755 --- a/PHPCI/Plugin/TechnicalDebt.php +++ b/PHPCI/Plugin/TechnicalDebt.php @@ -202,5 +202,7 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin } } } + + return array($errorCount, $data); } } From 69d05d6da8ac9497c02de4a600075db1c9616ed3 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Fri, 20 Feb 2015 12:46:43 +0000 Subject: [PATCH 116/329] Setup plugin failure should cease execution. Fixes #797 --- PHPCI/Plugin/Util/Executor.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/PHPCI/Plugin/Util/Executor.php b/PHPCI/Plugin/Util/Executor.php index bbc9507c..8782e231 100644 --- a/PHPCI/Plugin/Util/Executor.php +++ b/PHPCI/Plugin/Util/Executor.php @@ -60,6 +60,11 @@ class Executor $this->logger->logSuccess(Lang::get('plugin_success')); } else { + // If we're in the "setup" stage, execution should not continue after + // a plugin has failed: + if ($stage == 'setup') { + throw new \Exception('Plugin failed: ' . $plugin); + } // If we're in the "test" stage and the plugin is not allowed to fail, // then mark the build as failed: From 1f4fb58014f8ccae910f6bf279ba6ef3c1afd22e Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Fri, 20 Feb 2015 13:11:31 +0000 Subject: [PATCH 117/329] Update session cookie to last 12 hours. --- public/index.php | 1 + 1 file changed, 1 insertion(+) diff --git a/public/index.php b/public/index.php index 4d68e76b..56ebed12 100644 --- a/public/index.php +++ b/public/index.php @@ -7,6 +7,7 @@ * @link https://www.phptesting.org/ */ +session_set_cookie_params(43200); // Set session cookie to last 12 hours. session_start(); require_once('../bootstrap.php'); From 9133c544d59e22f57a3ec3372c2a9006072b51b4 Mon Sep 17 00:00:00 2001 From: James Inman Date: Wed, 18 Feb 2015 16:51:55 +0000 Subject: [PATCH 118/329] Adding output support to Behat plugin. --- PHPCI/Languages/lang.en.php | 1 + PHPCI/Plugin/Behat.php | 52 +++++++++++++++++ PHPCI/Plugin/TechnicalDebt.php | 2 - public/assets/js/build-plugins/behat.js | 74 +++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 public/assets/js/build-plugins/behat.js diff --git a/PHPCI/Languages/lang.en.php b/PHPCI/Languages/lang.en.php index 5f21d3dc..da767727 100644 --- a/PHPCI/Languages/lang.en.php +++ b/PHPCI/Languages/lang.en.php @@ -184,6 +184,7 @@ PHPCI', 'phpspec' => 'PHP Spec', 'phpunit' => 'PHP Unit', 'technical_debt' => 'Technical Debt', + 'behat' => 'Behat', 'file' => 'File', 'line' => 'Line', diff --git a/PHPCI/Plugin/Behat.php b/PHPCI/Plugin/Behat.php index 0e2fe69b..ad2956b4 100644 --- a/PHPCI/Plugin/Behat.php +++ b/PHPCI/Plugin/Behat.php @@ -66,12 +66,64 @@ class Behat implements \PHPCI\Plugin if (!$behat) { $this->phpci->logFailure(Lang::get('could_not_find', 'behat')); + return false; } $success = $this->phpci->executeCommand($behat . ' %s', $this->features); chdir($curdir); + list($errorCount, $data) = $this->parseBehatOutput(); + + $this->build->storeMeta('behat-warnings', $errorCount); + $this->build->storeMeta('behat-data', $data); + return $success; } + + /** + * Parse the behat output and return details on failures + * + * @return array + */ + public function parseBehatOutput() + { + $output = $this->phpci->getLastOutput(); + + $parts = explode('---', $output); + + if (count($parts) <= 1) { + return array(0, array()); + } + + $lines = explode(PHP_EOL, $parts[1]); + + $errorCount = 0; + $storeFailures = false; + $data = []; + + foreach ($lines as $line) { + $line = trim($line); + if ($line == 'Failed scenarios:') { + $storeFailures = true; + continue; + } + + if (strpos($line, ':') === false) { + $storeFailures = false; + } + + if ($storeFailures) { + $lineParts = explode(':', $line); + $data[] = array( + 'file' => $lineParts[0], + 'line' => $lineParts[1] + ); + } + } + + $errorCount = count($data); + + return array($errorCount, $data); + } } diff --git a/PHPCI/Plugin/TechnicalDebt.php b/PHPCI/Plugin/TechnicalDebt.php index fed76566..c10e7d29 100755 --- a/PHPCI/Plugin/TechnicalDebt.php +++ b/PHPCI/Plugin/TechnicalDebt.php @@ -202,7 +202,5 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin } } } - - return array($errorCount, $data); } } diff --git a/public/assets/js/build-plugins/behat.js b/public/assets/js/build-plugins/behat.js new file mode 100644 index 00000000..4140a267 --- /dev/null +++ b/public/assets/js/build-plugins/behat.js @@ -0,0 +1,74 @@ +var BehatPlugin = ActiveBuild.UiPlugin.extend({ + id: 'build-behat', + css: 'col-lg-6 col-md-12 col-sm-12 col-xs-12', + title: Lang.get('behat'), + lastData: null, + box: true, + rendered: false, + + register: function() { + var self = this; + var query = ActiveBuild.registerQuery('behat-data', -1, {key: 'behat-data'}) + + $(window).on('behat-data', function(data) { + self.onUpdate(data); + }); + + $(window).on('build-updated', function() { + if (!self.rendered) { + query(); + } + }); + }, + + render: function() { + return $('
    ' + + '' + + '' + + ' ' + + ' ' + + '' + + '
    '+Lang.get('file')+''+Lang.get('line')+'
    '); + }, + + onUpdate: function(e) { + if (!e.queryData) { + $('#build-behat').hide(); + return; + } + + this.rendered = true; + this.lastData = e.queryData; + + var errors = this.lastData[0].meta_value; + var tbody = $('#behat-data tbody'); + tbody.empty(); + + if (errors.length == 0) { + $('#build-behat').hide(); + return; + } + + for (var i in errors) { + var file = errors[i].file; + + if (ActiveBuild.fileLinkTemplate) { + var fileLink = ActiveBuild.fileLinkTemplate.replace('{FILE}', file); + fileLink = fileLink.replace('{LINE}', errors[i].line); + + file = '' + file + ''; + } + + var row = $('' + + ''+file+'' + + ''+errors[i].line+'' + + ''); + + tbody.append(row); + } + + $('#build-behat').show(); + } +}); + +ActiveBuild.registerPlugin(new BehatPlugin()); From 35a6d1f5773a81d9836b45cf5bc704ac53aaee4e Mon Sep 17 00:00:00 2001 From: Dzhilkibaev Nadir Date: Mon, 26 Jan 2015 21:12:48 +0300 Subject: [PATCH 119/329] Add SVN support to PHPCI. Closes #759 --- PHPCI/BuildFactory.php | 3 + PHPCI/Controller/ProjectController.php | 3 +- PHPCI/Languages/lang.en.php | 1 + PHPCI/Model/Build/SubversionBuild.php | 182 +++++++++++++++++++++++++ 4 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 PHPCI/Model/Build/SubversionBuild.php diff --git a/PHPCI/BuildFactory.php b/PHPCI/BuildFactory.php index 4825047d..ecc37c43 100644 --- a/PHPCI/BuildFactory.php +++ b/PHPCI/BuildFactory.php @@ -61,6 +61,9 @@ class BuildFactory case 'hg': $type = 'MercurialBuild'; break; + case 'svn': + $type = 'SubversionBuild'; + break; } $type = '\\PHPCI\\Model\\Build\\' . $type; diff --git a/PHPCI/Controller/ProjectController.php b/PHPCI/Controller/ProjectController.php index 8457a7bb..f2f63b26 100644 --- a/PHPCI/Controller/ProjectController.php +++ b/PHPCI/Controller/ProjectController.php @@ -311,10 +311,11 @@ class ProjectController extends PHPCI\Controller 'remote' => Lang::get('remote'), 'local' => Lang::get('local'), 'hg' => Lang::get('hg'), + 'svn' => Lang::get('svn'), ); $field = Form\Element\Select::create('type', Lang::get('where_hosted'), true); - $field->setPattern('^(github|bitbucket|gitlab|remote|local|hg)'); + $field->setPattern('^(github|bitbucket|gitlab|remote|local|hg|svn)'); $field->setOptions($options); $field->setClass('form-control')->setContainerClass('form-group'); $form->addField($field); diff --git a/PHPCI/Languages/lang.en.php b/PHPCI/Languages/lang.en.php index da767727..ec2306e0 100644 --- a/PHPCI/Languages/lang.en.php +++ b/PHPCI/Languages/lang.en.php @@ -101,6 +101,7 @@ PHPCI', 'remote' => 'Remote URL', 'local' => 'Local Path', 'hg' => 'Mercurial', + 'svn' => 'Subversion', 'where_hosted' => 'Where is your project hosted?', 'choose_github' => 'Choose a GitHub repository:', diff --git a/PHPCI/Model/Build/SubversionBuild.php b/PHPCI/Model/Build/SubversionBuild.php new file mode 100644 index 00000000..af884933 --- /dev/null +++ b/PHPCI/Model/Build/SubversionBuild.php @@ -0,0 +1,182 @@ + + * @package PHPCI + * @subpackage Core + */ +class SubversionBuild extends Build +{ + protected $svnCommand = 'svn export -q --non-interactive '; + + /** + * Get the URL to be used to clone this remote repository. + */ + protected function getCloneUrl() + { + $url = $this->getProject()->getReference(); + + if (substr($url, -1) != '/') { + $url .= '/'; + } + + $branch = $this->getBranch(); + + if (empty($branch) || $branch == 'trunk') { + $url .= 'trunk'; + } else { + $url .= 'branches/' . $branch; + } + + return $url; + } + + /** + * @param Builder $builder + * + * @return void + */ + protected function extendSvnCommandFromConfig(Builder $builder) + { + $cmd = $this->svnCommand; + + $svn = $builder->getConfig('svn'); + if ($svn) { + foreach ($svn as $key => $value) { + $cmd .= " --$key $value "; + } + } + + $depth = $builder->getConfig('clone_depth'); + + if (!is_null($depth)) { + $cmd .= ' --depth ' . intval($depth) . ' '; + } + + $this->svnCommand = $cmd; + } + + /** + * Create a working copy by cloning, copying, or similar. + */ + public function createWorkingCopy(Builder $builder, $buildPath) + { + $this->handleConfig($builder, $buildPath); + + $this->extendSvnCommandFromConfig($builder); + + $key = trim($this->getProject()->getSshPrivateKey()); + + if (!empty($key)) { + $success = $this->cloneBySsh($builder, $buildPath); + } else { + $success = $this->cloneByHttp($builder, $buildPath); + } + + if (!$success) { + $builder->logFailure('Failed to export remote subversion repository.'); + return false; + } + + return $this->handleConfig($builder, $buildPath); + } + + /** + * Use an HTTP-based svn export. + */ + protected function cloneByHttp(Builder $builder, $cloneTo) + { + $cmd = $this->svnCommand; + + if ($this->getCommitId() != 'Manual') { + $cmd .= ' -r %s %s "%s"'; + $success = $builder->executeCommand($cmd, $this->getCommitId(), $this->getCloneUrl(), $cloneTo); + } else { + $cmd .= ' %s "%s"'; + $success = $builder->executeCommand($cmd, $this->getCloneUrl(), $cloneTo); + } + + return $success; + } + + /** + * Use an SSH-based svn export. + */ + protected function cloneBySsh(Builder $builder, $cloneTo) + { + $cmd = $this->svnCommand . ' %s "%s"'; + + if (!IS_WIN) { + $keyFile = $this->writeSshKey($cloneTo); + $sshWrapper = $this->writeSshWrapper($cloneTo, $keyFile); + $cmd = 'export SVN_SSH="' . $sshWrapper . '" && ' . $cmd; + } + + $success = $builder->executeCommand($cmd, $this->getCloneUrl(), $cloneTo); + + if (!IS_WIN) { + // Remove the key file and svn wrapper: + unlink($keyFile); + unlink($sshWrapper); + } + + return $success; + } + + /** + * Create an SSH key file on disk for this build. + * @param $cloneTo + * @return string + */ + protected function writeSshKey($cloneTo) + { + $keyPath = dirname($cloneTo . '/temp'); + $keyFile = $keyPath . '.key'; + + // Write the contents of this project's svn key to the file: + file_put_contents($keyFile, $this->getProject()->getSshPrivateKey()); + chmod($keyFile, 0600); + + // Return the filename: + return $keyFile; + } + + /** + * Create an SSH wrapper script for Svn to use, to disable host key checking, etc. + * @param $cloneTo + * @param $keyFile + * @return string + */ + protected function writeSshWrapper($cloneTo, $keyFile) + { + $path = dirname($cloneTo . '/temp'); + $wrapperFile = $path . '.sh'; + + $sshFlags = '-o CheckHostIP=no -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o PasswordAuthentication=no'; + + // Write out the wrapper script for this build: + $script = << Date: Fri, 20 Feb 2015 14:24:37 +0000 Subject: [PATCH 120/329] Slight cleanup to please PHPMD. --- PHPCI/Plugin/Util/Executor.php | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/PHPCI/Plugin/Util/Executor.php b/PHPCI/Plugin/Util/Executor.php index 8782e231..9482c81f 100644 --- a/PHPCI/Plugin/Util/Executor.php +++ b/PHPCI/Plugin/Util/Executor.php @@ -48,27 +48,20 @@ class Executor foreach ($config[$stage] as $plugin => $options) { $this->logger->log(Lang::get('running_plugin', $plugin)); - // Is this plugin allowed to fail? - if ($stage == 'test' && !isset($options['allow_failures'])) { - $options['allow_failures'] = false; - } - // Try and execute it: if ($this->executePlugin($plugin, $options)) { // Execution was successful: $this->logger->logSuccess(Lang::get('plugin_success')); - } else { + } elseif ($stage == 'setup') { // If we're in the "setup" stage, execution should not continue after // a plugin has failed: - if ($stage == 'setup') { - throw new \Exception('Plugin failed: ' . $plugin); - } - + throw new \Exception('Plugin failed: ' . $plugin); + } else { // If we're in the "test" stage and the plugin is not allowed to fail, // then mark the build as failed: - if ($stage == 'test' && !$options['allow_failures']) { + if ($stage == 'test' && (!isset($options['allow_failures']) || !$options['allow_failures'])) { $success = false; } From dd58dd682f153483c7c2aaacbd4cc08d58bdfb46 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Fri, 20 Feb 2015 13:37:36 +0000 Subject: [PATCH 121/329] Adding support for commenting on Github diffs. --- PHPCI/Command/RebuildCommand.php | 92 +++++++++++++++++++++++++++++ PHPCI/Helper/Diff.php | 58 ++++++++++++++++++ PHPCI/Helper/Github.php | 70 ++++++++++++++++++++++ PHPCI/Model/Build.php | 13 ++++ PHPCI/Model/Build/GithubBuild.php | 54 +++++++++++++++++ PHPCI/Plugin/Behat.php | 4 +- PHPCI/Plugin/PhpCodeSniffer.php | 2 + PHPCI/Plugin/PhpCpd.php | 11 ++++ PHPCI/Plugin/PhpDocblockChecker.php | 20 ++++++- PHPCI/Plugin/PhpMessDetector.php | 1 + PHPCI/Plugin/TechnicalDebt.php | 7 ++- PHPCI/Plugin/Util/Factory.php | 4 +- console | 2 + 13 files changed, 333 insertions(+), 5 deletions(-) create mode 100644 PHPCI/Command/RebuildCommand.php create mode 100644 PHPCI/Helper/Diff.php diff --git a/PHPCI/Command/RebuildCommand.php b/PHPCI/Command/RebuildCommand.php new file mode 100644 index 00000000..165d94c7 --- /dev/null +++ b/PHPCI/Command/RebuildCommand.php @@ -0,0 +1,92 @@ + +* @package PHPCI +* @subpackage Console +*/ +class RebuildCommand extends Command +{ + /** + * @var Logger + */ + protected $logger; + + /** + * @var OutputInterface + */ + protected $output; + + /** + * @var boolean + */ + protected $run; + + /** + * @var int + */ + protected $sleep; + + /** + * @param \Monolog\Logger $logger + * @param string $name + */ + public function __construct(Logger $logger, $name = null) + { + parent::__construct($name); + $this->logger = $logger; + } + + protected function configure() + { + $this + ->setName('phpci:rebuild') + ->setDescription('Re-runs the last run build.'); + } + + /** + * Loops through running. + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $runner = new RunCommand($this->logger); + $runner->setMaxBuilds(1); + $runner->setDaemon(false); + + /** @var \PHPCI\Store\BuildStore $store */ + $store = Factory::getStore('Build'); + $service = new BuildService($store); + + $lastBuild = array_shift($store->getLatestBuilds(null, 1)); + $service->createDuplicateBuild($lastBuild); + + $runner->run(new ArgvInput(array()), $output); + } + + /** + * Called when log entries are made in Builder / the plugins. + * @see \PHPCI\Builder::log() + */ + public function logCallback($log) + { + $this->output->writeln($log); + } +} diff --git a/PHPCI/Helper/Diff.php b/PHPCI/Helper/Diff.php new file mode 100644 index 00000000..e50c7eaf --- /dev/null +++ b/PHPCI/Helper/Diff.php @@ -0,0 +1,58 @@ +get('phpci.github.token'); + + if (!$token) { + return null; + } + + $url = '/repos/' . strtolower($repo) . '/pulls/' . $pullId . '/comments'; + + $params = array( + 'body' => $comment, + 'commit_id' => $commitId, + 'path' => $file, + 'position' => $line, + ); + + $http = new HttpClient('https://api.github.com'); + $http->setHeaders(array( + 'Content-Type: application/x-www-form-urlencoded', + 'Authorization: Basic ' . base64_encode($token . ':x-oauth-basic'), + )); + + $http->post($url, json_encode($params)); + } + + /** + * Create a comment on a Github commit. + * @param $repo + * @param $commitId + * @param $file + * @param $line + * @param $comment + * @return null + */ + public function createCommitComment($repo, $commitId, $file, $line, $comment) + { + $token = Config::getInstance()->get('phpci.github.token'); + + if (!$token) { + return null; + } + + $url = '/repos/' . strtolower($repo) . '/commits/' . $commitId . '/comments'; + + $params = array( + 'body' => $comment, + 'path' => $file, + 'position' => $line, + ); + + $http = new HttpClient('https://api.github.com'); + $http->setHeaders(array( + 'Content-Type: application/x-www-form-urlencoded', + 'Authorization: Basic ' . base64_encode($token . ':x-oauth-basic'), + )); + + $http->post($url, json_encode($params)); + } } diff --git a/PHPCI/Model/Build.php b/PHPCI/Model/Build.php index 5561d51e..3d07778a 100644 --- a/PHPCI/Model/Build.php +++ b/PHPCI/Model/Build.php @@ -204,4 +204,17 @@ class Build extends BuildBase return $rtn; } + + /** + * Allows specific build types (e.g. Github) to report violations back to their respective services. + * @param Builder $builder + * @param $file + * @param $line + * @param $message + * @return mixed + */ + public function reportError(Builder $builder, $file, $line, $message) + { + return array($builder, $file, $line, $message); + } } diff --git a/PHPCI/Model/Build/GithubBuild.php b/PHPCI/Model/Build/GithubBuild.php index caa21979..7c8f3f6f 100644 --- a/PHPCI/Model/Build/GithubBuild.php +++ b/PHPCI/Model/Build/GithubBuild.php @@ -10,6 +10,8 @@ namespace PHPCI\Model\Build; use PHPCI\Builder; +use PHPCI\Helper\Diff; +use PHPCI\Helper\Github; use PHPCI\Model\Build\RemoteGitBuild; /** @@ -167,4 +169,56 @@ class GithubBuild extends RemoteGitBuild return $success; } + + /** + * @inheritDoc + */ + public function reportError(Builder $builder, $file, $line, $message) + { + $diffLineNumber = $this->getDiffLineNumber($builder, $file, $line); + + if (!is_null($diffLineNumber)) { + $helper = new Github(); + + $repo = $this->getProject()->getReference(); + $prNumber = $this->getExtra('pull_request_number'); + $commit = $this->getCommitId(); + + if (!empty($prNumber)) { + $helper->createPullRequestComment($repo, $prNumber, $commit, $file, $diffLineNumber, $message); + } else { + $helper->createCommitComment($repo, $commit, $file, $diffLineNumber, $message); + } + } + } + + /** + * Uses git diff to figure out what the diff line position is, based on the error line number. + * @param Builder $builder + * @param $file + * @param $line + * @return int|null + */ + protected function getDiffLineNumber(Builder $builder, $file, $line) + { + $builder->logExecOutput(false); + + $prNumber = $this->getExtra('pull_request_number'); + $path = $builder->buildPath; + + if (!empty($prNumber)) { + $builder->executeCommand('cd %s && git diff origin/%s "%s"', $path, $this->getBranch(), $file); + } else { + $builder->executeCommand('cd %s && git diff %s^! "%s"', $path, $this->getCommitId(), $file); + } + + $builder->logExecOutput(true); + + $diff = $builder->getLastOutput(); + + $helper = new Diff(); + $lines = $helper->getLinePositions($diff); + + return $lines[$line]; + } } diff --git a/PHPCI/Plugin/Behat.php b/PHPCI/Plugin/Behat.php index ad2956b4..23686270 100644 --- a/PHPCI/Plugin/Behat.php +++ b/PHPCI/Plugin/Behat.php @@ -100,7 +100,7 @@ class Behat implements \PHPCI\Plugin $errorCount = 0; $storeFailures = false; - $data = []; + $data = array(); foreach ($lines as $line) { $line = trim($line); @@ -119,6 +119,8 @@ class Behat implements \PHPCI\Plugin 'file' => $lineParts[0], 'line' => $lineParts[1] ); + + $this->build->reportError($this->phpci, $lineParts[0], $lineParts[1], 'Behat scenario failed.'); } } diff --git a/PHPCI/Plugin/PhpCodeSniffer.php b/PHPCI/Plugin/PhpCodeSniffer.php index 3ee232b7..df276eac 100644 --- a/PHPCI/Plugin/PhpCodeSniffer.php +++ b/PHPCI/Plugin/PhpCodeSniffer.php @@ -237,6 +237,8 @@ class PhpCodeSniffer implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin $fileName = str_replace($this->phpci->buildPath, '', $fileName); foreach ($file['messages'] as $message) { + $this->build->reportError($this->phpci, $fileName, $message['line'], 'PHPCS: ' . $message['message']); + $rtn[] = array( 'file' => $fileName, 'line' => $message['line'], diff --git a/PHPCI/Plugin/PhpCpd.php b/PHPCI/Plugin/PhpCpd.php index 9bbcfc09..c663db5a 100644 --- a/PHPCI/Plugin/PhpCpd.php +++ b/PHPCI/Plugin/PhpCpd.php @@ -140,6 +140,17 @@ class PhpCpd implements \PHPCI\Plugin 'line_end' => (int) $file['line'] + (int) $duplication['lines'], 'code' => (string) $duplication->codefragment ); + + $message = <<codefragment} +``` +CPD; + + $this->build->reportError($this->phpci, $fileName, $file['line'], $message); + } $warnings++; diff --git a/PHPCI/Plugin/PhpDocblockChecker.php b/PHPCI/Plugin/PhpDocblockChecker.php index 9650127f..9f71afef 100644 --- a/PHPCI/Plugin/PhpDocblockChecker.php +++ b/PHPCI/Plugin/PhpDocblockChecker.php @@ -143,12 +143,13 @@ class PhpDocblockChecker implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin // Re-enable exec output logging: $this->phpci->logExecOutput(true); - $output = json_decode($this->phpci->getLastOutput()); + $output = json_decode($this->phpci->getLastOutput(), true); $errors = count($output); $success = true; $this->build->storeMeta('phpdoccheck-warnings', $errors); $this->build->storeMeta('phpdoccheck-data', $output); + $this->reportErrors($output); if ($this->allowed_warnings != -1 && $errors > $this->allowed_warnings) { $success = false; @@ -156,4 +157,21 @@ class PhpDocblockChecker implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin return $success; } + + /** + * Report all of the errors we've encountered line-by-line. + * @param $output + */ + protected function reportErrors($output) + { + foreach ($output as $error) { + $message = 'Class ' . $error['class'] . ' does not have a Docblock comment.'; + + if ($error['type'] == 'method') { + $message = 'Method ' . $error['class'] . '::' . $error['method'] . ' does not have a Docblock comment.'; + } + + $this->build->reportError($this->phpci, $error['file'], $error['line'], $message); + } + } } diff --git a/PHPCI/Plugin/PhpMessDetector.php b/PHPCI/Plugin/PhpMessDetector.php index 4603d534..21c171cb 100644 --- a/PHPCI/Plugin/PhpMessDetector.php +++ b/PHPCI/Plugin/PhpMessDetector.php @@ -181,6 +181,7 @@ class PhpMessDetector implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin 'message' => (string)$violation, ); + $this->build->reportError($this->phpci, $fileName, (int)$violation['beginline'], (string)$violation); $data[] = $warning; } } diff --git a/PHPCI/Plugin/TechnicalDebt.php b/PHPCI/Plugin/TechnicalDebt.php index c10e7d29..afe6c920 100755 --- a/PHPCI/Plugin/TechnicalDebt.php +++ b/PHPCI/Plugin/TechnicalDebt.php @@ -194,11 +194,16 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin $errorCount++; $this->phpci->log("Found $search on line $lineNumber of $file:\n$content"); + + $fileName = str_replace($this->directory, '', $file); $data[] = array( - 'file' => str_replace($this->directory, '', $file), + 'file' => $fileName, 'line' => $lineNumber, 'message' => $content ); + + $this->build->reportError($this->phpci, $fileName, $lineNumber, $content); + } } } diff --git a/PHPCI/Plugin/Util/Factory.php b/PHPCI/Plugin/Util/Factory.php index 5430b56f..8127bcd2 100644 --- a/PHPCI/Plugin/Util/Factory.php +++ b/PHPCI/Plugin/Util/Factory.php @@ -76,11 +76,11 @@ class Factory * be passed along with any resources registered with the factory. * * @param $className - * @param array $options + * @param array|null $options * @throws \InvalidArgumentException if $className doesn't represent a valid plugin * @return \PHPCI\Plugin */ - public function buildPlugin($className, array $options = array()) + public function buildPlugin($className, $options = array()) { $this->currentPluginOptions = $options; diff --git a/console b/console index 782cc2d0..a812fc7f 100755 --- a/console +++ b/console @@ -13,6 +13,7 @@ define('PHPCI_IS_CONSOLE', true); require('bootstrap.php'); use PHPCI\Command\RunCommand; +use PHPCI\Command\RebuildCommand; use PHPCI\Command\GenerateCommand; use PHPCI\Command\UpdateCommand; use PHPCI\Command\InstallCommand; @@ -25,6 +26,7 @@ use b8\Store\Factory; $application = new Application(); $application->add(new RunCommand($loggerConfig->getFor('RunCommand'))); +$application->add(new RebuildCommand($loggerConfig->getFor('RunCommand'))); $application->add(new InstallCommand); $application->add(new UpdateCommand($loggerConfig->getFor('UpdateCommand'))); $application->add(new GenerateCommand); From 1a5ecb97b1ca36b7ae9fa4eec0d277bda045223d Mon Sep 17 00:00:00 2001 From: Leszek Date: Mon, 23 Feb 2015 09:10:17 +0000 Subject: [PATCH 122/329] Update lang.pl.php Closes #810 --- PHPCI/Languages/lang.pl.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/PHPCI/Languages/lang.pl.php b/PHPCI/Languages/lang.pl.php index 4aafc9cf..5bf5e043 100644 --- a/PHPCI/Languages/lang.pl.php +++ b/PHPCI/Languages/lang.pl.php @@ -101,6 +101,7 @@ od wybranego kodu źródłowego platformy hostingowej.', 'remote' => 'Zdalny URL ', 'local' => 'Lokalna Ścieżka ', 'hg' => 'Mercurial', + 'svn' => 'Subversion', 'where_hosted' => 'Gdzie hostowany jest Twój projekt?', 'choose_github' => 'Wybierz repozytorium GitHub:', @@ -181,8 +182,10 @@ Services repozytoria Bitbucket.', 'phpcs' => 'PHP Code Sniffer', 'phpdoccheck' => 'Brakuje sekcji DocBlock', 'phpmd' => 'PHP Mess Detector', - 'phpspec' => 'Speck PHP', - 'phpunit' => 'Jednostka PHP', + 'phpspec' => 'PHPSpec', + 'phpunit' => 'PHPUnit', + 'technical_debt' => 'Dług technologiczny', + 'behat' => 'Behat', 'file' => 'Plik', 'line' => 'Linia', From 4ffeec776716eff2a90bc40326ba01010b7a4bc6 Mon Sep 17 00:00:00 2001 From: corpsee Date: Sun, 22 Feb 2015 01:32:24 +0600 Subject: [PATCH 123/329] Updated lang.ru file Closes #807 --- PHPCI/Languages/lang.ru.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PHPCI/Languages/lang.ru.php b/PHPCI/Languages/lang.ru.php index 00b2783e..836db6f4 100644 --- a/PHPCI/Languages/lang.ru.php +++ b/PHPCI/Languages/lang.ru.php @@ -99,6 +99,7 @@ PHPCI', 'remote' => 'Внешний URL', 'local' => 'Локальный путь', 'hg' => 'Mercurial', + 'svn' => 'Subversion', 'where_hosted' => 'Расположение проекта', 'choose_github' => 'Выберите GitHub репозиторий:', @@ -178,6 +179,8 @@ PHPCI', 'phpmd' => 'PHP Mess Detector', 'phpspec' => 'PHP Spec', 'phpunit' => 'PHP Unit', + 'technical_debt' => 'Технические долги', + 'behat' => 'Behat', 'file' => 'Файл', 'line' => 'Строка', From 6420119f1af0b0e077d957b9c35f3d9166340523 Mon Sep 17 00:00:00 2001 From: Lee Willis Date: Thu, 19 Feb 2015 21:33:49 +0000 Subject: [PATCH 124/329] Make SSH key generation more robust. Do not try and predict whether we will be able to create a key. Instead try and create one and capture failure if it happens. Closes #803 --- PHPCI/Helper/SshKey.php | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/PHPCI/Helper/SshKey.php b/PHPCI/Helper/SshKey.php index 47bd85bd..932686f2 100644 --- a/PHPCI/Helper/SshKey.php +++ b/PHPCI/Helper/SshKey.php @@ -39,9 +39,9 @@ class SshKey $return = array('private_key' => '', 'public_key' => ''); - if ($this->canGenerateKeys()) { - shell_exec('ssh-keygen -q -t rsa -b 2048 -f '.$keyFile.' -N "" -C "deploy@phpci"'); + $output = @shell_exec('ssh-keygen -t rsa -b 2048 -f '.$keyFile.' -N "" -C "deploy@phpci"'); + if (!empty($output)) { $pub = file_get_contents($keyFile . '.pub'); $prv = file_get_contents($keyFile); @@ -56,16 +56,4 @@ class SshKey return $return; } - - /** - * Checks whether or not we can generate keys, by quietly test running ssh-keygen. - * @return bool - */ - public function canGenerateKeys() - { - $keygen = @shell_exec('ssh-keygen --help'); - $canGenerateKeys = !empty($keygen); - - return $canGenerateKeys; - } } From 601318b97b036f96d475f624ebcd3c204443773d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=CC=80lex=20Corretge=CC=81?= Date: Tue, 3 Feb 2015 13:19:33 +0100 Subject: [PATCH 125/329] Fix the problem when executing Phing out of the build dir. Closes #778 Closes #748 --- PHPCI/Plugin/Phing.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/PHPCI/Plugin/Phing.php b/PHPCI/Plugin/Phing.php index ae86968b..2bf64cca 100644 --- a/PHPCI/Plugin/Phing.php +++ b/PHPCI/Plugin/Phing.php @@ -213,8 +213,12 @@ class Phing implements \PHPCI\Plugin */ public function propertiesToString() { - if (empty($this->properties)) { - return ''; + /** + * fix the problem when execute phing out of the build dir + * @ticket 748 + */ + if (!isset($this->properties['project.basedir'])) { + $this->properties['project.basedir'] = $this->getDirectory(); } $propertiesString = array(); From 610a0e57efb3f2b4d230284e7db69413ebe3889b Mon Sep 17 00:00:00 2001 From: Daniel Seif Date: Wed, 28 Jan 2015 00:39:04 +0100 Subject: [PATCH 126/329] Fixed settings handling for symlink creation in local build Closes #766 --- PHPCI/Model/Build/LocalBuild.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/PHPCI/Model/Build/LocalBuild.php b/PHPCI/Model/Build/LocalBuild.php index b94ede4e..5131abe1 100644 --- a/PHPCI/Model/Build/LocalBuild.php +++ b/PHPCI/Model/Build/LocalBuild.php @@ -35,12 +35,14 @@ class LocalBuild extends Build return $this->handleConfig($builder, $buildPath) !== false; } - $buildSettings = $this->handleConfig($builder, $reference); + $configHandled = $this->handleConfig($builder, $reference); - if ($buildSettings === false) { + if ($configHandled === false) { return false; } + $buildSettings = $builder->getConfig('build_settings'); + if (isset($buildSettings['prefer_symlink']) && $buildSettings['prefer_symlink'] === true) { return $this->handleSymlink($builder, $reference, $buildPath); } else { From ea3109be6702be5cd645172d47a2192783b59978 Mon Sep 17 00:00:00 2001 From: Matthew Leffler Date: Mon, 12 Jan 2015 12:25:55 -0700 Subject: [PATCH 127/329] Convert tapString to UTF-8, allowing UTF8 output from PHPUnit, etc. Closes #738 --- PHPCI/Plugin/PhpUnit.php | 1 + 1 file changed, 1 insertion(+) diff --git a/PHPCI/Plugin/PhpUnit.php b/PHPCI/Plugin/PhpUnit.php index 3ea1ae8d..70e0e74f 100644 --- a/PHPCI/Plugin/PhpUnit.php +++ b/PHPCI/Plugin/PhpUnit.php @@ -162,6 +162,7 @@ class PhpUnit implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin } $tapString = $this->phpci->getLastOutput(); + $tapString = mb_convert_encoding($tapString, "UTF-8", "ISO-8859-1"); try { $tapParser = new TapParser($tapString); From 49056792980bab50cced463e2fcb5608457fe199 Mon Sep 17 00:00:00 2001 From: Adirelle Date: Thu, 8 Jan 2015 16:51:15 +0100 Subject: [PATCH 128/329] Email plugin: use "default_mailto_address" as a fallback only. Closes #730 --- PHPCI/Plugin/Email.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PHPCI/Plugin/Email.php b/PHPCI/Plugin/Email.php index 9e361d91..8414451f 100644 --- a/PHPCI/Plugin/Email.php +++ b/PHPCI/Plugin/Email.php @@ -159,10 +159,10 @@ class Email implements \PHPCI\Plugin } } - if (isset($this->options['default_mailto_address'])) { + if (empty($addresses) && isset($this->options['default_mailto_address'])) { $addresses[] = $this->options['default_mailto_address']; - return $addresses; } + return array_unique($addresses); } From 893deada33a9fcabe6f9aa255cb25f418d7ba72a Mon Sep 17 00:00:00 2001 From: nonlux Date: Mon, 23 Feb 2015 14:40:20 +0000 Subject: [PATCH 129/329] Set the CommandExecutor buildPath property when a build is created. Closes #556 --- PHPCI/Builder.php | 2 ++ PHPCI/Helper/BaseCommandExecutor.php | 13 ++++++++++--- PHPCI/Helper/CommandExecutor.php | 6 ++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/PHPCI/Builder.php b/PHPCI/Builder.php index 904aa506..d911e04b 100644 --- a/PHPCI/Builder.php +++ b/PHPCI/Builder.php @@ -296,6 +296,8 @@ class Builder implements LoggerAwareInterface PHPCI_URL ); + $this->commandExecutor->setBuildPath($this->buildPath); + // Create a working copy of the project: if (!$this->build->createWorkingCopy($this, $this->buildPath)) { throw new \Exception(Lang::get('could_not_create_working')); diff --git a/PHPCI/Helper/BaseCommandExecutor.php b/PHPCI/Helper/BaseCommandExecutor.php index 28247ebc..4c2f1fa8 100644 --- a/PHPCI/Helper/BaseCommandExecutor.php +++ b/PHPCI/Helper/BaseCommandExecutor.php @@ -62,9 +62,7 @@ abstract class BaseCommandExecutor implements CommandExecutor $this->logger = $logger; $this->quiet = $quiet; $this->verbose = $verbose; - $this->lastOutput = array(); - $this->rootDir = $rootDir; } @@ -92,7 +90,7 @@ abstract class BaseCommandExecutor implements CommandExecutor $pipes = array(); - $process = proc_open($command, $descriptorSpec, $pipes, dirname($this->buildPath), null); + $process = proc_open($command, $descriptorSpec, $pipes, $this->buildPath, null); if (is_resource($process)) { fclose($pipes[0]); @@ -219,4 +217,13 @@ abstract class BaseCommandExecutor implements CommandExecutor } return null; } + + /** + * Set the buildPath property. + * @param string $path + */ + public function setBuildPath($path) + { + $this->buildPath = $path; + } } diff --git a/PHPCI/Helper/CommandExecutor.php b/PHPCI/Helper/CommandExecutor.php index d83f0c6b..bacd1a2e 100644 --- a/PHPCI/Helper/CommandExecutor.php +++ b/PHPCI/Helper/CommandExecutor.php @@ -30,4 +30,10 @@ interface CommandExecutor * @return null|string */ public function findBinary($binary, $buildPath = null); + + /** + * Set the buildPath property. + * @param string $path + */ + public function setBuildPath($path); } From e98647bd97d49741242d252514b8703504a62869 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Mon, 23 Feb 2015 15:58:14 +0000 Subject: [PATCH 130/329] Add support for Mercurial SSH-based clones. --- PHPCI/Model/Build/MercurialBuild.php | 75 +++++++++++++++++++++++----- 1 file changed, 63 insertions(+), 12 deletions(-) diff --git a/PHPCI/Model/Build/MercurialBuild.php b/PHPCI/Model/Build/MercurialBuild.php index 500b5372..0f38940b 100644 --- a/PHPCI/Model/Build/MercurialBuild.php +++ b/PHPCI/Model/Build/MercurialBuild.php @@ -13,36 +13,87 @@ use PHPCI\Model\Build; use PHPCI\Builder; /** -* Mercurial Build Model -* @author Pavel Gopanenko -* @package PHPCI -* @subpackage Core -*/ + * Mercurial Build Model + * @author Pavel Gopanenko + * @package PHPCI + * @subpackage Core + */ class MercurialBuild extends Build { /** - * Get the URL to be used to clone this remote repository. - */ + * Get the URL to be used to clone this remote repository. + */ protected function getCloneUrl() { return $this->getProject()->getReference(); } /** - * Create a working copy by cloning, copying, or similar. - */ + * Create a working copy by cloning, copying, or similar. + */ public function createWorkingCopy(Builder $builder, $buildPath) { - $this->cloneByHttp($builder, $buildPath); + $key = trim($this->getProject()->getSshPublicKey()); + + if (!empty($key) && strpos($this->getProject()->getReference(), 'ssh') > -1) { + $success = $this->cloneBySsh($builder, $buildPath); + } else { + $success = $this->cloneByHttp($builder, $buildPath); + } + + if (!$success) { + $builder->logFailure('Failed to clone remote git repository.'); + return false; + } return $this->handleConfig($builder, $buildPath); } /** - * Use an mercurial clone. - */ + * Use a HTTP-based Mercurial clone. + */ protected function cloneByHttp(Builder $builder, $cloneTo) { return $builder->executeCommand('hg clone %s "%s" -r %s', $this->getCloneUrl(), $cloneTo, $this->getBranch()); } + + /** + * Use an SSH-based Mercurial clone. + */ + protected function cloneBySsh(Builder $builder, $cloneTo) + { + $keyFile = $this->writeSshKey(); + + // Do the git clone: + $cmd = 'hg clone --ssh "ssh -i '.$keyFile.'" %s "%s"'; + $success = $builder->executeCommand($cmd, $this->getCloneUrl(), $cloneTo); + + if ($success) { + $success = $this->postCloneSetup($builder, $cloneTo); + } + + // Remove the key file: + unlink($keyFile); + return $success; + } + + /** + * Handle post-clone tasks (switching branch, etc.) + * @param Builder $builder + * @param $cloneTo + * @return bool + */ + protected function postCloneSetup(Builder $builder, $cloneTo) + { + $success = true; + $commit = $this->getCommitId(); + + // Allow switching to a specific branch: + if (!empty($commit) && $commit != 'Manual') { + $cmd = 'cd "%s" && hg checkout %s'; + $success = $builder->executeCommand($cmd, $cloneTo, $this->getBranch()); + } + + return $success; + } } From 1466ad06ef708cbab2b53112fc59e8c1d70c2e33 Mon Sep 17 00:00:00 2001 From: Alex Davyskiba Date: Sat, 31 Jan 2015 11:50:57 +0200 Subject: [PATCH 131/329] Allow projects to be archived. Closes #771 --- PHPCI/Application.php | 8 +++- PHPCI/Controller/HomeController.php | 8 +++- PHPCI/Controller/ProjectController.php | 7 ++++ PHPCI/Languages/lang.da.php | 1 + PHPCI/Languages/lang.de.php | 1 + PHPCI/Languages/lang.el.php | 1 + PHPCI/Languages/lang.en.php | 1 + PHPCI/Languages/lang.fr.php | 1 + PHPCI/Languages/lang.it.php | 1 + PHPCI/Languages/lang.nl.php | 1 + PHPCI/Languages/lang.pl.php | 1 + PHPCI/Languages/lang.ru.php | 1 + PHPCI/Languages/lang.uk.php | 1 + .../20150131075425_archive_project.php | 26 +++++++++++++ PHPCI/Model/Base/ProjectBase.php | 39 +++++++++++++++++++ PHPCI/Service/ProjectService.php | 4 ++ PHPCI/View/layout.phtml | 8 +++- 17 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 PHPCI/Migrations/20150131075425_archive_project.php diff --git a/PHPCI/Application.php b/PHPCI/Application.php index ee1f6e15..8582fc41 100644 --- a/PHPCI/Application.php +++ b/PHPCI/Application.php @@ -137,7 +137,13 @@ class Application extends b8\Application { /** @var \PHPCI\Store\ProjectStore $projectStore */ $projectStore = b8\Store\Factory::getStore('Project'); - $layout->projects = $projectStore->getAll(); + $layout->projects = $projectStore->getWhere( + array('archived' => (int)isset($_GET['archived'])), + 50, + 0, + array(), + array('title' => 'ASC') + ); } /** diff --git a/PHPCI/Controller/HomeController.php b/PHPCI/Controller/HomeController.php index 65abf79a..2455e709 100644 --- a/PHPCI/Controller/HomeController.php +++ b/PHPCI/Controller/HomeController.php @@ -48,7 +48,13 @@ class HomeController extends \PHPCI\Controller { $this->layout->title = Lang::get('dashboard'); - $projects = $this->projectStore->getWhere(array(), 50, 0, array(), array('title' => 'ASC')); + $projects = $this->projectStore->getWhere( + array('archived' => (int)isset($_GET['archived'])), + 50, + 0, + array(), + array('title' => 'ASC') + ); $builds = $this->buildStore->getLatestBuilds(null, 10); diff --git a/PHPCI/Controller/ProjectController.php b/PHPCI/Controller/ProjectController.php index f2f63b26..285982a1 100644 --- a/PHPCI/Controller/ProjectController.php +++ b/PHPCI/Controller/ProjectController.php @@ -282,6 +282,7 @@ class ProjectController extends PHPCI\Controller 'ssh_public_key' => $this->getParam('pubkey', null), 'build_config' => $this->getParam('build_config', null), 'allow_public_status' => $this->getParam('allow_public_status', 0), + 'archived' => $this->getParam('archived', 0), 'branch' => $this->getParam('branch', null), ); @@ -357,6 +358,12 @@ class ProjectController extends PHPCI\Controller $field->setValue(0); $form->addField($field); + $field = Form\Element\Checkbox::create('archived', Lang::get('archived'), false); + $field->setContainerClass('form-group'); + $field->setCheckedValue(1); + $field->setValue(0); + $form->addField($field); + $field = new Form\Element\Submit(); $field->setValue(Lang::get('save_project')); $field->setContainerClass('form-group'); diff --git a/PHPCI/Languages/lang.da.php b/PHPCI/Languages/lang.da.php index 5010f28e..5ba3d96f 100644 --- a/PHPCI/Languages/lang.da.php +++ b/PHPCI/Languages/lang.da.php @@ -112,6 +112,7 @@ i din foretrukne hosting-platform.', (hvis du ikke har mulighed for at tilføje en phpci.yml fil i projektets repository)', 'default_branch' => 'Default branch navn', 'allow_public_status' => 'Tillad offentlig status-side og -billede for dette projekt?', + 'archived' => 'Archived', 'save_project' => 'Gem Projekt', 'error_mercurial' => 'Mercurial repository-URL skal starte med http:// eller https://', diff --git a/PHPCI/Languages/lang.de.php b/PHPCI/Languages/lang.de.php index c40ef23d..bdcc1459 100644 --- a/PHPCI/Languages/lang.de.php +++ b/PHPCI/Languages/lang.de.php @@ -113,6 +113,7 @@ generiert. Um es zu verwenden, fügen Sie einfach den folgenden Public Key im Ab (falls Sie Ihrem Projektrepository kein phpci.yml hinzufügen können)', 'default_branch' => 'Name des Standardbranches', 'allow_public_status' => 'Öffentliche Statusseite und -bild für dieses Projekt einschalten?', + 'archived' => 'Archived', 'save_project' => 'Projekt speichern', 'error_mercurial' => 'Mercurial Repository-URL muss mit http://, oder https:// beginnen', diff --git a/PHPCI/Languages/lang.el.php b/PHPCI/Languages/lang.el.php index c611a87d..82b3cf22 100644 --- a/PHPCI/Languages/lang.el.php +++ b/PHPCI/Languages/lang.el.php @@ -113,6 +113,7 @@ PHPCI', (αν δεν μπορείτε να προσθέσετε ένα αρχείο phpci.yml στο αποθετήριο έργων)', 'default_branch' => 'Προκαθορισμένο όνομα διακλάδωσης', 'allow_public_status' => 'Ενεργοποίηση της σελίδας δημόσιας κατάστασης και την εικόνα για το έργο αυτό;', + 'archived' => 'Archived', 'save_project' => 'Αποθήκευση έργου', 'error_mercurial' => 'Ο σύνδεσμος URL του ευμετάβλητου αποθετηρίου πρέπει να ξεκινάει με http:// ή https://', diff --git a/PHPCI/Languages/lang.en.php b/PHPCI/Languages/lang.en.php index ec2306e0..450be1ef 100644 --- a/PHPCI/Languages/lang.en.php +++ b/PHPCI/Languages/lang.en.php @@ -114,6 +114,7 @@ PHPCI', (if you cannot add a phpci.yml file in the project repository)', 'default_branch' => 'Default branch name', 'allow_public_status' => 'Enable public status page and image for this project?', + 'archived' => 'Archived', 'save_project' => 'Save Project', 'error_mercurial' => 'Mercurial repository URL must be start with http:// or https://', diff --git a/PHPCI/Languages/lang.fr.php b/PHPCI/Languages/lang.fr.php index 45cc3958..20410684 100644 --- a/PHPCI/Languages/lang.fr.php +++ b/PHPCI/Languages/lang.fr.php @@ -113,6 +113,7 @@ PHPCI', (si vous ne pouvez pas ajouter de fichier phpci.yml à la racine du dépôt)', 'default_branch' => 'Nom de la branche par défaut', 'allow_public_status' => 'Activer la page de statut publique et l\'image pour ce projet ?', + 'archived' => 'Archived', 'save_project' => 'Enregistrer le projet', 'error_mercurial' => 'Les URLs de dépôt Mercurial doivent commencer par http:// ou https://', diff --git a/PHPCI/Languages/lang.it.php b/PHPCI/Languages/lang.it.php index c5c4e01b..46ca14fd 100644 --- a/PHPCI/Languages/lang.it.php +++ b/PHPCI/Languages/lang.it.php @@ -113,6 +113,7 @@ PHPCI', 'default_branch' => 'Nome del branch di default', 'allow_public_status' => 'Vuoi rendere pubblica la pagina dello stato e l\'immagine per questo progetto?', 'save_project' => 'Salva il Progetto', + 'archived' => 'Archived', 'error_mercurial' => 'L\'URL del repository Mercurial URL deve iniziare con http:// o https://', 'error_remote' => 'L\'URL del repository deve iniziare con git://, http:// o https://', diff --git a/PHPCI/Languages/lang.nl.php b/PHPCI/Languages/lang.nl.php index f38272c4..83d5e317 100644 --- a/PHPCI/Languages/lang.nl.php +++ b/PHPCI/Languages/lang.nl.php @@ -113,6 +113,7 @@ van je gekozen source code hosting platform', (indien je geen phpci.yml bestand aan de project repository kan toevoegen)', 'default_branch' => 'Standaard branch naam', 'allow_public_status' => 'Publieke statuspagina en afbeelding beschikbaar maken voor dit project?', + 'archived' => 'Archived', 'save_project' => 'Project opslaan', 'error_mercurial' => 'Mercurial repository URL dient te starten met http:// of https://', diff --git a/PHPCI/Languages/lang.pl.php b/PHPCI/Languages/lang.pl.php index 5bf5e043..9d6de6c8 100644 --- a/PHPCI/Languages/lang.pl.php +++ b/PHPCI/Languages/lang.pl.php @@ -114,6 +114,7 @@ od wybranego kodu źródłowego platformy hostingowej.', (jeśli nie możesz dodać pliku phpci.yml do repozytorium projektu)', 'default_branch' => 'Domyślna nazwa gałęzi', 'allow_public_status' => 'Włączyć status publiczny dla tego projektu?', + 'archived' => 'Archived', 'save_project' => 'Zachowaj Projekt', 'error_mercurial' => 'URL repozytorium Mercurialnego powinno zaczynać się od http:// and https://', diff --git a/PHPCI/Languages/lang.ru.php b/PHPCI/Languages/lang.ru.php index 836db6f4..2f9ab0ee 100644 --- a/PHPCI/Languages/lang.ru.php +++ b/PHPCI/Languages/lang.ru.php @@ -112,6 +112,7 @@ PHPCI', (если вы не добавили файл phpci.yml в репозиторий вашего проекта)', 'default_branch' => 'Ветка по умолчанию', 'allow_public_status' => 'Разрешить публичный статус и изображение (статуса) для проекта', + 'archived' => 'Archived', 'save_project' => 'Сохранить проект', 'error_mercurial' => 'URL репозитория Mercurial должен начинаться с http:// или https://', diff --git a/PHPCI/Languages/lang.uk.php b/PHPCI/Languages/lang.uk.php index f51bd885..7e4a2191 100644 --- a/PHPCI/Languages/lang.uk.php +++ b/PHPCI/Languages/lang.uk.php @@ -112,6 +112,7 @@ PHPCI', (якщо ви не додали файл phpci.yml до репозиторію вашого проекту)', 'default_branch' => 'Назва гілки за замовчуванням', 'allow_public_status' => 'Увімкнути публічну сторінку статусу та зображення для цього проекта?', + 'archived' => 'Archived', 'save_project' => 'Зберегти проект', 'error_mercurial' => 'URL репозиторію Mercurial повинен починатись із http:// або https://', diff --git a/PHPCI/Migrations/20150131075425_archive_project.php b/PHPCI/Migrations/20150131075425_archive_project.php new file mode 100644 index 00000000..796fd48b --- /dev/null +++ b/PHPCI/Migrations/20150131075425_archive_project.php @@ -0,0 +1,26 @@ +table('project'); + $project->addColumn('archived', 'boolean'); + $project->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + $project = $this->table('project'); + $project->removeColumn('archived'); + $project->save(); + } +} \ No newline at end of file diff --git a/PHPCI/Model/Base/ProjectBase.php b/PHPCI/Model/Base/ProjectBase.php index 305cafbf..0ea77b78 100644 --- a/PHPCI/Model/Base/ProjectBase.php +++ b/PHPCI/Model/Base/ProjectBase.php @@ -44,6 +44,7 @@ class ProjectBase extends Model 'build_config' => null, 'ssh_public_key' => null, 'allow_public_status' => null, + 'archived' => null, ); /** @@ -62,6 +63,7 @@ class ProjectBase extends Model 'build_config' => 'getBuildConfig', 'ssh_public_key' => 'getSshPublicKey', 'allow_public_status' => 'getAllowPublicStatus', + 'archived' => 'getArchived', // Foreign key getters: ); @@ -82,6 +84,7 @@ class ProjectBase extends Model 'build_config' => 'setBuildConfig', 'ssh_public_key' => 'setSshPublicKey', 'allow_public_status' => 'setAllowPublicStatus', + 'archived' => 'setArchived', // Foreign key setters: ); @@ -148,6 +151,12 @@ class ProjectBase extends Model 'type' => 'int', 'length' => 11, ), + 'archived' => array( + 'type' => 'tinyint', + 'length' => 4, + 'nullable' => true, + 'default' => null, + ), ); /** @@ -296,6 +305,18 @@ class ProjectBase extends Model return $rtn; } + /** + * Get the value of Archived / archived. + * + * @return int + */ + public function getArchived() + { + $rtn = $this->data['archived']; + + return $rtn; + } + /** * Set the value of Id / id. * @@ -506,6 +527,24 @@ class ProjectBase extends Model $this->_setModified('allow_public_status'); } + /** + * Set the value of Archived / archived. + * + * @param $value int + */ + public function setArchived($value) + { + $this->_validateInt('Archived', $value); + + if ($this->data['archived'] === $value) { + return; + } + + $this->data['archived'] = $value; + + $this->_setModified('archived'); + } + /** * Get Build models by ProjectId for this Project. * diff --git a/PHPCI/Service/ProjectService.php b/PHPCI/Service/ProjectService.php index 22e940ac..c7ba787c 100644 --- a/PHPCI/Service/ProjectService.php +++ b/PHPCI/Service/ProjectService.php @@ -81,6 +81,10 @@ class ProjectService $project->setAllowPublicStatus((int)$options['allow_public_status']); } + if (array_key_exists('archived', $options)) { + $project->setArchived((bool)$options['archived']); + } + if (array_key_exists('branch', $options)) { $project->setBranch($options['branch']); } diff --git a/PHPCI/View/layout.phtml b/PHPCI/View/layout.phtml index 64ec0834..6c384a9e 100644 --- a/PHPCI/View/layout.phtml +++ b/PHPCI/View/layout.phtml @@ -147,7 +147,7 @@
  • - + User()->getIsAdmin()): ?>
  • @@ -242,6 +242,12 @@
  • +
  • + + + +
  • + From 1f0cd491427b361fd9489ae3fc2f35be43e8ebc4 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Mon, 23 Feb 2015 19:40:31 +0000 Subject: [PATCH 132/329] Adding changelog. --- changelog.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 changelog.md diff --git a/changelog.md b/changelog.md new file mode 100644 index 00000000..15fd9560 --- /dev/null +++ b/changelog.md @@ -0,0 +1,10 @@ +# PHPCI Changelog + +## v.Next + +### New Features: +- SSH-based Mercurial clones. +- Ability to archive projects. + +### Bug Fixes and Tweaks: + From fba6f2372fffa9cca66afb08e1f5573453042146 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Mon, 23 Feb 2015 19:45:33 +0000 Subject: [PATCH 133/329] Updating changelog. --- changelog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 15fd9560..541e4a13 100644 --- a/changelog.md +++ b/changelog.md @@ -3,8 +3,8 @@ ## v.Next ### New Features: -- SSH-based Mercurial clones. -- Ability to archive projects. +- SSH-based Mercurial clones (Commit: [e98647bd](https://github.com/Block8/PHPCI/commit/e98647bd97d49741242d252514b8703504a62869), PR: [#812](https://github.com/Block8/PHPCI/pull/812)) +- Ability to archive projects (Commit: [1466ad06](https://github.com/Block8/PHPCI/commit/1466ad06ef708cbab2b53112fc59e8c1d70c2e33), PR: [#771](https://github.com/Block8/PHPCI/pull/771)) ### Bug Fixes and Tweaks: From f4a0804100a1a94bc7175e98cddc38735daff1d6 Mon Sep 17 00:00:00 2001 From: zviryatko Date: Tue, 24 Feb 2015 10:01:36 +0200 Subject: [PATCH 134/329] Fix username style in user panel block. --- PHPCI/View/layout.phtml | 2 +- public/assets/css/AdminLTE-custom.css | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/PHPCI/View/layout.phtml b/PHPCI/View/layout.phtml index 6c384a9e..569772c1 100644 --- a/PHPCI/View/layout.phtml +++ b/PHPCI/View/layout.phtml @@ -134,7 +134,7 @@
    <?php print $this->User()->getName(); ?>
    -
    +

    User()->getName()); ?>

    diff --git a/public/assets/css/AdminLTE-custom.css b/public/assets/css/AdminLTE-custom.css index a0fb3dd7..b95f7b70 100644 --- a/public/assets/css/AdminLTE-custom.css +++ b/public/assets/css/AdminLTE-custom.css @@ -74,4 +74,14 @@ margin-left: 1px; margin-top: 19px; font-size: 11px; +} + +.user-panel > .image { + padding-right: 15px; +} + +.user-panel > .info { + padding: 0; + font-size: inherit; + line-height: inherit; } \ No newline at end of file From db90f2ea11a76d70e5805c1cab7ddba54a97bd3c Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Wed, 25 Feb 2015 09:36:50 +0000 Subject: [PATCH 135/329] Updating the UpdateCommand to check for a config key rather than a specific file. --- PHPCI/Command/UpdateCommand.php | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/PHPCI/Command/UpdateCommand.php b/PHPCI/Command/UpdateCommand.php index 8170675a..13e31f79 100644 --- a/PHPCI/Command/UpdateCommand.php +++ b/PHPCI/Command/UpdateCommand.php @@ -9,6 +9,7 @@ namespace PHPCI\Command; +use b8\Config; use Monolog\Logger; use PHPCI\Helper\Lang; use Symfony\Component\Console\Command\Command; @@ -59,19 +60,9 @@ class UpdateCommand extends Command protected function verifyInstalled(OutputInterface $output) { - if (!file_exists(PHPCI_DIR . 'PHPCI/config.yml')) { - $output->writeln(''.Lang::get('not_installed').''); - $output->writeln(''.Lang::get('install_instead').''); - return false; - } + $config = Config::getInstance(); + $phpciUrl = $config->get('phpci.url'); - $content = file_get_contents(PHPCI_DIR . 'PHPCI/config.yml'); - if (empty($content)) { - $output->writeln(''.Lang::get('not_installed').''); - $output->writeln(''.Lang::get('install_instead').''); - return false; - } - - return true; + return !empty($phpciUrl); } } From 18ff21174e56874c91cdef02cc62e3c675062e5a Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Wed, 25 Feb 2015 10:33:11 +0000 Subject: [PATCH 136/329] Fixing dates: Stop all dates from appearing as the current date/time. Fixes #820 --- public/assets/js/phpci.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/assets/js/phpci.js b/public/assets/js/phpci.js index a2d5954c..0d262235 100644 --- a/public/assets/js/phpci.js +++ b/public/assets/js/phpci.js @@ -9,8 +9,9 @@ var PHPCI = { $(document).ready(function () { // Format datetimes $('time[datetime]').each(function() { - var $this = $(this); - $this.text(moment(this.dateTime).format($this.data('format') || 'lll')); + var thisDate = $(this).attr('datetime'); + var formattedDate = moment(thisDate).format($(this).data('format') || 'lll'); + $(this).text(formattedDate); }); // Update latest builds every 5 seconds: From e423c73c4e0bbe87e1bf1d4506335be23b900662 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Wed, 25 Feb 2015 14:13:28 +0000 Subject: [PATCH 137/329] Fixing comparison where commit ID is Manual. Fixes #823 --- PHPCI/Model/Build/GithubBuild.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PHPCI/Model/Build/GithubBuild.php b/PHPCI/Model/Build/GithubBuild.php index 7c8f3f6f..1482f1c0 100644 --- a/PHPCI/Model/Build/GithubBuild.php +++ b/PHPCI/Model/Build/GithubBuild.php @@ -209,7 +209,9 @@ class GithubBuild extends RemoteGitBuild if (!empty($prNumber)) { $builder->executeCommand('cd %s && git diff origin/%s "%s"', $path, $this->getBranch(), $file); } else { - $builder->executeCommand('cd %s && git diff %s^! "%s"', $path, $this->getCommitId(), $file); + $commitId = $this->getCommitId(); + $compare = $commitId == 'Manual' ? 'HEAD' : $commitId; + $builder->executeCommand('cd %s && git diff %s^! "%s"', $path, $compare, $file); } $builder->logExecOutput(true); From 8ab098821b048bc09ef616aae5554f8345d15055 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Wed, 25 Feb 2015 14:18:05 +0000 Subject: [PATCH 138/329] Updating Settings Controller to use the configured config file, rather than assuming config.yml --- PHPCI/Controller/SettingsController.php | 7 ++++--- bootstrap.php | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/PHPCI/Controller/SettingsController.php b/PHPCI/Controller/SettingsController.php index 3c5ab5f4..a13c9fd4 100644 --- a/PHPCI/Controller/SettingsController.php +++ b/PHPCI/Controller/SettingsController.php @@ -40,7 +40,7 @@ class SettingsController extends Controller parent::init(); $parser = new Parser(); - $yaml = file_get_contents(APPLICATION_PATH . 'PHPCI/config.yml'); + $yaml = file_get_contents(PHPCI_CONFIG_FILE); $this->settings = $parser->parse($yaml); } @@ -76,6 +76,7 @@ class SettingsController extends Controller $authSettings = $this->settings['phpci']['authentication_settings']; } + $this->view->configFile = PHPCI_CONFIG_FILE; $this->view->basicSettings = $this->getBasicForm($basicSettings); $this->view->buildSettings = $this->getBuildForm($buildSettings); $this->view->github = $this->getGithubForm(); @@ -241,7 +242,7 @@ class SettingsController extends Controller { $dumper = new Dumper(); $yaml = $dumper->dump($this->settings, 4); - file_put_contents(APPLICATION_PATH . 'PHPCI/config.yml', $yaml); + file_put_contents(PHPCI_CONFIG_FILE, $yaml); if (error_get_last()) { $error_get_last = error_get_last(); @@ -386,7 +387,7 @@ class SettingsController extends Controller */ protected function canWriteConfig() { - return is_writeable(APPLICATION_PATH . 'PHPCI/config.yml'); + return is_writeable(PHPCI_CONFIG_FILE); } /** diff --git a/bootstrap.php b/bootstrap.php index b15a7e59..252697a8 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -25,6 +25,8 @@ if (!empty($configEnv)) { $configFile = $configEnv; } +define('PHPCI_CONFIG_FILE', $configFile); + // If we don't have a config file at all, fail at this point and tell the user to install: if (!file_exists($configFile) && (!defined('PHPCI_IS_CONSOLE') || !PHPCI_IS_CONSOLE)) { $message = 'PHPCI has not yet been installed - Please use the command "./console phpci:install" '; From 4a84aad327c1c8b2ac2731d5d72617025bd8ac3a Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 26 Feb 2015 08:08:12 +0000 Subject: [PATCH 139/329] Updating basic Dockerfile. --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index af646d09..5fb6df83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,4 +15,7 @@ RUN git config --global user.email "hello@php.ci" ADD ./ /phpci +RUN php -r "readfile('https://getcomposer.org/installer');" | php +RUN mv composer.phar /phpci/composer + CMD /phpci/daemonise phpci:daemonise From bfc56a753d9ac3669dfe74df79c2867f0436ec26 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 26 Feb 2015 08:08:45 +0000 Subject: [PATCH 140/329] Updating dependencies. --- composer.json | 2 +- composer.lock | 55 ++++++++++++++++++++++----------------------------- 2 files changed, 25 insertions(+), 32 deletions(-) diff --git a/composer.json b/composer.json index 0e5a308b..2eec5e30 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ "phpunit/phpunit": "~4.0", "phpspec/prophecy-phpunit": "~1.0", "phpmd/phpmd": "~2.0", - "squizlabs/php_codesniffer": "~1.5", + "squizlabs/php_codesniffer": "~2.0", "block8/php-docblock-checker": "~1.0", "phploc/phploc": "~2.0" }, diff --git a/composer.lock b/composer.lock index 4ec183ff..c5abc102 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "b24b68e21f65f3de2b7c3c84a67a0e6f", + "hash": "4ae188c6be1c1388de6271a3b0e0475d", "packages": [ { "name": "block8/b8framework", @@ -255,16 +255,16 @@ }, { "name": "robmorgan/phinx", - "version": "v0.4.2.1", + "version": "v0.4.3", "source": { "type": "git", "url": "https://github.com/robmorgan/phinx.git", - "reference": "1bc1396392d4073b8b29ee5289e445889cbc12b5" + "reference": "0d1f9cb9939f65f506a8a3f5fee356764c310fd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/robmorgan/phinx/zipball/1bc1396392d4073b8b29ee5289e445889cbc12b5", - "reference": "1bc1396392d4073b8b29ee5289e445889cbc12b5", + "url": "https://api.github.com/repos/robmorgan/phinx/zipball/0d1f9cb9939f65f506a8a3f5fee356764c310fd4", + "reference": "0d1f9cb9939f65f506a8a3f5fee356764c310fd4", "shasum": "" }, "require": { @@ -313,7 +313,7 @@ "migrations", "phinx" ], - "time": "2015-02-08 03:41:44" + "time": "2015-02-23 16:38:12" }, { "name": "swiftmailer/swiftmailer", @@ -935,21 +935,21 @@ }, { "name": "phpspec/prophecy-phpunit", - "version": "v1.0.1", - "target-dir": "Prophecy/PhpUnit", + "version": "v1.1.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy-phpunit.git", - "reference": "640c8c3bc9e02d7878e5ed22b1f79818d6bb6caf" + "reference": "0d06c84b9f26aab2b9940354f0fe6037dd9799c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy-phpunit/zipball/640c8c3bc9e02d7878e5ed22b1f79818d6bb6caf", - "reference": "640c8c3bc9e02d7878e5ed22b1f79818d6bb6caf", + "url": "https://api.github.com/repos/phpspec/prophecy-phpunit/zipball/0d06c84b9f26aab2b9940354f0fe6037dd9799c3", + "reference": "0d06c84b9f26aab2b9940354f0fe6037dd9799c3", "shasum": "" }, "require": { - "phpspec/prophecy": "~1.0" + "php": ">=5.3.3", + "phpspec/prophecy": "~1.3" }, "suggest": { "phpunit/phpunit": "if it is not installed globally" @@ -961,7 +961,7 @@ } }, "autoload": { - "psr-0": { + "psr-4": { "Prophecy\\PhpUnit\\": "" } }, @@ -976,12 +976,12 @@ } ], "description": "PhpUnit test case integrating the Prophecy mocking library", - "homepage": "http://phpspec.org", + "homepage": "http://phpspec.net", "keywords": [ "phpunit", "prophecy" ], - "time": "2014-03-03 23:03:12" + "time": "2015-02-09 11:20:26" }, { "name": "phpunit/php-code-coverage", @@ -1812,46 +1812,39 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "1.5.6", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "6f3e42d311b882b25b4d409d23a289f4d3b803d5" + "reference": "b301c98f19414d836fdaa678648745fcca5aeb4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/6f3e42d311b882b25b4d409d23a289f4d3b803d5", - "reference": "6f3e42d311b882b25b4d409d23a289f4d3b803d5", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/b301c98f19414d836fdaa678648745fcca5aeb4f", + "reference": "b301c98f19414d836fdaa678648745fcca5aeb4f", "shasum": "" }, "require": { "ext-tokenizer": "*", "php": ">=5.1.2" }, - "suggest": { - "phpunit/php-timer": "dev-master" - }, "bin": [ - "scripts/phpcs" + "scripts/phpcs", + "scripts/phpcbf" ], "type": "library", - "extra": { - "branch-alias": { - "dev-phpcs-fixer": "2.0.x-dev" - } - }, "autoload": { "classmap": [ "CodeSniffer.php", "CodeSniffer/CLI.php", "CodeSniffer/Exception.php", "CodeSniffer/File.php", + "CodeSniffer/Fixer.php", "CodeSniffer/Report.php", "CodeSniffer/Reporting.php", "CodeSniffer/Sniff.php", "CodeSniffer/Tokens.php", "CodeSniffer/Reports/", - "CodeSniffer/CommentParser/", "CodeSniffer/Tokenizers/", "CodeSniffer/DocGenerators/", "CodeSniffer/Standards/AbstractPatternSniff.php", @@ -1877,13 +1870,13 @@ "role": "lead" } ], - "description": "PHP_CodeSniffer tokenises PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", "homepage": "http://www.squizlabs.com/php-codesniffer", "keywords": [ "phpcs", "standards" ], - "time": "2014-12-04 22:32:15" + "time": "2015-01-21 22:44:05" }, { "name": "symfony/dependency-injection", From ab4396e00d5a47c360fbf5cbe7ac0f5cc98d92d6 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 26 Feb 2015 08:31:58 +0000 Subject: [PATCH 141/329] Hopefully fixing a bug where reporting errors back to Github causes an infinite loop. --- PHPCI/Helper/Diff.php | 6 +++++- PHPCI/Model/Build/GithubBuild.php | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/PHPCI/Helper/Diff.php b/PHPCI/Helper/Diff.php index e50c7eaf..5dcb5388 100644 --- a/PHPCI/Helper/Diff.php +++ b/PHPCI/Helper/Diff.php @@ -26,11 +26,15 @@ class Diff */ public function getLinePositions($diff) { + if (empty($diff)) { + return null; + } + $rtn = array(); $diffLines = explode(PHP_EOL, $diff); - while (1) { + while (count($diffLines)) { $line = array_shift($diffLines); if (substr($line, 0, 2) == '@@') { diff --git a/PHPCI/Model/Build/GithubBuild.php b/PHPCI/Model/Build/GithubBuild.php index 1482f1c0..4d61b9d6 100644 --- a/PHPCI/Model/Build/GithubBuild.php +++ b/PHPCI/Model/Build/GithubBuild.php @@ -211,7 +211,7 @@ class GithubBuild extends RemoteGitBuild } else { $commitId = $this->getCommitId(); $compare = $commitId == 'Manual' ? 'HEAD' : $commitId; - $builder->executeCommand('cd %s && git diff %s^! "%s"', $path, $compare, $file); + $builder->executeCommand('cd %s && git diff %s^^ "%s"', $path, $compare, $file); } $builder->logExecOutput(true); From 86b9c05f98e3fa740565e2d45884afecd29eae24 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 26 Feb 2015 08:45:42 +0000 Subject: [PATCH 142/329] Fixing PHPCS errors. --- PHPCI/Controller/PluginController.php | 2 +- PHPCI/Controller/WebhookController.php | 6 ++---- PHPCI/Helper/BaseCommandExecutor.php | 1 - PHPCI/Helper/Github.php | 6 ------ PHPCI/Helper/LoginIsDisabled.php | 2 +- PHPCI/Logging/Handler.php | 2 -- PHPCI/Plugin/Phar.php | 1 - PHPCI/Plugin/PhpTalLint.php | 1 - PHPCI/Plugin/Util/Executor.php | 2 -- PHPCI/Plugin/Util/FilesPluginInformation.php | 2 +- bootstrap.php | 8 ++------ vars.php | 6 ++++++ 12 files changed, 13 insertions(+), 26 deletions(-) diff --git a/PHPCI/Controller/PluginController.php b/PHPCI/Controller/PluginController.php index 20675e46..cefcdbb5 100644 --- a/PHPCI/Controller/PluginController.php +++ b/PHPCI/Controller/PluginController.php @@ -126,7 +126,7 @@ class PluginController extends \PHPCI\Controller /** * Convert array to json and save composer.json - * + * * @param $array */ protected function setComposerJson($array) diff --git a/PHPCI/Controller/WebhookController.php b/PHPCI/Controller/WebhookController.php index cef7dc53..619eec16 100644 --- a/PHPCI/Controller/WebhookController.php +++ b/PHPCI/Controller/WebhookController.php @@ -157,6 +157,8 @@ class WebhookController extends \PHPCI\Controller * Handle the payload when Github sends a commit webhook. * @param $project * @param array $payload + * @param b8\Http\Response\JsonResponse $response + * @return b8\Http\Response\JsonResponse */ protected function githubCommitRequest($project, array $payload, b8\Http\Response\JsonResponse $response) { @@ -167,7 +169,6 @@ class WebhookController extends \PHPCI\Controller } try { - if (isset($payload['commits']) && is_array($payload['commits'])) { // If we have a list of commits, then add them all as builds to be tested: @@ -265,13 +266,10 @@ class WebhookController extends \PHPCI\Controller $payload = json_decode($payloadString, true); try { - - // build on merge request events if (isset($payload['object_kind']) && $payload['object_kind'] == 'merge_request') { $attributes = $payload['object_attributes']; if ($attributes['state'] == 'opened' || $attributes['state'] == 'reopened') { - $branch = $attributes['source_branch']; $commit = $attributes['last_commit']; $committer = $commit['author']['email']; diff --git a/PHPCI/Helper/BaseCommandExecutor.php b/PHPCI/Helper/BaseCommandExecutor.php index 4c2f1fa8..5435d597 100644 --- a/PHPCI/Helper/BaseCommandExecutor.php +++ b/PHPCI/Helper/BaseCommandExecutor.php @@ -160,7 +160,6 @@ abstract class BaseCommandExecutor implements CommandExecutor $this->logger->log(Lang::get('looking_for_binary', $bin), LogLevel::DEBUG); if (is_dir($composerBin) && is_file($composerBin.'/'.$bin)) { - $this->logger->log(Lang::get('found_in_path', $composerBin, $bin), LogLevel::DEBUG); $binaryPath = $composerBin . '/' . $bin; break; diff --git a/PHPCI/Helper/Github.php b/PHPCI/Helper/Github.php index 00d959df..67173eb4 100644 --- a/PHPCI/Helper/Github.php +++ b/PHPCI/Helper/Github.php @@ -48,24 +48,18 @@ class Github $res = $http->get($url, $params); foreach ($res['body'] as $item) { - $results[] = $item; - } foreach ($res['headers'] as $header) { - if (preg_match('/^Link: <([^>]+)>; rel="next"/', $header, $r)) { - $host = parse_url($r[1]); parse_str($host['query'], $params); $results = $this->makeRecursiveRequest($host['path'], $params, $results); break; - } - } return $results; diff --git a/PHPCI/Helper/LoginIsDisabled.php b/PHPCI/Helper/LoginIsDisabled.php index d30fcd9f..437b95cd 100644 --- a/PHPCI/Helper/LoginIsDisabled.php +++ b/PHPCI/Helper/LoginIsDisabled.php @@ -20,7 +20,7 @@ use b8\Config; class LoginIsDisabled { /** - * Checks if + * Checks if * @param $method * @param array $params * @return mixed|null diff --git a/PHPCI/Logging/Handler.php b/PHPCI/Logging/Handler.php index ebfa2620..e22351da 100644 --- a/PHPCI/Logging/Handler.php +++ b/PHPCI/Logging/Handler.php @@ -70,7 +70,6 @@ class Handler public function handleError($level, $message, $file, $line) { if (error_reporting() & $level) { - $exception_level = isset($this->levels[$level]) ? $this->levels[$level] : $level; throw new \ErrorException( @@ -140,7 +139,6 @@ class Handler protected function log(\Exception $exception) { if (null !== $this->logger) { - $message = sprintf( '%s: %s (uncaught exception) at %s line %s', get_class($exception), diff --git a/PHPCI/Plugin/Phar.php b/PHPCI/Plugin/Phar.php index f04580e7..0500fddc 100644 --- a/PHPCI/Plugin/Phar.php +++ b/PHPCI/Plugin/Phar.php @@ -227,7 +227,6 @@ class Phar implements \PHPCI\Plugin $success = false; try { - $phar = new PHPPhar($this->getDirectory() . '/' . $this->getFilename(), 0, $this->getFilename()); $phar->buildFromDirectory($this->getPHPCI()->buildPath, $this->getRegExp()); diff --git a/PHPCI/Plugin/PhpTalLint.php b/PHPCI/Plugin/PhpTalLint.php index 146b0479..e12b6c34 100644 --- a/PHPCI/Plugin/PhpTalLint.php +++ b/PHPCI/Plugin/PhpTalLint.php @@ -210,7 +210,6 @@ class PhpTalLint implements PHPCI\Plugin $output = $this->phpci->getLastOutput(); if (preg_match('/Found (.+?) (error|warning)/i', $output, $matches)) { - $rows = explode(PHP_EOL, $output); unset($rows[0]); diff --git a/PHPCI/Plugin/Util/Executor.php b/PHPCI/Plugin/Util/Executor.php index 9482c81f..8c74707e 100644 --- a/PHPCI/Plugin/Util/Executor.php +++ b/PHPCI/Plugin/Util/Executor.php @@ -50,10 +50,8 @@ class Executor // Try and execute it: if ($this->executePlugin($plugin, $options)) { - // Execution was successful: $this->logger->logSuccess(Lang::get('plugin_success')); - } elseif ($stage == 'setup') { // If we're in the "setup" stage, execution should not continue after // a plugin has failed: diff --git a/PHPCI/Plugin/Util/FilesPluginInformation.php b/PHPCI/Plugin/Util/FilesPluginInformation.php index 2593ae22..d5ccebd5 100644 --- a/PHPCI/Plugin/Util/FilesPluginInformation.php +++ b/PHPCI/Plugin/Util/FilesPluginInformation.php @@ -69,7 +69,7 @@ class FilesPluginInformation implements InstalledPluginInformation { return array_map( function (\stdClass $plugin) { - return $plugin->class; + return $plugin->class; }, $this->getInstalledPlugins() ); diff --git a/bootstrap.php b/bootstrap.php index 252697a8..e3f9985e 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -16,17 +16,13 @@ if (empty($timezone)) { date_default_timezone_set('UTC'); } -// If the PHPCI config file is not where we expect it, try looking in -// env for an alternative config path. $configFile = dirname(__FILE__) . '/PHPCI/config.yml'; - $configEnv = getenv('phpci_config_file'); -if (!empty($configEnv)) { + +if (!empty($configEnv) && file_exists($configEnv)) { $configFile = $configEnv; } -define('PHPCI_CONFIG_FILE', $configFile); - // If we don't have a config file at all, fail at this point and tell the user to install: if (!file_exists($configFile) && (!defined('PHPCI_IS_CONSOLE') || !PHPCI_IS_CONSOLE)) { $message = 'PHPCI has not yet been installed - Please use the command "./console phpci:install" '; diff --git a/vars.php b/vars.php index 2408d41d..aed6b6f8 100644 --- a/vars.php +++ b/vars.php @@ -29,3 +29,9 @@ if (!defined('PHPCI_IS_CONSOLE')) { if (!defined('IS_WIN')) { define('IS_WIN', ((strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? true : false)); } + +// If an environment variable is set defining our config location, use that +// otherwise fall back to PHPCI/config.yml. +if (!defined('PHPCI_CONFIG_FILE')) { + define('PHPCI_CONFIG_FILE', $configFile); +} \ No newline at end of file From b8983b23a30b66058be1927c9848f8c851785f05 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Thu, 26 Feb 2015 08:48:40 +0000 Subject: [PATCH 143/329] Fixing final PHPCS error. --- vars.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vars.php b/vars.php index aed6b6f8..b622ab31 100644 --- a/vars.php +++ b/vars.php @@ -34,4 +34,4 @@ if (!defined('IS_WIN')) { // otherwise fall back to PHPCI/config.yml. if (!defined('PHPCI_CONFIG_FILE')) { define('PHPCI_CONFIG_FILE', $configFile); -} \ No newline at end of file +} From da9be4930dee9607dc661cf65bc4f451903322c6 Mon Sep 17 00:00:00 2001 From: corpsee Date: Sat, 28 Feb 2015 23:13:02 +0600 Subject: [PATCH 144/329] Added total builds count to index and project page --- PHPCI/Controller/HomeController.php | 15 ++++++++++----- PHPCI/Controller/ProjectController.php | 2 +- PHPCI/View/Project/view.phtml | 3 +-- PHPCI/View/SummaryTable.phtml | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/PHPCI/Controller/HomeController.php b/PHPCI/Controller/HomeController.php index 2455e709..8b57db58 100644 --- a/PHPCI/Controller/HomeController.php +++ b/PHPCI/Controller/HomeController.php @@ -98,12 +98,16 @@ class HomeController extends \PHPCI\Controller protected function getSummaryHtml($projects) { $summaryBuilds = array(); - $successes = array(); - $failures = array(); + $successes = array(); + $failures = array(); + $counts = array(); foreach ($projects['items'] as $project) { $summaryBuilds[$project->getId()] = $this->buildStore->getLatestBuilds($project->getId()); + $count = $this->buildStore->getWhere(array('project_id' => $project->getId()), 1, 0, array(), array('id' => 'DESC')); + $counts[$project->getId()] = $count['count']; + $success = $this->buildStore->getLastBuildByStatus($project->getId(), Build::STATUS_SUCCESS); $failure = $this->buildStore->getLastBuildByStatus($project->getId(), Build::STATUS_FAILED); @@ -112,10 +116,11 @@ class HomeController extends \PHPCI\Controller } $summaryView = new b8\View('SummaryTable'); - $summaryView->projects = $projects['items']; - $summaryView->builds = $summaryBuilds; + $summaryView->projects = $projects['items']; + $summaryView->builds = $summaryBuilds; $summaryView->successful = $successes; - $summaryView->failed = $failures; + $summaryView->failed = $failures; + $summaryView->counts = $counts; return $summaryView->render(); } diff --git a/PHPCI/Controller/ProjectController.php b/PHPCI/Controller/ProjectController.php index 285982a1..3a55f49b 100644 --- a/PHPCI/Controller/ProjectController.php +++ b/PHPCI/Controller/ProjectController.php @@ -86,7 +86,7 @@ class ProjectController extends PHPCI\Controller $this->view->builds = $builds[0]; $this->view->total = $builds[1]; $this->view->project = $project; - $this->view->branch = urldecode($branch); + $this->view->branch = urldecode($branch); $this->view->branches = $this->projectStore->getKnownBranches($projectId); $this->view->page = $page; $this->view->pages = $pages; diff --git a/PHPCI/View/Project/view.phtml b/PHPCI/View/Project/view.phtml index 0bfd5e59..900349c8 100644 --- a/PHPCI/View/Project/view.phtml +++ b/PHPCI/View/Project/view.phtml @@ -36,8 +36,7 @@
    - -

    +

    ()

    diff --git a/PHPCI/View/SummaryTable.phtml b/PHPCI/View/SummaryTable.phtml index 56df6e38..9ddaabb0 100644 --- a/PHPCI/View/SummaryTable.phtml +++ b/PHPCI/View/SummaryTable.phtml @@ -123,7 +123,7 @@ foreach($projects as $project): - + (getId()]; ?>) From 9f534711866112429c84494f3d6fb5b5ebb4bc10 Mon Sep 17 00:00:00 2001 From: corpsee Date: Sat, 28 Feb 2015 23:38:56 +0600 Subject: [PATCH 145/329] Added Date column for builds table in project page. --- PHPCI/Languages/lang.da.php | 1 + PHPCI/Languages/lang.de.php | 1 + PHPCI/Languages/lang.el.php | 1 + PHPCI/Languages/lang.en.php | 1 + PHPCI/Languages/lang.fr.php | 1 + PHPCI/Languages/lang.it.php | 1 + PHPCI/Languages/lang.nl.php | 1 + PHPCI/Languages/lang.pl.php | 1 + PHPCI/Languages/lang.ru.php | 1 + PHPCI/Languages/lang.uk.php | 1 + PHPCI/View/BuildsTable.phtml | 1 + PHPCI/View/Project/view.phtml | 1 + 12 files changed, 12 insertions(+) diff --git a/PHPCI/Languages/lang.da.php b/PHPCI/Languages/lang.da.php index 5ba3d96f..fdf27bc6 100644 --- a/PHPCI/Languages/lang.da.php +++ b/PHPCI/Languages/lang.da.php @@ -126,6 +126,7 @@ i din foretrukne hosting-platform.', 'all_branches' => 'Alle branches', 'builds' => 'Builds', 'id' => 'ID', + 'date' => 'Date', 'project' => 'Projekt', 'commit' => 'Commit', 'branch' => 'Branch', diff --git a/PHPCI/Languages/lang.de.php b/PHPCI/Languages/lang.de.php index bdcc1459..a4b83580 100644 --- a/PHPCI/Languages/lang.de.php +++ b/PHPCI/Languages/lang.de.php @@ -127,6 +127,7 @@ generiert. Um es zu verwenden, fügen Sie einfach den folgenden Public Key im Ab 'all_branches' => 'Alle Branches', 'builds' => 'Builds', 'id' => 'ID', + 'date' => 'Date', 'project' => 'Projekt', 'commit' => 'Commit', 'branch' => 'Branch', diff --git a/PHPCI/Languages/lang.el.php b/PHPCI/Languages/lang.el.php index 82b3cf22..bf5649fb 100644 --- a/PHPCI/Languages/lang.el.php +++ b/PHPCI/Languages/lang.el.php @@ -127,6 +127,7 @@ PHPCI', 'all_branches' => 'Όλες οι διακλαδώσεις', 'builds' => 'Κατασκευές', 'id' => 'Αριθμός αναγνώρισης', + 'date' => 'Date', 'project' => 'Έργο', 'commit' => 'Συνεισφορά', 'branch' => 'Διακλάδωση', diff --git a/PHPCI/Languages/lang.en.php b/PHPCI/Languages/lang.en.php index 450be1ef..1e4ffa32 100644 --- a/PHPCI/Languages/lang.en.php +++ b/PHPCI/Languages/lang.en.php @@ -128,6 +128,7 @@ PHPCI', 'all_branches' => 'All Branches', 'builds' => 'Builds', 'id' => 'ID', + 'date' => 'Date', 'project' => 'Project', 'commit' => 'Commit', 'branch' => 'Branch', diff --git a/PHPCI/Languages/lang.fr.php b/PHPCI/Languages/lang.fr.php index 20410684..77e0d0ab 100644 --- a/PHPCI/Languages/lang.fr.php +++ b/PHPCI/Languages/lang.fr.php @@ -127,6 +127,7 @@ PHPCI', 'all_branches' => 'Toutes les branches', 'builds' => 'Builds', 'id' => 'ID', + 'date' => 'Date', 'project' => 'Projet', 'commit' => 'Commit', 'branch' => 'Branche', diff --git a/PHPCI/Languages/lang.it.php b/PHPCI/Languages/lang.it.php index 46ca14fd..50b59040 100644 --- a/PHPCI/Languages/lang.it.php +++ b/PHPCI/Languages/lang.it.php @@ -127,6 +127,7 @@ PHPCI', 'all_branches' => 'Tutti i Branche', 'builds' => 'Builds', 'id' => 'ID', + 'date' => 'Date', 'project' => 'Progetto', 'commit' => 'Commit', 'branch' => 'Branch', diff --git a/PHPCI/Languages/lang.nl.php b/PHPCI/Languages/lang.nl.php index 83d5e317..d62cd4e5 100644 --- a/PHPCI/Languages/lang.nl.php +++ b/PHPCI/Languages/lang.nl.php @@ -127,6 +127,7 @@ van je gekozen source code hosting platform', 'all_branches' => 'Alle brances', 'builds' => 'Builds', 'id' => 'ID', + 'date' => 'Date', 'project' => 'Project', 'commit' => 'Commit', 'branch' => 'Branch', diff --git a/PHPCI/Languages/lang.pl.php b/PHPCI/Languages/lang.pl.php index 9d6de6c8..640ac4e0 100644 --- a/PHPCI/Languages/lang.pl.php +++ b/PHPCI/Languages/lang.pl.php @@ -128,6 +128,7 @@ od wybranego kodu źródłowego platformy hostingowej.', 'all_branches' => 'Wszystkie Gałęzie', 'builds' => 'Budowania', 'id' => 'ID', + 'date' => 'Date', 'project' => 'Projekt', 'commit' => 'Commit', 'branch' => 'Gałąź', diff --git a/PHPCI/Languages/lang.ru.php b/PHPCI/Languages/lang.ru.php index 2f9ab0ee..dba0d934 100644 --- a/PHPCI/Languages/lang.ru.php +++ b/PHPCI/Languages/lang.ru.php @@ -126,6 +126,7 @@ PHPCI', 'all_branches' => 'Все ветки', 'builds' => 'Сборки', 'id' => 'ID', + 'date' => 'Дата', 'project' => 'Проект', 'commit' => 'Коммит', 'branch' => 'Ветка', diff --git a/PHPCI/Languages/lang.uk.php b/PHPCI/Languages/lang.uk.php index 7e4a2191..0fd6d66a 100644 --- a/PHPCI/Languages/lang.uk.php +++ b/PHPCI/Languages/lang.uk.php @@ -126,6 +126,7 @@ PHPCI', 'all_branches' => 'Усі гілки', 'builds' => 'Збірки', 'id' => 'ID', + 'date' => 'Дата', 'project' => 'Проект', 'commit' => 'Комміт', 'branch' => 'Гілка', diff --git a/PHPCI/View/BuildsTable.phtml b/PHPCI/View/BuildsTable.phtml index c9d44930..93ea593b 100644 --- a/PHPCI/View/BuildsTable.phtml +++ b/PHPCI/View/BuildsTable.phtml @@ -39,6 +39,7 @@ switch($build->getStatus()) ?> + + From 000aff9121c7dbeeae9cdd5f217cfd4bf5e8fcd3 Mon Sep 17 00:00:00 2001 From: corpsee Date: Sat, 28 Feb 2015 23:51:04 +0600 Subject: [PATCH 146/329] Code style fix --- PHPCI/Controller/HomeController.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/PHPCI/Controller/HomeController.php b/PHPCI/Controller/HomeController.php index 8b57db58..d0e5a14b 100644 --- a/PHPCI/Controller/HomeController.php +++ b/PHPCI/Controller/HomeController.php @@ -105,7 +105,13 @@ class HomeController extends \PHPCI\Controller foreach ($projects['items'] as $project) { $summaryBuilds[$project->getId()] = $this->buildStore->getLatestBuilds($project->getId()); - $count = $this->buildStore->getWhere(array('project_id' => $project->getId()), 1, 0, array(), array('id' => 'DESC')); + $count = $this->buildStore->getWhere( + array('project_id' => $project->getId()), + 1, + 0, + array(), + array('id' => 'DESC') + ); $counts[$project->getId()] = $count['count']; $success = $this->buildStore->getLastBuildByStatus($project->getId(), Build::STATUS_SUCCESS); From f97089529c332a83a314fefc1d9daf58407b5be4 Mon Sep 17 00:00:00 2001 From: corpsee Date: Mon, 2 Mar 2015 08:18:40 +0600 Subject: [PATCH 147/329] Fixed 'date' it lang string --- PHPCI/Languages/lang.it.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/Languages/lang.it.php b/PHPCI/Languages/lang.it.php index 50b59040..b022068c 100644 --- a/PHPCI/Languages/lang.it.php +++ b/PHPCI/Languages/lang.it.php @@ -127,7 +127,7 @@ PHPCI', 'all_branches' => 'Tutti i Branche', 'builds' => 'Builds', 'id' => 'ID', - 'date' => 'Date', + 'date' => 'Data', 'project' => 'Progetto', 'commit' => 'Commit', 'branch' => 'Branch', From d2e6182a2ff847035b86d2e9d46184fa907563bf Mon Sep 17 00:00:00 2001 From: Mark Clements Date: Mon, 2 Mar 2015 22:31:01 +0000 Subject: [PATCH 148/329] Fixed an inconsistency in the way the prompts in the install scripts were being output. All the DB/host fields have a space after the colon, which is much better as it means the text you type is slightly separated from the prompt. However, the admin user fields didn't include this space which was inconsistent and made the install script a little less professional. I have therefore added the missing space for the prompts which didn't have it in the English language file, and have also updated all other language files to also use this format. Most of them followed the same inconsistency as the English version, though some were consistent but without a space, and some which were differently inconsistent (both internally, and between languages). --- PHPCI/Languages/lang.da.php | 16 ++++++++-------- PHPCI/Languages/lang.de.php | 6 +++--- PHPCI/Languages/lang.el.php | 12 ++++++------ PHPCI/Languages/lang.en.php | 6 +++--- PHPCI/Languages/lang.fr.php | 6 +++--- PHPCI/Languages/lang.it.php | 6 +++--- PHPCI/Languages/lang.nl.php | 16 ++++++++-------- PHPCI/Languages/lang.pl.php | 16 ++++++++-------- PHPCI/Languages/lang.ru.php | 6 +++--- PHPCI/Languages/lang.uk.php | 16 ++++++++-------- 10 files changed, 53 insertions(+), 53 deletions(-) diff --git a/PHPCI/Languages/lang.da.php b/PHPCI/Languages/lang.da.php index 5ba3d96f..3f8f2f38 100644 --- a/PHPCI/Languages/lang.da.php +++ b/PHPCI/Languages/lang.da.php @@ -296,15 +296,15 @@ du kører composer update.', Kontrollér venligst nedenstående fejl før du fortsætter.', 'must_be_valid_email' => 'Skal være en gyldig email-adresse.', 'must_be_valid_url' => 'Skal være en gyldig URL.', - 'enter_name' => 'Administrator-navn:', - 'enter_email' => 'Administrators email-adresse:', - 'enter_password' => 'Administrator-adgangskode:', - 'enter_phpci_url' => 'Din PHPCI URL (eksempelvis "http://phpci.local"):', + 'enter_name' => 'Administrator-navn: ', + 'enter_email' => 'Administrators email-adresse: ', + 'enter_password' => 'Administrator-adgangskode: ', + 'enter_phpci_url' => 'Din PHPCI URL (eksempelvis "http://phpci.local"): ', - 'enter_db_host' => 'Indtast dit MySQL-hostnavn [localhost]:', - 'enter_db_name' => 'Indtast dit MySQL database-navn [phpci]:', - 'enter_db_user' => 'Indtast dit MySQL-brugernavn [phpci]:', - 'enter_db_pass' => 'Indtast dit MySQL-password:', + 'enter_db_host' => 'Indtast dit MySQL-hostnavn [localhost]: ', + 'enter_db_name' => 'Indtast dit MySQL database-navn [phpci]: ', + 'enter_db_user' => 'Indtast dit MySQL-brugernavn [phpci]: ', + 'enter_db_pass' => 'Indtast dit MySQL-password: ', 'could_not_connect' => 'PHPCI kunne ikke forbinde til MySQL med de angivning oplysninger. Forsøg igen.', 'setting_up_db' => 'Indlæser database...', 'user_created' => 'Brugerkonto oprettet!', diff --git a/PHPCI/Languages/lang.de.php b/PHPCI/Languages/lang.de.php index bdcc1459..6a8bd5c4 100644 --- a/PHPCI/Languages/lang.de.php +++ b/PHPCI/Languages/lang.de.php @@ -294,9 +294,9 @@ generiert. Um es zu verwenden, fügen Sie einfach den folgenden Public Key im Ab Bitte überprüfen Sie die Fehler, bevor Sie fortfahren,', 'must_be_valid_email' => 'Muss eine gültige Emailadresse sein.', 'must_be_valid_url' => 'Muss eine valide URL sein.', - 'enter_name' => 'Name des Administrators:', - 'enter_email' => 'Emailadresse des Administrators:', - 'enter_password' => 'Passwort des Administrators:', + 'enter_name' => 'Name des Administrators: ', + 'enter_email' => 'Emailadresse des Administrators: ', + 'enter_password' => 'Passwort des Administrators: ', 'enter_phpci_url' => 'Ihre PHPCI-URL (z.B. "http://phpci.local"): ', 'enter_db_host' => 'Bitte geben Sie Ihren MySQL-Host ein [localhost]: ', diff --git a/PHPCI/Languages/lang.el.php b/PHPCI/Languages/lang.el.php index 82b3cf22..e3503c99 100644 --- a/PHPCI/Languages/lang.el.php +++ b/PHPCI/Languages/lang.el.php @@ -297,15 +297,15 @@ Services του Bitbucket αποθετηρίου σας.', Παρακαλούμε διαβάστε τα παραπάνω λάθη πριν συνεχίσετε.', 'must_be_valid_email' => 'Πρέπει να είναι μια έγκυρη διεύθυνση ηλεκτρονικού ταχυδρομείου.', 'must_be_valid_url' => 'Πρέπει να είναι μια έγκυρη διεύθυνση URL.', - 'enter_name' => 'Όνομα διαχειριστή:', - 'enter_email' => 'Ηλ. Διεύθυνση διαχειριστή:', - 'enter_password' => 'Κωδικός πρόσβασης διαχειριστή:', + 'enter_name' => 'Όνομα διαχειριστή: ', + 'enter_email' => 'Ηλ. Διεύθυνση διαχειριστή: ', + 'enter_password' => 'Κωδικός πρόσβασης διαχειριστή: ', 'enter_phpci_url' => 'Ο URL σύνδεσμος σας για το PHPCI ("http://phpci.local" για παράδειγμα): ', - 'enter_db_host' => 'Παρακαλώ εισάγετε τον MySQL οικοδεσπότη σας [localhost]:', + 'enter_db_host' => 'Παρακαλώ εισάγετε τον MySQL οικοδεσπότη σας [localhost]: ', 'enter_db_name' => 'Παρακαλώ εισάγετε το όνομα της MySQL βάσης δεδομένων σας [phpci]: ', - 'enter_db_user' => 'Παρακαλώ εισάγετε το όνομα χρήστη της MySQL σας [phpci]:', - 'enter_db_pass' => 'Παρακαλώ εισάγετε τον κωδικό χρήστη της MySQL σας:', + 'enter_db_user' => 'Παρακαλώ εισάγετε το όνομα χρήστη της MySQL σας [phpci]: ', + 'enter_db_pass' => 'Παρακαλώ εισάγετε τον κωδικό χρήστη της MySQL σας: ', 'could_not_connect' => 'Το PHPCI δεν μπόρεσε να συνδεθεί με την MySQL με τα στοχεία που δώσατε. Παρακαλώ δοκιμάστε ξανά.', 'setting_up_db' => 'Γίνεται ρύθμιση της βάσης δεδομένων σας ...', 'user_created' => 'Λογαριασμός χρήστη δημιουργήθηκε!', diff --git a/PHPCI/Languages/lang.en.php b/PHPCI/Languages/lang.en.php index 450be1ef..2040986d 100644 --- a/PHPCI/Languages/lang.en.php +++ b/PHPCI/Languages/lang.en.php @@ -301,9 +301,9 @@ PHPCI', Please review the errors above before continuing.', 'must_be_valid_email' => 'Must be a valid email address.', 'must_be_valid_url' => 'Must be a valid URL.', - 'enter_name' => 'Admin Name:', - 'enter_email' => 'Admin Email:', - 'enter_password' => 'Admin Password:', + 'enter_name' => 'Admin Name: ', + 'enter_email' => 'Admin Email: ', + 'enter_password' => 'Admin Password: ', 'enter_phpci_url' => 'Your PHPCI URL ("http://phpci.local" for example): ', 'enter_db_host' => 'Please enter your MySQL host [localhost]: ', diff --git a/PHPCI/Languages/lang.fr.php b/PHPCI/Languages/lang.fr.php index 20410684..7d438d2f 100644 --- a/PHPCI/Languages/lang.fr.php +++ b/PHPCI/Languages/lang.fr.php @@ -298,9 +298,9 @@ PHPCI', Merci de corriger les erreurs ci-dessus avant de continuer.', 'must_be_valid_email' => 'Doit être une adresse email valide.', 'must_be_valid_url' => 'Doit être une URL valide.', - 'enter_name' => 'Nom de l\'admin :', - 'enter_email' => 'Email de l\'admin :', - 'enter_password' => 'Mot de passe de l\'admin :', + 'enter_name' => 'Nom de l\'admin: ', + 'enter_email' => 'Email de l\'admin: ', + 'enter_password' => 'Mot de passe de l\'admin: ', 'enter_phpci_url' => 'Votre URL vers PHPCI (par exemple "http://phpci.local"): ', 'enter_db_host' => 'Merci d\'entrer le nom d\'hôte MySQL [localhost]: ', diff --git a/PHPCI/Languages/lang.it.php b/PHPCI/Languages/lang.it.php index 46ca14fd..ea023454 100644 --- a/PHPCI/Languages/lang.it.php +++ b/PHPCI/Languages/lang.it.php @@ -300,9 +300,9 @@ PHPCI', Per favore controlla gli errori riportati prima di proseguire.', 'must_be_valid_email' => 'Deve essere un indirizzo email valido.', 'must_be_valid_url' => 'Deve essere un URL valido.', - 'enter_name' => 'Nome dell\'amministratore:', - 'enter_email' => 'Email dell\'amministratore:', - 'enter_password' => 'Password dell\'amministratore:', + 'enter_name' => 'Nome dell\'amministratore: ', + 'enter_email' => 'Email dell\'amministratore: ', + 'enter_password' => 'Password dell\'amministratore: ', 'enter_phpci_url' => 'L\'URL di PHPCI ("http://phpci.locale" ad esempio): ', 'enter_db_host' => 'Per favore inserisci l\'host MySQL [localhost]: ', diff --git a/PHPCI/Languages/lang.nl.php b/PHPCI/Languages/lang.nl.php index 83d5e317..f118e5b3 100644 --- a/PHPCI/Languages/lang.nl.php +++ b/PHPCI/Languages/lang.nl.php @@ -298,15 +298,15 @@ keer je composer update uitvoert.', Gelieve de fouten na te kijken vooraleer verder te gaan.', 'must_be_valid_email' => 'Moet een geldig e-mailadres zijn.', 'must_be_valid_url' => 'Moet een geldige URL zijn.', - 'enter_name' => 'Administrator naam:', - 'enter_email' => 'Administrator e-mailadres:', - 'enter_password' => 'Administrator wachtwoord:', - 'enter_phpci_url' => 'Je PHPCI URL (bijvoorbeeld "http://phpci.local")', + 'enter_name' => 'Administrator naam: ', + 'enter_email' => 'Administrator e-mailadres: ', + 'enter_password' => 'Administrator wachtwoord: ', + 'enter_phpci_url' => 'Je PHPCI URL (bijvoorbeeld "http://phpci.local"): ', - 'enter_db_host' => 'Vul je MySQL host in [localhost]:', - 'enter_db_name' => 'Vul je MySQL databasenaam in [phpci]:', - 'enter_db_user' => 'Vul je MySQL gebruikersnaam in [phpci]:', - 'enter_db_pass' => 'Vul je MySQL watchtwoord in:', + 'enter_db_host' => 'Vul je MySQL host in [localhost]: ', + 'enter_db_name' => 'Vul je MySQL databasenaam in [phpci]: ', + 'enter_db_user' => 'Vul je MySQL gebruikersnaam in [phpci]: ', + 'enter_db_pass' => 'Vul je MySQL watchtwoord in: ', 'could_not_connect' => 'PHPCI kon met deze gegevens geen verbinding maken met MySQL. Gelieve opnieuw te proberen.', 'setting_up_db' => 'Database wordt aangemaakt...', 'user_created' => 'Gebruikersprofiel aangemaakt!', diff --git a/PHPCI/Languages/lang.pl.php b/PHPCI/Languages/lang.pl.php index 9d6de6c8..c3291d1d 100644 --- a/PHPCI/Languages/lang.pl.php +++ b/PHPCI/Languages/lang.pl.php @@ -299,15 +299,15 @@ wywołaniu polecenia composer update.', Przejrzyj powyższą listę błędów przed kontynuowaniem.', 'must_be_valid_email' => 'Poprawny adres email jest wymagany.', 'must_be_valid_url' => 'Poprawny URL jest wymagany.', - 'enter_name' => 'Imię Admina:', - 'enter_email' => 'Email Admina:', - 'enter_password' => 'Hasło Admina:', - 'enter_phpci_url' => 'URL PHPCI (na przykład "http://phpci.local"):', + 'enter_name' => 'Imię Admina: ', + 'enter_email' => 'Email Admina: ', + 'enter_password' => 'Hasło Admina: ', + 'enter_phpci_url' => 'URL PHPCI (na przykład "http://phpci.local"): ', - 'enter_db_host' => 'Wpisz hosta MySQL [host lokalny]:', - 'enter_db_name' => 'Wpisz nazwę bazy danych MySQL [phpci]:', - 'enter_db_user' => 'Wpisz nazwę użytkownika MySQL [phpci]:', - 'enter_db_pass' => 'Wpisz hasło MySQL:', + 'enter_db_host' => 'Wpisz hosta MySQL [host lokalny]: ', + 'enter_db_name' => 'Wpisz nazwę bazy danych MySQL [phpci]: ', + 'enter_db_user' => 'Wpisz nazwę użytkownika MySQL [phpci]: ', + 'enter_db_pass' => 'Wpisz hasło MySQL: ', 'could_not_connect' => 'Z podanymi ustawieniami PHPCI nie udało się połączyć z MySQL. Spróbuj ponownie.', 'setting_up_db' => 'Ustawianie Twojej bazy danych...', 'user_created' => 'Utworzono konto użytkownika!', diff --git a/PHPCI/Languages/lang.ru.php b/PHPCI/Languages/lang.ru.php index 2f9ab0ee..422f17e1 100644 --- a/PHPCI/Languages/lang.ru.php +++ b/PHPCI/Languages/lang.ru.php @@ -294,9 +294,9 @@ PHPCI', Пожалуйста, просмотрите возникшие ошибки перед тем, как продолжить.', 'must_be_valid_email' => 'Должен быть корректным email-адресом.', 'must_be_valid_url' => 'Должен быть корректным URL-адресом.', - 'enter_name' => 'Имя администратора:', - 'enter_email' => 'Email администратора:', - 'enter_password' => 'Пароль администратора:', + 'enter_name' => 'Имя администратора: ', + 'enter_email' => 'Email администратора: ', + 'enter_password' => 'Пароль администратора: ', 'enter_phpci_url' => 'URL-адрес вашего PHPCI (например: "http://phpci.local"): ', 'enter_db_host' => 'Пожалуйста, введите хост MySQL [localhost]: ', diff --git a/PHPCI/Languages/lang.uk.php b/PHPCI/Languages/lang.uk.php index 7e4a2191..118fae9e 100644 --- a/PHPCI/Languages/lang.uk.php +++ b/PHPCI/Languages/lang.uk.php @@ -298,15 +298,15 @@ PHPCI', Будь ласка, продивіться наявні помилки перед тим, як продовжити.', 'must_be_valid_email' => 'Повинно бути коректною email адресою.', 'must_be_valid_url' => 'Повинно бути коректним URL.', - 'enter_name' => 'Ім’я адміністратора:', - 'enter_email' => 'Email адміністратора:', - 'enter_password' => 'Пароль адміністратора:', - 'enter_phpci_url' => 'URL адреса вашого PHPCI (наприклад, "http://phpci.local"):', + 'enter_name' => 'Ім’я адміністратора: ', + 'enter_email' => 'Email адміністратора: ', + 'enter_password' => 'Пароль адміністратора: ', + 'enter_phpci_url' => 'URL адреса вашого PHPCI (наприклад, "http://phpci.local"): ', - 'enter_db_host' => 'Будь ласка, введіть хост MySQL [localhost]:', - 'enter_db_name' => 'Будь ласка, введить ім’я бази даних MySQL [phpci]:', - 'enter_db_user' => 'Будь ласка, введить ім’я користувача MySQL [phpci]:', - 'enter_db_pass' => 'Будь ласка, введить ваш пароль MySQL:', + 'enter_db_host' => 'Будь ласка, введіть хост MySQL [localhost]: ', + 'enter_db_name' => 'Будь ласка, введить ім’я бази даних MySQL [phpci]: ', + 'enter_db_user' => 'Будь ласка, введить ім’я користувача MySQL [phpci]: ', + 'enter_db_pass' => 'Будь ласка, введить ваш пароль MySQL: ', 'could_not_connect' => 'PHPCI не може підключитися до MySQL із наданими параметрами. Будь ласка, спробуйте ще раз.', 'setting_up_db' => 'Налаштування вашої бази даних...', 'user_created' => 'Аккаунт користувача створено!', From 8a96ec85517caac25864a03306b1efb9d027a882 Mon Sep 17 00:00:00 2001 From: Mark Clements Date: Mon, 2 Mar 2015 22:49:22 +0000 Subject: [PATCH 149/329] Fixed the install script, which bails-out with an error if the PHPCI path contains spaces. This occurs commonly on Windows, but from my reading of the code it would also be a problem on other platforms if spaces were present (though this is less likely, due to different naming conventions). It has been fixed by using escapeshellarg() on both of the paths used in the command. Fixes #698, which I've just noticed has a similar solution suggested in one of the comments, but was closed without anyone actually implementing it. --- PHPCI/Command/InstallCommand.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PHPCI/Command/InstallCommand.php b/PHPCI/Command/InstallCommand.php index 04e0f2dd..a50eefec 100644 --- a/PHPCI/Command/InstallCommand.php +++ b/PHPCI/Command/InstallCommand.php @@ -323,7 +323,9 @@ class InstallCommand extends Command { $output->write(Lang::get('setting_up_db')); - shell_exec(PHPCI_DIR . 'vendor/bin/phinx migrate -c "' . PHPCI_DIR . 'phinx.php"'); + $phinxBinary = escapeshellarg(PHPCI_DIR . 'vendor/bin/phinx'); + $phinxScript = escapeshellarg(PHPCI_DIR . 'phinx.php'); + shell_exec($phinxBinary . ' migrate -c ' . $phinxScript); $output->writeln(''.Lang::get('ok').''); } From 610d0991a86e6c65bfb9276153070a1e117633b8 Mon Sep 17 00:00:00 2001 From: Nathan Jovin Date: Thu, 5 Mar 2015 00:12:42 -0800 Subject: [PATCH 150/329] Fix issue #840 Technical Debt not storing data nor displaying results in table --- PHPCI/Plugin/TechnicalDebt.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PHPCI/Plugin/TechnicalDebt.php b/PHPCI/Plugin/TechnicalDebt.php index afe6c920..4d38ec59 100755 --- a/PHPCI/Plugin/TechnicalDebt.php +++ b/PHPCI/Plugin/TechnicalDebt.php @@ -207,5 +207,7 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin } } } + + return array( $errorCount, $data ); } } From 542d2a35457cc2ebfd12771eeeac875256b9166a Mon Sep 17 00:00:00 2001 From: Thomas Frei Date: Fri, 6 Mar 2015 17:44:22 +0100 Subject: [PATCH 151/329] Remove short array syntax to keep backwards compatibility with php5.3 --- PHPCI/Application.php | 2 +- PHPCI/Controller/SettingsController.php | 6 +++--- PHPCI/Plugin/TechnicalDebt.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/PHPCI/Application.php b/PHPCI/Application.php index 8582fc41..8dcdf76d 100644 --- a/PHPCI/Application.php +++ b/PHPCI/Application.php @@ -51,7 +51,7 @@ class Application extends b8\Application return false; }; - $skipAuth = [$this, 'shouldSkipAuth']; + $skipAuth = array($this, 'shouldSkipAuth'); // Handler for the route we're about to register, checks for a valid session where necessary: $routeHandler = function (&$route, Response &$response) use (&$request, $validateSession, $skipAuth) { diff --git a/PHPCI/Controller/SettingsController.php b/PHPCI/Controller/SettingsController.php index a13c9fd4..aaaa637f 100644 --- a/PHPCI/Controller/SettingsController.php +++ b/PHPCI/Controller/SettingsController.php @@ -351,7 +351,7 @@ class SettingsController extends Controller $form->addField($field); $field = new Form\Element\Select('smtp_encryption'); - $field->setOptions(['' => Lang::get('none'), 'tls' => Lang::get('tls'), 'ssl' => Lang::get('ssl')]); + $field->setOptions(array('' => Lang::get('none'), 'tls' => Lang::get('tls'), 'ssl' => Lang::get('ssl'))); $field->setRequired(false); $field->setLabel(Lang::get('use_smtp_encryption')); $field->setContainerClass('form-group'); @@ -406,13 +406,13 @@ class SettingsController extends Controller $field->setLabel(Lang::get('failed_after')); $field->setClass('form-control'); $field->setContainerClass('form-group'); - $field->setOptions([ + $field->setOptions(array( 300 => Lang::get('5_mins'), 900 => Lang::get('15_mins'), 1800 => Lang::get('30_mins'), 3600 => Lang::get('1_hour'), 10800 => Lang::get('3_hours'), - ]); + )); $field->setValue(1800); $form->addField($field); diff --git a/PHPCI/Plugin/TechnicalDebt.php b/PHPCI/Plugin/TechnicalDebt.php index 4d38ec59..eca05213 100755 --- a/PHPCI/Plugin/TechnicalDebt.php +++ b/PHPCI/Plugin/TechnicalDebt.php @@ -152,7 +152,7 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin { $dirIterator = new \RecursiveDirectoryIterator($this->directory); $iterator = new \RecursiveIteratorIterator($dirIterator, \RecursiveIteratorIterator::SELF_FIRST); - $files = []; + $files = array(); $ignores = $this->ignore; $ignores[] = 'phpci.yml'; @@ -207,7 +207,7 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin } } } - + return array( $errorCount, $data ); } } From a7b40ce176617156acc1348cba6fe19414cb4658 Mon Sep 17 00:00:00 2001 From: Leszek Date: Sat, 7 Mar 2015 13:26:12 +0000 Subject: [PATCH 152/329] archived --- PHPCI/Languages/lang.pl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/Languages/lang.pl.php b/PHPCI/Languages/lang.pl.php index 9d6de6c8..7b2d8cc7 100644 --- a/PHPCI/Languages/lang.pl.php +++ b/PHPCI/Languages/lang.pl.php @@ -114,7 +114,7 @@ od wybranego kodu źródłowego platformy hostingowej.', (jeśli nie możesz dodać pliku phpci.yml do repozytorium projektu)', 'default_branch' => 'Domyślna nazwa gałęzi', 'allow_public_status' => 'Włączyć status publiczny dla tego projektu?', - 'archived' => 'Archived', + 'archived' => 'W archiwum', 'save_project' => 'Zachowaj Projekt', 'error_mercurial' => 'URL repozytorium Mercurialnego powinno zaczynać się od http:// and https://', From 942127ffe6e8132b5d03123206ab049dab112d8f Mon Sep 17 00:00:00 2001 From: corpsee Date: Fri, 27 Feb 2015 15:56:05 +0600 Subject: [PATCH 153/329] Added default value in profile language select (current language) --- PHPCI/Controller/UserController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/PHPCI/Controller/UserController.php b/PHPCI/Controller/UserController.php index 8d09ecd0..19a7313f 100644 --- a/PHPCI/Controller/UserController.php +++ b/PHPCI/Controller/UserController.php @@ -123,6 +123,7 @@ class UserController extends Controller $lang->setLabel(Lang::get('language')); $lang->setRequired(true); $lang->setOptions(Lang::getLanguageOptions()); + $lang->setValue(Lang::getLanguage()); $form->addField($lang); $submit = new Form\Element\Submit(); From 00b88630fbb4762f3bd32cc856e2238abffba5b9 Mon Sep 17 00:00:00 2001 From: Adirelle Date: Thu, 26 Feb 2015 15:20:44 +0100 Subject: [PATCH 154/329] Display a green border in passing build notifications. --- PHPCI/View/Email/success.phtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/View/Email/success.phtml b/PHPCI/View/Email/success.phtml index a6dfccea..a3a604c5 100644 --- a/PHPCI/View/Email/success.phtml +++ b/PHPCI/View/Email/success.phtml @@ -1,4 +1,4 @@ -
    +
    getTitle(); ?> - Build #getId(); ?> From 4e8dc7c87b89f541a1753c2d107f5a039fb0661a Mon Sep 17 00:00:00 2001 From: Igor Timoshenko Date: Wed, 25 Feb 2015 16:05:30 +0000 Subject: [PATCH 155/329] Fixed typos in Ukrainian language --- PHPCI/Languages/lang.uk.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/PHPCI/Languages/lang.uk.php b/PHPCI/Languages/lang.uk.php index 7e4a2191..f7ac8627 100644 --- a/PHPCI/Languages/lang.uk.php +++ b/PHPCI/Languages/lang.uk.php @@ -8,7 +8,7 @@ */ $strings = array( - 'language_name' => 'Український', + 'language_name' => 'Українська', 'language' => 'Мова', // Log in: @@ -20,10 +20,10 @@ $strings = array( і вам буде надіслано листа із посиланням на скидання паролю.', 'reset_email_address' => 'Введіть свою email адресу:', 'reset_send_email' => 'Скидання пароля', - 'reset_enter_password' => 'Введіть будь-ласка новий пароль', + 'reset_enter_password' => 'Введіть, будь ласка, новий пароль', 'reset_new_password' => 'Новий пароль:', 'reset_change_password' => 'Змінити пароль', - 'reset_no_user_exists' => 'Не існує користувача з такою email адресою, будь-ласка повторіть знову.', + 'reset_no_user_exists' => 'Не існує користувача з такою email адресою, будь ласка, повторіть знову.', 'reset_email_body' => 'Привіт, %s, Ви отримали цей лист, тому що ви або хтось інший запросили скидання пароля в PHPCI. @@ -62,7 +62,7 @@ PHPCI', 'manage_users' => 'Управління користувачами', 'plugins' => 'Плагіни', 'view' => 'Переглянути', - 'build_now' => 'Збірати', + 'build_now' => 'Зібрати', 'edit_project' => 'Редагувати проект', 'delete_project' => 'Видалити проект', @@ -186,7 +186,7 @@ PHPCI', 'phpunit' => 'PHP Unit', 'file' => 'Файл', - 'line' => 'Строка', + 'line' => 'Рядок', 'class' => 'Клас', 'method' => 'Метод', 'message' => 'Повідомлення', From e75ffe0b763d152438c493ee11341c3c360e45c2 Mon Sep 17 00:00:00 2001 From: corpsee Date: Tue, 10 Mar 2015 15:44:33 +0600 Subject: [PATCH 156/329] Fixed 'date' nl lang string --- PHPCI/Languages/lang.nl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/Languages/lang.nl.php b/PHPCI/Languages/lang.nl.php index d62cd4e5..a4c95b6d 100644 --- a/PHPCI/Languages/lang.nl.php +++ b/PHPCI/Languages/lang.nl.php @@ -127,7 +127,7 @@ van je gekozen source code hosting platform', 'all_branches' => 'Alle brances', 'builds' => 'Builds', 'id' => 'ID', - 'date' => 'Date', + 'date' => 'Datum', 'project' => 'Project', 'commit' => 'Commit', 'branch' => 'Branch', From 33fc50a0b55b69289b343560eabf32150600d525 Mon Sep 17 00:00:00 2001 From: Gustavo Novaro Date: Tue, 10 Mar 2015 15:45:36 -0300 Subject: [PATCH 157/329] Remove blank style tag in header Remove the tag From a188afb0da25503d1800e1959da4d5f75504c273 Mon Sep 17 00:00:00 2001 From: vsguts Date: Thu, 5 Mar 2015 21:25:11 +0300 Subject: [PATCH 158/329] Fixing symlink removal. Closes #854 --- PHPCI/Builder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/Builder.php b/PHPCI/Builder.php index d911e04b..619c0b38 100644 --- a/PHPCI/Builder.php +++ b/PHPCI/Builder.php @@ -223,7 +223,7 @@ class Builder implements LoggerAwareInterface if (IS_WIN) { $cmd = 'rmdir /S /Q "%s"'; } - $this->executeCommand($cmd, $this->buildPath); + $this->executeCommand($cmd, rtrim($this->buildPath, '/')); } catch (\Exception $ex) { $this->build->setStatus(Build::STATUS_FAILED); $this->buildLogger->logFailure(Lang::get('exception') . $ex->getMessage()); From 5f2de9a6798ffee196345da183b5f16573aab101 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Tue, 10 Mar 2015 20:20:54 +0000 Subject: [PATCH 159/329] Update to only build the latest commit from a Github pull request webhook. --- PHPCI/Controller/WebhookController.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/PHPCI/Controller/WebhookController.php b/PHPCI/Controller/WebhookController.php index 619eec16..7e38d60a 100644 --- a/PHPCI/Controller/WebhookController.php +++ b/PHPCI/Controller/WebhookController.php @@ -232,6 +232,11 @@ class WebhookController extends \PHPCI\Controller } foreach ($response['body'] as $commit) { + // Skip all but the current HEAD commit ID: + if ($commit['sha'] != $payload['head']['sha']) { + continue; + } + $branch = str_replace('refs/heads/', '', $payload['pull_request']['base']['ref']); $committer = $commit['commit']['author']['email']; $message = $commit['commit']['message']; From ecc92b5f3eed0e2ea5bc36cf79add5086a42a9f1 Mon Sep 17 00:00:00 2001 From: Dan Cryer Date: Wed, 11 Mar 2015 07:48:22 +0000 Subject: [PATCH 160/329] Fixing pull request builds. --- PHPCI/Controller/WebhookController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/Controller/WebhookController.php b/PHPCI/Controller/WebhookController.php index 7e38d60a..78824ddb 100644 --- a/PHPCI/Controller/WebhookController.php +++ b/PHPCI/Controller/WebhookController.php @@ -233,7 +233,7 @@ class WebhookController extends \PHPCI\Controller foreach ($response['body'] as $commit) { // Skip all but the current HEAD commit ID: - if ($commit['sha'] != $payload['head']['sha']) { + if ($commit['sha'] != $payload['pull_request']['head']['sha']) { continue; } From 70f0d2516f7c3155a0c04e03179176b844f5005d Mon Sep 17 00:00:00 2001 From: Stephen Ball Date: Fri, 13 Mar 2015 13:31:38 +0000 Subject: [PATCH 161/329] Removed log output so that it matches the other plugins which don't pollute the build log, and to prevent issues with the log output not being escaped --- PHPCI/Plugin/TechnicalDebt.php | 1 - 1 file changed, 1 deletion(-) diff --git a/PHPCI/Plugin/TechnicalDebt.php b/PHPCI/Plugin/TechnicalDebt.php index eca05213..6fdc81a7 100755 --- a/PHPCI/Plugin/TechnicalDebt.php +++ b/PHPCI/Plugin/TechnicalDebt.php @@ -193,7 +193,6 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin $content = trim($allLines[$lineNumber - 1]); $errorCount++; - $this->phpci->log("Found $search on line $lineNumber of $file:\n$content"); $fileName = str_replace($this->directory, '', $file); $data[] = array( From 9ad0e90fa19dc169bda65e1280c6aee41d223c8d Mon Sep 17 00:00:00 2001 From: Stephen Ball Date: Mon, 16 Mar 2015 11:05:33 +0000 Subject: [PATCH 162/329] Preventing the plugin failing due to an undefined variable --- PHPCI/Plugin/Wipe.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PHPCI/Plugin/Wipe.php b/PHPCI/Plugin/Wipe.php index 1a71e293..86e543b5 100644 --- a/PHPCI/Plugin/Wipe.php +++ b/PHPCI/Plugin/Wipe.php @@ -61,8 +61,8 @@ class Wipe implements \PHPCI\Plugin if (IS_WIN) { $cmd = 'rmdir /S /Q "%s"'; } - $success = $this->phpci->executeCommand($cmd, $this->directory); + return $this->phpci->executeCommand($cmd, $this->directory); } - return $success; + return true; } } From 52b2f87df22e4152dd2e3f5a288c0f4f091b0adf Mon Sep 17 00:00:00 2001 From: Stephen Ball Date: Mon, 16 Mar 2015 11:09:45 +0000 Subject: [PATCH 163/329] Parsing variables in the Wipe plugin --- PHPCI/Plugin/Wipe.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/Plugin/Wipe.php b/PHPCI/Plugin/Wipe.php index 1a71e293..7de4b975 100644 --- a/PHPCI/Plugin/Wipe.php +++ b/PHPCI/Plugin/Wipe.php @@ -43,7 +43,7 @@ class Wipe implements \PHPCI\Plugin $path = $phpci->buildPath; $this->phpci = $phpci; $this->build = $build; - $this->directory = isset($options['directory']) ? $options['directory'] : $path; + $this->directory = isset($options['directory']) ? $this->phpci->interpolate($options['directory']) : $path; } /** From fdaaa1ede4a34d54955adb5449c53ce4a2fd0711 Mon Sep 17 00:00:00 2001 From: Stephen Ball Date: Mon, 16 Mar 2015 11:10:03 +0000 Subject: [PATCH 164/329] Parsing variables in the code coverage output directory for PHPUnit --- PHPCI/Plugin/PhpUnit.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/Plugin/PhpUnit.php b/PHPCI/Plugin/PhpUnit.php index 70e0e74f..fc597f2d 100644 --- a/PHPCI/Plugin/PhpUnit.php +++ b/PHPCI/Plugin/PhpUnit.php @@ -133,7 +133,7 @@ class PhpUnit implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin } if (isset($options['coverage'])) { - $this->coverage = " --coverage-html {$options['coverage']} "; + $this->coverage = ' --coverage-html ' . $this->phpci->interpolate($options['coverage']) . ' '; } } From 1dd1af2443157e28860ab18708e2d9563960b1bb Mon Sep 17 00:00:00 2001 From: Mark Clements Date: Wed, 18 Mar 2015 09:47:47 +0000 Subject: [PATCH 165/329] Switching tabs to spaces as per style guide. No functional changes. --- PHPCI/Command/InstallCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PHPCI/Command/InstallCommand.php b/PHPCI/Command/InstallCommand.php index a50eefec..d10f34b7 100644 --- a/PHPCI/Command/InstallCommand.php +++ b/PHPCI/Command/InstallCommand.php @@ -323,8 +323,8 @@ class InstallCommand extends Command { $output->write(Lang::get('setting_up_db')); - $phinxBinary = escapeshellarg(PHPCI_DIR . 'vendor/bin/phinx'); - $phinxScript = escapeshellarg(PHPCI_DIR . 'phinx.php'); + $phinxBinary = escapeshellarg(PHPCI_DIR . 'vendor/bin/phinx'); + $phinxScript = escapeshellarg(PHPCI_DIR . 'phinx.php'); shell_exec($phinxBinary . ' migrate -c ' . $phinxScript); $output->writeln(''.Lang::get('ok').''); From 3467e77e7489112abef26c52537106b2c75b1559 Mon Sep 17 00:00:00 2001 From: Adirelle Date: Sun, 8 Mar 2015 17:57:16 +0100 Subject: [PATCH 166/329] Use a CSRF token on the login form to prevent CSRF attacks. --- PHPCI/Controller/SessionController.php | 46 +++++++++++++++++++++----- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/PHPCI/Controller/SessionController.php b/PHPCI/Controller/SessionController.php index 540b043f..8fa9d48b 100644 --- a/PHPCI/Controller/SessionController.php +++ b/PHPCI/Controller/SessionController.php @@ -43,15 +43,23 @@ class SessionController extends \PHPCI\Controller $isLoginFailure = false; if ($this->request->getMethod() == 'POST') { - $user = $this->userStore->getByEmail($this->getParam('email')); - if ($user && password_verify($this->getParam('password', ''), $user->getHash())) { - $_SESSION['phpci_user_id'] = $user->getId(); - $response = new b8\Http\Response\RedirectResponse(); - $response->setHeader('Location', $this->getLoginRedirect()); - return $response; - } else { + $token = $this->getParam('token'); + if ($token === null || $token !== $_SESSION['login_token']) { $isLoginFailure = true; + } else { + unset($_SESSION['login_token']); + + $user = $this->userStore->getByEmail($this->getParam('email')); + + if ($user && password_verify($this->getParam('password', ''), $user->getHash())) { + $_SESSION['phpci_user_id'] = $user->getId(); + $response = new b8\Http\Response\RedirectResponse(); + $response->setHeader('Location', $this->getLoginRedirect()); + return $response; + } else { + $isLoginFailure = true; + } } } @@ -78,9 +86,15 @@ class SessionController extends \PHPCI\Controller $pwd->setClass('btn-success'); $form->addField($pwd); + $tokenValue = $this->generateToken(); + $_SESSION['login_token'] = $tokenValue; + $token = new b8\Form\Element\Hidden('token'); + $token->setValue($tokenValue); + $form->addField($token); + $this->view->form = $form->render(); $this->view->failed = $isLoginFailure; - + return $this->view->render(); } @@ -180,4 +194,20 @@ class SessionController extends \PHPCI\Controller return $rtn; } + + /** Generate a random token. + * + * @return string + */ + protected function generateToken() + { + if(function_exists('openssl_random_pseudo_bytes')) { + return bin2hex(openssl_random_pseudo_bytes(16)); + } + + return sprintf("%04x", mt_rand(0, 0xFFFF)) + . sprintf("%04x", mt_rand(0, 0xFFFF)) + . sprintf("%04x", mt_rand(0, 0xFFFF)) + . sprintf("%04x", mt_rand(0, 0xFFFF)); + } } From f29ff197c62181d1d7918f7baa768d6d81f19fa4 Mon Sep 17 00:00:00 2001 From: Adirelle Date: Sun, 8 Mar 2015 17:53:27 +0100 Subject: [PATCH 167/329] Generate an new session identifier on successful login to prevent session fixation attacks. --- PHPCI/Controller/SessionController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/PHPCI/Controller/SessionController.php b/PHPCI/Controller/SessionController.php index 8fa9d48b..6ad2681e 100644 --- a/PHPCI/Controller/SessionController.php +++ b/PHPCI/Controller/SessionController.php @@ -53,6 +53,7 @@ class SessionController extends \PHPCI\Controller $user = $this->userStore->getByEmail($this->getParam('email')); if ($user && password_verify($this->getParam('password', ''), $user->getHash())) { + session_regenerate_id(true); $_SESSION['phpci_user_id'] = $user->getId(); $response = new b8\Http\Response\RedirectResponse(); $response->setHeader('Location', $this->getLoginRedirect()); From ea3b0c219afb0ba0e5bc09c4ab21d5990c593323 Mon Sep 17 00:00:00 2001 From: Adirelle Date: Tue, 10 Mar 2015 21:29:10 +0100 Subject: [PATCH 168/329] Code style fixed. --- PHPCI/Controller/SessionController.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PHPCI/Controller/SessionController.php b/PHPCI/Controller/SessionController.php index 6ad2681e..eb7001df 100644 --- a/PHPCI/Controller/SessionController.php +++ b/PHPCI/Controller/SessionController.php @@ -43,7 +43,6 @@ class SessionController extends \PHPCI\Controller $isLoginFailure = false; if ($this->request->getMethod() == 'POST') { - $token = $this->getParam('token'); if ($token === null || $token !== $_SESSION['login_token']) { $isLoginFailure = true; @@ -202,7 +201,7 @@ class SessionController extends \PHPCI\Controller */ protected function generateToken() { - if(function_exists('openssl_random_pseudo_bytes')) { + if (function_exists('openssl_random_pseudo_bytes')) { return bin2hex(openssl_random_pseudo_bytes(16)); } From d8df6cab4a6311df1e4803364c9af0e5f73e47ff Mon Sep 17 00:00:00 2001 From: LAHAXE Arnaud Date: Tue, 17 Mar 2015 15:49:52 +0100 Subject: [PATCH 169/329] Fix french typo mistake --- PHPCI/Languages/lang.fr.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/Languages/lang.fr.php b/PHPCI/Languages/lang.fr.php index 7d438d2f..da2e35b4 100644 --- a/PHPCI/Languages/lang.fr.php +++ b/PHPCI/Languages/lang.fr.php @@ -264,7 +264,7 @@ PHPCI', // Plugins 'cannot_update_composer' => 'PHPCI ne peut pas mettre à jour le fichier composer.json pour vous, il n\'est pas modifiable.', 'x_has_been_removed' => '%s a été supprimé.', - 'x_has_been_added' => '%s a été ajouté au fichier composer.json poru vous et il sera installé la prochaine fois + 'x_has_been_added' => '%s a été ajouté au fichier composer.json pour vous et il sera installé la prochaine fois que vous lancerez "composer update".', 'enabled_plugins' => 'Plugins activés', 'provided_by_package' => 'Fournis par le paquet', From 3a867eb9d56117ff9195cf34b60ff68621fa4893 Mon Sep 17 00:00:00 2001 From: corpsee Date: Mon, 16 Mar 2015 12:43:01 +0600 Subject: [PATCH 170/329] Fixed 'start' string for ru lang Fixed 'from' and 'to' strings for ru lang --- PHPCI/Languages/lang.ru.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PHPCI/Languages/lang.ru.php b/PHPCI/Languages/lang.ru.php index 422f17e1..ed5a8eeb 100644 --- a/PHPCI/Languages/lang.ru.php +++ b/PHPCI/Languages/lang.ru.php @@ -188,10 +188,10 @@ PHPCI', 'class' => 'Класс', 'method' => 'Метод', 'message' => 'Сообщение', - 'start' => 'Запуск', + 'start' => 'Начало', 'end' => 'Конец', - 'from' => 'От', - 'to' => 'До', + 'from' => 'Из', + 'to' => 'В', 'suite' => 'Комплект', 'test' => 'Тест', 'result' => 'Результат', From d804438a8785760987c6c8ae9811217d136fb246 Mon Sep 17 00:00:00 2001 From: Adirelle Date: Thu, 26 Feb 2015 15:57:45 +0100 Subject: [PATCH 171/329] Use sensiolabs/ansi-to-html to parse the build logs. Added an AnsiConverter helper. Use the AnsiConverter in the email and page templates that display the build log. Use a dedicated stylesheet for the ANSI converter. It can be customized. It can be inlined in the notifications. Do not use ProphecyTestCase when not needed. --- PHPCI/Controller/BuildController.php | 7 +-- PHPCI/Helper/AnsiConverter.php | 55 ++++++++++++++++++++++++ PHPCI/View/Email/success.phtml | 4 +- PHPCI/View/layout.phtml | 1 + Tests/PHPCI/Helper/AnsiConverterTest.php | 20 +++++++++ composer.json | 3 +- composer.lock | 47 +++++++++++++++++++- public/assets/css/ansi-colors.css | 32 ++++++++++++++ public/assets/js/build-plugins/log.js | 2 +- 9 files changed, 162 insertions(+), 9 deletions(-) create mode 100644 PHPCI/Helper/AnsiConverter.php create mode 100644 Tests/PHPCI/Helper/AnsiConverterTest.php create mode 100644 public/assets/css/ansi-colors.css diff --git a/PHPCI/Controller/BuildController.php b/PHPCI/Controller/BuildController.php index bf898d2d..e4660ba7 100644 --- a/PHPCI/Controller/BuildController.php +++ b/PHPCI/Controller/BuildController.php @@ -13,6 +13,7 @@ use b8; use b8\Exception\HttpException\NotFoundException; use b8\Http\Response\JsonResponse; use PHPCI\BuildFactory; +use PHPCI\Helper\AnsiConverter; use PHPCI\Helper\Lang; use PHPCI\Model\Build; use PHPCI\Model\Project; @@ -198,11 +199,7 @@ class BuildController extends \PHPCI\Controller */ protected function cleanLog($log) { - $log = str_replace('[0;32m', '', $log); - $log = str_replace('[0;31m', '', $log); - $log = str_replace('[0m', '', $log); - - return $log; + return AnsiConverter::convert($log); } /** diff --git a/PHPCI/Helper/AnsiConverter.php b/PHPCI/Helper/AnsiConverter.php new file mode 100644 index 00000000..a5e42269 --- /dev/null +++ b/PHPCI/Helper/AnsiConverter.php @@ -0,0 +1,55 @@ +convert($text); + } + + /** + * Do not instanciate this class. + */ + private function __construct() + { + } +} diff --git a/PHPCI/View/Email/success.phtml b/PHPCI/View/Email/success.phtml index a3a604c5..342e3483 100644 --- a/PHPCI/View/Email/success.phtml +++ b/PHPCI/View/Email/success.phtml @@ -1,3 +1,5 @@ + +
    @@ -8,7 +10,7 @@

    Your commit getCommitId(); ?> genrate a successfull build in project getTitle(); ?>.

    getCommitMessage(); ?>

    -
    getLog(); ?>
    +
    getLog()); ?>

    You can review your commit and the build log.

    diff --git a/PHPCI/View/layout.phtml b/PHPCI/View/layout.phtml index 33290fb3..72ef27ff 100644 --- a/PHPCI/View/layout.phtml +++ b/PHPCI/View/layout.phtml @@ -13,6 +13,7 @@ + diff --git a/Tests/PHPCI/Helper/AnsiConverterTest.php b/Tests/PHPCI/Helper/AnsiConverterTest.php new file mode 100644 index 00000000..4d1e8522 --- /dev/null +++ b/Tests/PHPCI/Helper/AnsiConverterTest.php @@ -0,0 +1,20 @@ +This is red !'; + + $actualOutput = AnsiConverter::convert($input); + + $this->assertEquals($expectedOutput, $actualOutput); + } +} diff --git a/composer.json b/composer.json index 2eec5e30..6658bffd 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,8 @@ "psr/log": "~1.0", "monolog/monolog": "~1.6", "pimple/pimple": "~1.1", - "robmorgan/phinx": "~0.4" + "robmorgan/phinx": "~0.4", + "sensiolabs/ansi-to-html": "~1.1" }, "require-dev": { diff --git a/composer.lock b/composer.lock index c5abc102..56c28655 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "4ae188c6be1c1388de6271a3b0e0475d", + "hash": "5fc23800ea77b50b496d34f7aa5cf6b3", "packages": [ { "name": "block8/b8framework", @@ -315,6 +315,50 @@ ], "time": "2015-02-23 16:38:12" }, + { + "name": "sensiolabs/ansi-to-html", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/sensiolabs/ansi-to-html.git", + "reference": "92d2ef7ffba5418be060d8ba8adaf7223d741f93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sensiolabs/ansi-to-html/zipball/92d2ef7ffba5418be060d8ba8adaf7223d741f93", + "reference": "92d2ef7ffba5418be060d8ba8adaf7223d741f93", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "suggest": { + "twig/twig": "Provides nice templating features" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-0": { + "SensioLabs\\AnsiConverter": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "A library to convert a text with ANSI codes to HTML", + "time": "2014-08-01 14:02:39" + }, { "name": "swiftmailer/swiftmailer", "version": "v5.3.1", @@ -2030,6 +2074,7 @@ "minimum-stability": "stable", "stability-flags": [], "prefer-stable": false, + "prefer-lowest": false, "platform": { "php": ">=5.3.8", "ext-pdo": "*", diff --git a/public/assets/css/ansi-colors.css b/public/assets/css/ansi-colors.css new file mode 100644 index 00000000..c3011299 --- /dev/null +++ b/public/assets/css/ansi-colors.css @@ -0,0 +1,32 @@ +.ansi_color_fg_black { color: black } +.ansi_color_bg_black { background-color: black } +.ansi_color_fg_red { color: darkred } +.ansi_color_bg_red { background-color: darkred } +.ansi_color_fg_green { color: green } +.ansi_color_bg_green { background-color: green } +.ansi_color_fg_yellow { color: yellow } +.ansi_color_bg_yellow { background-color: yellow } +.ansi_color_fg_blue { color: blue } +.ansi_color_bg_blue { background-color: blue } +.ansi_color_fg_magenta { color: darkmagenta } +.ansi_color_bg_magenta { background-color: darkmagenta } +.ansi_color_fg_cyan { color: cyan } +.ansi_color_bg_cyan { background-color: cyan } +.ansi_color_fg_white { color: white } +.ansi_color_bg_white { background-color: white } +.ansi_color_fg_brblack { color: black } +.ansi_color_bg_brblack { background-color: black } +.ansi_color_fg_brred { color: red } +.ansi_color_bg_brred { background-color: red } +.ansi_color_fg_brgreen { color: lightgreen } +.ansi_color_bg_brgreen { background-color: lightgreen } +.ansi_color_fg_bryellow { color: lightyellow } +.ansi_color_bg_bryellow { background-color: lightyellow } +.ansi_color_fg_brblue { color: lightblue } +.ansi_color_bg_brblue { background-color: lightblue } +.ansi_color_fg_brmagenta { color: magenta } +.ansi_color_bg_brmagenta { background-color: magenta } +.ansi_color_fg_brcyan { color: lightcyan } +.ansi_color_bg_brcyan { background-color: lightcyan } +.ansi_color_fg_brwhite { color: white } +.ansi_color_bg_brwhite { background-color: white } diff --git a/public/assets/js/build-plugins/log.js b/public/assets/js/build-plugins/log.js index 8798e23c..44ebab2b 100644 --- a/public/assets/js/build-plugins/log.js +++ b/public/assets/js/build-plugins/log.js @@ -8,7 +8,7 @@ var logPlugin = ActiveBuild.UiPlugin.extend({ }, render: function() { - var container = $('
    ');
    +        var container = $('
    ');
             container.css({height: '300px', 'overflow-y': 'auto'});
             container.html(ActiveBuild.buildData.log);
     
    
    From 2c43cd1cace4dbb9c25142dedc977d7507134018 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?Nicolpla=C3=A1s?= 
    Date: Sun, 1 Mar 2015 19:56:16 -0200
    Subject: [PATCH 172/329] Add spanish laguage support
    
    ---
     PHPCI/Languages/lang.es.php | 385 ++++++++++++++++++++++++++++++++++++
     1 file changed, 385 insertions(+)
     create mode 100644 PHPCI/Languages/lang.es.php
    
    diff --git a/PHPCI/Languages/lang.es.php b/PHPCI/Languages/lang.es.php
    new file mode 100644
    index 00000000..7c9f5a62
    --- /dev/null
    +++ b/PHPCI/Languages/lang.es.php
    @@ -0,0 +1,385 @@
    + 'Español',
    +    'language' => 'Lenguaje',
    +
    +    // Log in:
    +    'log_in_to_phpci' => 'Ingresar a PHPCI',
    +    'login_error' => 'Email o contraseña incorrectos',
    +    'forgotten_password_link' => '¿Olvidaste tu contraseña?',
    +    'reset_emailed' => 'Te hemos enviado un email para reiniciar tu contraseña.',
    +    'reset_header' => '¡No te preocupes!
    Solo tienes que ingresar tu dirección de email + y te enviaremos por email un enlace para reiniciar tu contraseña.', + 'reset_email_address' => 'Ingresa tu dirección de email:', + 'reset_send_email' => 'Enviar enlace', + 'reset_enter_password' => 'Ingresa una nueva contraseña', + 'reset_new_password' => 'Nueva contraseña:', + 'reset_change_password' => 'Cambiar contraseña', + 'reset_no_user_exists' => 'No existe ningún usuario con ese email, por favor intenta nuevamente.', + 'reset_email_body' => 'Hola %s, + +Has recibido este correo porque tú, o alguien más, ha solicitado reiniciar la contraseña de PHPCI + +Si fuiste tú, por favor haz click en el siguiente enlace para reiniciar tu contraseña: %ssession/reset-password/%d/%s + +De lo contrario, por favor ignora este correo y ninguna acción será realizada. + +Gracias, + +PHPCI', + + 'reset_email_title' => 'Reiniciar contraseña de PHPCI para %s', + 'reset_invalid' => 'Pedido inválido.', + 'email_address' => 'Dirección de email', + 'password' => 'Contraseña', + 'log_in' => 'Ingresar', + + + // Top Nav + 'toggle_navigation' => 'Activar navegación', + 'n_builds_pending' => '%d builds pendientes', + 'n_builds_running' => '%d builds ejecutándose', + 'edit_profile' => 'Editar Perfil', + 'sign_out' => 'Cerrar Sesión', + 'branch_x' => 'Rama: %s', + 'created_x' => 'Creada el: %s', + 'started_x' => 'Comenzó: %s', + + // Sidebar + 'hello_name' => 'Hola, %s', + 'dashboard' => 'Escritorio', + 'admin_options' => 'Opciones de Admin.', + 'add_project' => 'Agregar Proyecto', + 'settings' => 'Configuración', + 'manage_users' => 'Administrar Usuarios', + 'plugins' => 'Plugins', + 'view' => 'Vista', + 'build_now' => 'Ejecutar Build', + 'edit_project' => 'Editar Proyecto', + 'delete_project' => 'Eliminar Proyecto', + + // Project Summary: + 'no_builds_yet' => '¡No existen builds aún!', + 'x_of_x_failed' => '%d de los últimos %d builds fallaron.', + 'x_of_x_failed_short' => '%d / %d fallaron.', + 'last_successful_build' => ' El último build exitoso fue %s.', + 'never_built_successfully' => ' Este proyecto nunca tuvo un build exitoso.', + 'all_builds_passed' => 'Todos los últimos %d builds pasaron.', + 'all_builds_passed_short' => '%d / %d pasaron.', + 'last_failed_build' => ' El último build en fallar fue %s.', + 'never_failed_build' => ' Este proyecto no tiene ningún build fallido.', + 'view_project' => 'Ver Proyecto', + + // Timeline: + 'latest_builds' => 'Últimos builds', + 'pending' => 'Pediente', + 'running' => 'Ejecutando', + 'success' => 'Éxito', + 'successful' => 'Exitoso', + 'failed' => 'Falló', + 'manual_build' => 'Build Manual', + + // Add/Edit Project: + 'new_project' => 'Nuevo Proyecto', + 'project_x_not_found' => 'El Proyecto con ID %d no existe.', + 'project_details' => 'Detalles del Proyecto', + 'public_key_help' => 'Para facilitarte, hemos generado un par de llaves SSH para que uses en este proyecto. + Para usarlo, sólo agrega la siguiente llave pública a la sección de "deploy keys" + de tu plataforma de hosting de versionado de código.', + 'select_repository_type' => 'Selecciona tipo de repositorio...', + 'github' => 'GitHub', + 'bitbucket' => 'Bitbucket', + 'gitlab' => 'GitLab', + 'remote' => 'URL Remota', + 'local' => 'Path local', + 'hg' => 'Mercurial', + 'svn' => 'Subversion', + + 'where_hosted' => '¿Dónde está alojado tu proyecto?', + 'choose_github' => 'Selecciona un repositorio de GitHub:', + + 'repo_name' => 'Nombre del repositorio / URL (Remoto) o Ruta (Local)', + 'project_title' => 'Titulo del proyecto', + 'project_private_key' => 'Clave privada a usar para acceder al repositorio + (dejar en blanco para remotos locales o anónimos)', + 'build_config' => 'Configuración PHPCI para builds del proyecto + (en caso que no puedas agregar el archivo phpci.yml al repositorio)', + 'default_branch' => 'Nombre de la rama por defecto', + 'allow_public_status' => '¿Activar página pública con el estado del proyecto?', + 'save_project' => 'Guardar Proyecto', + + 'error_mercurial' => 'La URL del repositorio de Mercurial debe comenzar con http:// or https://', + 'error_remote' => 'La URL del repositorio debe comenzar con git://, http:// or https://', + 'error_gitlab' => 'El nombre del repositorio de GitLab debe tener el formato "user@domain.tld:owner/repo.git"', + 'error_github' => 'El nombre del repositorio debe tener el formato "owner/repo"', + 'error_bitbucket' => 'El nombre del repo debe tener el formato "owner/repo"', + 'error_path' => 'La ruta especificada no existe.', + + // View Project: + 'all_branches' => 'Todas las ramas', + 'builds' => 'Builds', + 'id' => 'ID', + 'project' => 'Proyecto', + 'commit' => 'Commit', + 'branch' => 'Rama', + 'status' => 'Estado', + 'prev_link' => '« Anterior', + 'next_link' => 'Siguiente »', + 'public_key' => 'Llave pública', + 'delete_build' => 'Eliminar Build', + + 'webhooks' => 'Webhooks', + 'webhooks_help_github' => 'Para compilar automáticamente este proyecto cada vez que se realiza un commit, agreagar la siguiente URL + como un nuevo "webhook" en la sección Webhooks + and Services de tu repositorio en GitHub.', + + 'webhooks_help_gitlab' => 'Para compilar automáticamente este proyecto, cada vez que se realiza un commit, agreagar la siguiente URL + como una "WebHook URL" en la sección "web hooks" de tu repositorio en GitLab.', + + 'webhooks_help_bitbucket' => 'Para compilar automáticamente este proyecto, cada vez que se realiza un commit, agreagar la siguiente URL + como un servicio "POST" en la sección + + Services de tu repositorio en Bitbucket.', + + // View Build + 'build_x_not_found' => 'El build con ID %d no existe.', + 'build_n' => 'Build %d', + 'rebuild_now' => 'Rebuild Ahora', + + + 'committed_by_x' => 'Commit hecho por %s', + 'commit_id_x' => 'Commit: %s', + + 'chart_display' => 'Este gráfico será mostrado una vez que el build se haya completado.', + + 'build' => 'Build', + 'lines' => 'Líneas', + 'comment_lines' => 'Líneas de comentario', + 'noncomment_lines' => 'Líneas no comentario', + 'logical_lines' => 'Líneas lógicas', + 'lines_of_code' => 'Líneas de código', + 'build_log' => 'Log', + 'quality_trend' => 'Tendencia de calidad', + 'codeception_errors' => 'Errores de Codeception', + 'phpmd_warnings' => 'PHPMD Warnings', + 'phpcs_warnings' => 'PHPCS Warnings', + 'phpcs_errors' => 'PHPCS Errors', + 'phplint_errors' => 'Lint Errors', + 'phpunit_errors' => 'PHPUnit Errors', + 'phpdoccheck_warnings' => 'Docblocks faltantes', + 'issues' => 'Incidencias', + + 'codeception' => 'Codeception', + 'phpcpd' => 'PHP Copy/Paste Detector', + 'phpcs' => 'PHP Code Sniffer', + 'phpdoccheck' => 'Missing Docblocks', + 'phpmd' => 'PHP Mess Detector', + 'phpspec' => 'PHP Spec', + 'phpunit' => 'PHP Unit', + 'technical_debt' => 'Deuda Técnica', + 'behat' => 'Behat', + + 'file' => 'Archivo', + 'line' => 'Línea', + 'class' => 'Clase', + 'method' => 'Método', + 'message' => 'Mensaje', + 'start' => 'Inicio', + 'end' => 'Fin', + 'from' => 'De', + 'to' => 'Para', + 'suite' => 'Suite', + 'test' => 'Test', + 'result' => 'Resultado', + 'ok' => 'OK', + 'took_n_seconds' => 'Tomó %d segundos', + 'build_created' => 'Build Creado', + 'build_started' => 'Build Comenzado', + 'build_finished' => 'Build Terminado', + + // Users + 'name' => 'Nombre', + 'password_change' => 'Contraseña (dejar en blanco si no quiere cambiarla)', + 'save' => 'Guardar »', + 'update_your_details' => 'Actualizar los detalles', + 'your_details_updated' => 'Tus detalles han sido actualizados.', + 'add_user' => 'Agregar Usuario', + 'is_admin' => '¿Es Admin?', + 'yes' => 'Si', + 'no' => 'No', + 'edit' => 'Editar', + 'edit_user' => 'Editar Usuario', + 'delete_user' => 'Delete Usuario', + 'user_n_not_found' => 'Usuario con ID %d no existe.', + 'is_user_admin' => '¿Es un usuario administrador?', + 'save_user' => 'Guardar Usuario', + + // Settings: + 'settings_saved' => 'Tu configuración ha sido guardada.', + 'settings_check_perms' => 'Tu configuración no fue guardada, verificar los permisos del archivo config.yml.', + 'settings_cannot_write' => 'PHPCI no puede escribir en el archivo config.yml file, la configuración no será guardada correctamente + hasta no corregir esto.', + 'settings_github_linked' => 'Tu cuenta GitHub ha sido conectada.', + 'settings_github_not_linked' => 'No se pudo conectar a tu cuenta GitHub.', + 'build_settings' => 'Configuración del Build ', + 'github_application' => 'Aplicación GitHub', + 'github_sign_in' => 'Antes de comenzar a utilizar GitHub, tienes que ingresar y permitir + el acceso a tu cuenta a PHPCI.', + 'github_phpci_linked' => 'PHPCI ha sido conectado a tu cuenta GitHub.', + 'github_where_to_find' => 'Donde encontrar estos...', + 'github_where_help' => 'Si eres priopietario de la aplicaión que quieres usar, puedes encontrar esta información en + el área de configuración de aplicaciones.', + + 'email_settings' => 'Configuraciones de Email', + 'email_settings_help' => 'Para que PHPCI pueda enviar email con el status de los builds, + debes configurar las siguientes propiedades SMTP.', + + 'application_id' => 'ID de aplicación', + 'application_secret' => 'Application Secret', + + 'smtp_server' => 'Servidor SMTP', + 'smtp_port' => 'Puerto SMTP', + 'smtp_username' => 'Usuario SMTP', + 'smtp_password' => 'Contraseña SMTP', + 'from_email_address' => 'Dirección de email DE', + 'default_notification_address' => 'Dirección de correo de notificación por defecto', + 'use_smtp_encryption' => 'Usar encriptación SMTP?', + 'none' => 'None', + 'ssl' => 'SSL', + 'tls' => 'TLS', + + 'failed_after' => 'Considerar el build como fallido luego de ', + '5_mins' => '5 Minutos', + '15_mins' => '15 Minutos', + '30_mins' => '30 Minutos', + '1_hour' => '1 Hora', + '3_hours' => '3 Horas', + + // Plugins + 'cannot_update_composer' => 'PHPCI no puede actualizar composer.json porque no tiene permisos de escritura.', + 'x_has_been_removed' => '%s ha sido elimiando.', + 'x_has_been_added' => '%s ha sido agregado a composer.json y será instalado la próxima vez que ejecutes composer update.', + 'enabled_plugins' => 'Activar Plugins', + 'provided_by_package' => 'Provisto por Paquete', + 'installed_packages' => 'Paquetes Instalados', + 'suggested_packages' => 'Paquetes Sugeridos', + 'title' => 'Título', + 'description' => 'Descripción', + 'version' => 'Versión', + 'install' => 'Instalar »', + 'remove' => 'Eliminar »', + 'search_packagist_for_more' => 'Buscar más paquetes en Packagist', + 'search' => 'Buscar »', + + // Installer + 'installation_url' => 'URL de la instalación PHPCI', + 'db_host' => 'Host', + 'db_name' => 'Nombre de la base de datos', + 'db_user' => 'Usuario de la base de datos', + 'db_pass' => 'Clave de la base de datos', + 'admin_name' => 'Nombre del Admin', + 'admin_pass' => 'Clave del Admin', + 'admin_email' => 'Email de Admin', + 'config_path' => 'Ruta al archivo config', + 'install_phpci' => 'Instalar PHPCI', + 'welcome_to_phpci' => 'Bienvenido a PHPCI', + 'please_answer' => 'Por favor, responde las siguientes preguntas:', + 'phpci_php_req' => 'PHPCI requiere al menos PHP 5.3.8 para funcionar.', + 'extension_required' => 'Extensión requerida: %s', + 'function_required' => 'PHPCI debe poder invocar la función %s(). Está deshabilitada en php.ini?', + 'requirements_not_met' => 'PHPCI no pudo ser instalado, ya que no se cumplen todos los requerimientos. + Por favor, corrige los errores antes de continuar.', + 'must_be_valid_email' => 'Debe ser una dirección de correos válida.', + 'must_be_valid_url' => 'Debe ser una URL válida.', + 'enter_name' => 'Nombre del Admin:', + 'enter_email' => 'Email del Admin:', + 'enter_password' => 'Contraseña de Admin:', + 'enter_phpci_url' => 'La URL de PHPCI ("Por ejemplo: http://phpci.local"): ', + + 'enter_db_host' => 'Por favor, ingresa el servidor MySQL [localhost]: ', + 'enter_db_name' => 'Por favor, ingresa el nombre de la base de datos MySQL [phpci]: ', + 'enter_db_user' => 'Por favor, ingresa el usuario MySQL [phpci]: ', + 'enter_db_pass' => 'Por favor, ingresa la contraseña MySQL: ', + 'could_not_connect' => 'PHPCI no pudo conectarse a MySQL con los datos dados. Por favor, intenta nuevamente.', + 'setting_up_db' => 'Configurando base de datos... ', + 'user_created' => '¡Cuenta de usuario creada!', + 'failed_to_create' => 'PHPCI no pudo crear la cuenta de admin.', + 'config_exists' => 'El archivo config de PHPCI ya existe y no es vacío.', + 'update_instead' => 'Si está intentando actualizar PHPCI, por favor, utiliza phpci:update.', + + // Update + 'update_phpci' => 'Actuliza la base de datos para reflejar los modelos actualizados.', + 'updating_phpci' => 'Actualizando base de datos PHPCI: ', + 'not_installed' => 'PHPCI no está instalado.', + 'install_instead' => 'Por favor, instala PHPCI via phpci:install.', + + // Poll Command + 'poll_github' => 'Chequear en GitHub si se necesita comenzar un Build.', + 'no_token' => 'No se encontró ningún token GitHub', + 'finding_projects' => 'Buscando proyectos para chequear', + 'found_n_projects' => 'Se encontraron %d proyectos', + 'last_commit_is' => 'El último commit en GitHub para %s es %s', + 'adding_new_build' => 'Último commit es diferente a la base de datos, agregando nuevo build.', + 'finished_processing_builds' => 'Fin de procesamiento de builds.', + + // Create Admin + 'create_admin_user' => 'Crear un usuario Admin', + 'incorrect_format' => 'Formato incorrecto', + + // Run Command + 'run_all_pending' => 'Ejecutar todos los builds PHPCI pendientes.', + 'finding_builds' => 'Buscando builds a procesar', + 'found_n_builds' => 'Se encontraron %d builds', + 'skipping_build' => 'Saltando Build %d - Build del proyecto ya en ejecución.', + 'marked_as_failed' => 'Build %d falló debido a timeout.', + + // Builder + 'missing_phpci_yml' => 'Este proyecto no contiene el archivo phpci.yml o está vacío.', + 'build_success' => 'BUILD EXITOSO', + 'build_failed' => 'BUILD FALLIDO', + 'removing_build' => 'Eliminando Build.', + 'exception' => 'Excepción: ', + 'could_not_create_working' => 'Imposible crear copia de trabajo.', + 'working_copy_created' => 'Copia de trabajo creada: %s', + 'looking_for_binary' => 'Buscando binario: %s', + 'found_in_path' => 'Encontrado en %s: %s', + 'running_plugin' => 'EJECUTANDO PLUGIN: %s', + 'plugin_success' => 'PLUGIN: EXITO', + 'plugin_failed' => 'PLUGIN: FALLÓ', + 'plugin_missing' => 'No existe el plugin: %s', + 'tap_version' => 'TapParser únicamente soporta la verisón 13 de TAP', + 'tap_error' => 'Cadena de caracteres TAP inválida, el número de tests no coincide con la cuenta de tests declarada.', + + // Build Plugins: + 'no_tests_performed' => 'No se ejecutaron tests.', + 'could_not_find' => 'No se encontró %s', + 'no_campfire_settings' => 'No se especificaron parámetros de conexión para el plugin Campfire', + 'failed_to_wipe' => 'Imposible eliminar directorio existente %s antes de copiarlo', + 'passing_build' => 'Build Exitoso', + 'failing_build' => 'Build Fallido', + 'log_output' => 'Log de Salida: ', + 'n_emails_sent' => '%d emails enviados.', + 'n_emails_failed' => '%d emails no pudieron ser enviados.', + 'unable_to_set_env' => 'Imposible setear variable de entorno', + 'tag_created' => 'Tag creado por PHPCI: %s', + 'x_built_at_x' => 'Build de %PROJECT_TITLE% en %BUILD_URI%', + 'hipchat_settings' => 'Por favor, definir room y authToken para el plugin hipchat_notify', + 'irc_settings' => 'Debes configurar un servidor, room y apodo.', + 'invalid_command' => 'Comando inválido', + 'import_file_key' => 'Sentencia de importación debe contener una llave \'file\'', + 'cannot_open_import' => 'Imposible abrir archivo de importación SQL: %s', + 'unable_to_execute' => 'Imposible ejecutar archivo SQL', + 'phar_internal_error' => 'Error interno en plugin Phar', + 'build_file_missing' => 'El archivo de build especificado no existe.', + 'property_file_missing' => 'El archivo de propiedades especificado no existe.', + 'could_not_process_report' => 'Imposible procesar el reporte generado por la herramienta.', + 'shell_not_enabled' => 'El plugin shell no está habilitado. Por favor, habilitalo desde config.yml.' +); From 9d4116e3c95e782e127de4a24121af29480b1775 Mon Sep 17 00:00:00 2001 From: Adirelle Date: Tue, 3 Mar 2015 20:10:55 +0100 Subject: [PATCH 173/329] Reworked TapParser to be compliant and more robust. Added another test case from #571. Updated the output of TapParser::processTestLine. Broke TapParser::parse down in simpler methods. TapParser: ignore leading garbage and properly complain on missing TAP log. TapParser: detect and report duplicated TAP log. TapParser: got rid of the "test" and "suite" values. They are only available with PHPUnit. TapParser: append the message from yaml diagnostic to existing message. Reworked the dispaly of test results. PHPUnit plugin: pretty print test data. --- PHPCI/Languages/lang.da.php | 10 +- PHPCI/Languages/lang.de.php | 10 +- PHPCI/Languages/lang.el.php | 10 +- PHPCI/Languages/lang.en.php | 10 +- PHPCI/Languages/lang.fr.php | 10 +- PHPCI/Languages/lang.it.php | 10 +- PHPCI/Languages/lang.nl.php | 10 +- PHPCI/Languages/lang.pl.php | 10 +- PHPCI/Languages/lang.ru.php | 10 +- PHPCI/Languages/lang.uk.php | 10 +- PHPCI/Plugin/Util/TapParser.php | 249 ++++++++++++++++------ Tests/PHPCI/Plugin/Util/TapParserTest.php | 240 ++++++++++++++++++++- public/assets/css/AdminLTE-custom.css | 9 +- public/assets/js/build-plugins/phpunit.js | 94 ++++++-- 14 files changed, 587 insertions(+), 105 deletions(-) diff --git a/PHPCI/Languages/lang.da.php b/PHPCI/Languages/lang.da.php index 4e61c3db..883e52b0 100644 --- a/PHPCI/Languages/lang.da.php +++ b/PHPCI/Languages/lang.da.php @@ -194,14 +194,20 @@ Services sektionen under dit Bitbucket-repository.', 'end' => 'Slut', 'from' => 'Fra', 'to' => 'Til', - 'suite' => 'Suite', - 'test' => 'Test', 'result' => 'Resultat', 'ok' => 'OK', 'took_n_seconds' => 'Tog %d sekunder', 'build_created' => 'Build Oprettet', 'build_started' => 'Build Startet', 'build_finished' => 'Build Afsluttet', + 'test_message' => 'Message', + 'test_no_message' => 'No message', + 'test_success' => 'Succesfull: %d', + 'test_fail' => 'Failures: %d', + 'test_skipped' => 'Skipped: %d', + 'test_error' => 'Errors: %d', + 'test_todo' => 'Todos: %d', + 'test_total' => '%d test(s)', // Users 'name' => 'Navn', diff --git a/PHPCI/Languages/lang.de.php b/PHPCI/Languages/lang.de.php index a04e40e1..62504310 100644 --- a/PHPCI/Languages/lang.de.php +++ b/PHPCI/Languages/lang.de.php @@ -192,14 +192,20 @@ generiert. Um es zu verwenden, fügen Sie einfach den folgenden Public Key im Ab 'end' => 'Ende', 'from' => 'Von', 'to' => 'Bis', - 'suite' => 'Suite', - 'test' => 'Test', 'result' => 'Resultat', 'ok' => 'OK', 'took_n_seconds' => 'Benötigte %d Sekunden', 'build_created' => 'Build erstellt', 'build_started' => 'Build gestartet', 'build_finished' => 'Build abgeschlossen', + 'test_message' => 'Message', + 'test_no_message' => 'No message', + 'test_success' => 'Succesfull: %d', + 'test_fail' => 'Failures: %d', + 'test_skipped' => 'Skipped: %d', + 'test_error' => 'Errors: %d', + 'test_todo' => 'Todos: %d', + 'test_total' => '%d test(s)', // Users 'name' => 'Name', diff --git a/PHPCI/Languages/lang.el.php b/PHPCI/Languages/lang.el.php index 4b1e82ca..5afc6593 100644 --- a/PHPCI/Languages/lang.el.php +++ b/PHPCI/Languages/lang.el.php @@ -194,14 +194,20 @@ Services του Bitbucket αποθετηρίου σας.', 'end' => 'Τέλος', 'from' => 'Από', 'to' => 'Προς', - 'suite' => 'Σουίτα', - 'test' => 'Τέστ', 'result' => 'Αποτέλεσμα', 'ok' => 'ΟΚ', 'took_n_seconds' => 'Χρειάστηκαν %d δευτερόλεπτα', 'build_created' => 'Η κατασκευή δημιουργήθηκε', 'build_started' => 'Η κατασκευή άρχισε', 'build_finished' => 'Η κατασκευή ολοκληρώθηκε', + 'test_message' => 'Message', + 'test_no_message' => 'No message', + 'test_success' => 'Succesfull: %d', + 'test_fail' => 'Failures: %d', + 'test_skipped' => 'Skipped: %d', + 'test_error' => 'Errors: %d', + 'test_todo' => 'Todos: %d', + 'test_total' => '%d test(s)', // Users 'name' => 'Όνομα', diff --git a/PHPCI/Languages/lang.en.php b/PHPCI/Languages/lang.en.php index b23eff7b..393ef584 100644 --- a/PHPCI/Languages/lang.en.php +++ b/PHPCI/Languages/lang.en.php @@ -198,14 +198,20 @@ PHPCI', 'end' => 'End', 'from' => 'From', 'to' => 'To', - 'suite' => 'Suite', - 'test' => 'Test', 'result' => 'Result', 'ok' => 'OK', 'took_n_seconds' => 'Took %d seconds', 'build_created' => 'Build Created', 'build_started' => 'Build Started', 'build_finished' => 'Build Finished', + 'test_message' => 'Message', + 'test_no_message' => 'No message', + 'test_success' => 'Succesfull: %d', + 'test_fail' => 'Failures: %d', + 'test_skipped' => 'Skipped: %d', + 'test_error' => 'Errors: %d', + 'test_todo' => 'Todos: %d', + 'test_total' => '%d test(s)', // Users 'name' => 'Name', diff --git a/PHPCI/Languages/lang.fr.php b/PHPCI/Languages/lang.fr.php index 7be361fc..6965cb06 100644 --- a/PHPCI/Languages/lang.fr.php +++ b/PHPCI/Languages/lang.fr.php @@ -195,14 +195,20 @@ PHPCI', 'end' => 'Fin', 'from' => 'À partir de', 'to' => 'jusque', - 'suite' => 'Suite', - 'test' => 'Test', 'result' => 'Resultat', 'ok' => 'OK', 'took_n_seconds' => 'Exécuté en %d secondes', 'build_created' => 'Build créé', 'build_started' => 'Build démarré', 'build_finished' => 'Build terminé', + 'test_message' => 'Message', + 'test_no_message' => 'Pas de message', + 'test_success' => 'Réussi(s): %d', + 'test_fail' => 'Echec(s): %d', + 'test_skipped' => 'Passé(s): %d', + 'test_error' => 'Erreurs: %d', + 'test_todo' => 'Todos: %d', + 'test_total' => '%d test(s)', // Users 'name' => 'Nom', diff --git a/PHPCI/Languages/lang.it.php b/PHPCI/Languages/lang.it.php index a8a3e13c..db6b23fc 100644 --- a/PHPCI/Languages/lang.it.php +++ b/PHPCI/Languages/lang.it.php @@ -197,14 +197,20 @@ PHPCI', 'end' => 'Finisci', 'from' => 'Da', 'to' => 'A', - 'suite' => 'Suite', - 'test' => 'Test', 'result' => 'Risultati', 'ok' => 'OK', 'took_n_seconds' => 'Sono stati impiegati %d seconds', 'build_created' => 'Build Creata', 'build_started' => 'Build Avviata', 'build_finished' => 'Build Terminata', + 'test_message' => 'Message', + 'test_no_message' => 'No message', + 'test_success' => 'Succesfull: %d', + 'test_fail' => 'Failures: %d', + 'test_skipped' => 'Skipped: %d', + 'test_error' => 'Errors: %d', + 'test_todo' => 'Todos: %d', + 'test_total' => '%d test(s)', // Users 'name' => 'Nome', diff --git a/PHPCI/Languages/lang.nl.php b/PHPCI/Languages/lang.nl.php index 29e8b0f8..38e9466c 100644 --- a/PHPCI/Languages/lang.nl.php +++ b/PHPCI/Languages/lang.nl.php @@ -195,14 +195,20 @@ Services sectie van je Bitbucket repository toegevoegd worden.', 'end' => 'Einde', 'from' => 'Van', 'to' => 'Tot', - 'suite' => 'Suite', - 'test' => 'Test', 'result' => 'Resultaat', 'ok' => 'OK', 'took_n_seconds' => 'Duurde %d seconden', 'build_created' => 'Build aangemaakt', 'build_started' => 'Build gestart', 'build_finished' => 'Build beëindigd', + 'test_message' => 'Message', + 'test_no_message' => 'No message', + 'test_success' => 'Succesfull: %d', + 'test_fail' => 'Failures: %d', + 'test_skipped' => 'Skipped: %d', + 'test_error' => 'Errors: %d', + 'test_todo' => 'Todos: %d', + 'test_total' => '%d test(s)', // Users 'name' => 'Naam', diff --git a/PHPCI/Languages/lang.pl.php b/PHPCI/Languages/lang.pl.php index 816eb009..b4f8430c 100644 --- a/PHPCI/Languages/lang.pl.php +++ b/PHPCI/Languages/lang.pl.php @@ -198,14 +198,20 @@ Services repozytoria Bitbucket.', 'end' => 'Koniec', 'from' => 'Od', 'to' => 'Do', - 'suite' => 'Zestaw ', - 'test' => 'Test', 'result' => 'Wynik', 'ok' => 'OK', 'took_n_seconds' => 'Zajęło %d sekund', 'build_created' => 'Budowanie Stworzone', 'build_started' => 'Budowanie Rozpoczęte', 'build_finished' => 'Budowanie Zakończone', + 'test_message' => 'Message', + 'test_no_message' => 'No message', + 'test_success' => 'Succesfull: %d', + 'test_fail' => 'Failures: %d', + 'test_skipped' => 'Skipped: %d', + 'test_error' => 'Errors: %d', + 'test_todo' => 'Todos: %d', + 'test_total' => '%d test(s)', // Users 'name' => 'Nazwa', diff --git a/PHPCI/Languages/lang.ru.php b/PHPCI/Languages/lang.ru.php index 44ed1550..931c8910 100644 --- a/PHPCI/Languages/lang.ru.php +++ b/PHPCI/Languages/lang.ru.php @@ -193,14 +193,20 @@ PHPCI', 'end' => 'Конец', 'from' => 'Из', 'to' => 'В', - 'suite' => 'Комплект', - 'test' => 'Тест', 'result' => 'Результат', 'ok' => 'OK', 'took_n_seconds' => 'Заняло секунд: %d', 'build_created' => 'Сборка создана', 'build_started' => 'Сборка запущена', 'build_finished' => 'Сборка окончена', + 'test_message' => 'Message', + 'test_no_message' => 'No message', + 'test_success' => 'Succesfull: %d', + 'test_fail' => 'Failures: %d', + 'test_skipped' => 'Skipped: %d', + 'test_error' => 'Errors: %d', + 'test_todo' => 'Todos: %d', + 'test_total' => '%d test(s)', // Users 'name' => 'Имя', diff --git a/PHPCI/Languages/lang.uk.php b/PHPCI/Languages/lang.uk.php index 3e96459e..b080c65a 100644 --- a/PHPCI/Languages/lang.uk.php +++ b/PHPCI/Languages/lang.uk.php @@ -195,14 +195,20 @@ PHPCI', 'end' => 'Кінець', 'from' => 'Від', 'to' => 'До', - 'suite' => 'Комплект', - 'test' => 'Тест', 'result' => 'Результат', 'ok' => 'OK', 'took_n_seconds' => 'Зайняло %d секунд', 'build_created' => 'Збірка створена', 'build_started' => 'Збірка розпочата', 'build_finished' => 'Збірка завершена', + 'test_message' => 'Message', + 'test_no_message' => 'No message', + 'test_success' => 'Succesfull: %d', + 'test_fail' => 'Failures: %d', + 'test_skipped' => 'Skipped: %d', + 'test_error' => 'Errors: %d', + 'test_todo' => 'Todos: %d', + 'test_total' => '%d test(s)', // Users 'name' => 'Ім’я', diff --git a/PHPCI/Plugin/Util/TapParser.php b/PHPCI/Plugin/Util/TapParser.php index 57b92b55..18772a6a 100644 --- a/PHPCI/Plugin/Util/TapParser.php +++ b/PHPCI/Plugin/Util/TapParser.php @@ -2,7 +2,9 @@ namespace PHPCI\Plugin\Util; +use Exception; use PHPCI\Helper\Lang; +use Symfony\Component\Yaml\Yaml; /** * Processes TAP format strings into usable test result data. @@ -10,18 +12,41 @@ use PHPCI\Helper\Lang; */ class TapParser { - const TEST_COUNTS_PATTERN = '/([0-9]+)\.\.([0-9]+)/'; - const TEST_LINE_PATTERN = '/(ok|not ok)\s+[0-9]+\s+\-\s+([^\n]+)::([^\n]+)/'; - const TEST_MESSAGE_PATTERN = '/message\:\s+\'([^\']+)\'/'; - const TEST_COVERAGE_PATTERN = '/Generating code coverage report/'; - const TEST_SKIP_PATTERN = '/ok\s+[0-9]+\s+\-\s+#\s+SKIP/'; + const TEST_COUNTS_PATTERN = '/^\d+\.\.(\d+)/'; + const TEST_LINE_PATTERN = '/^(ok|not ok)(?:\s+\d+)?(?:\s+\-)?\s*(.*?)(?:\s*#\s*(skip|todo)\s*(.*))?\s*$/i'; + const TEST_YAML_START = '/^(\s*)---/'; + const TEST_DIAGNOSTIC = '/^#/'; /** * @var string */ protected $tapString; + + /** + * @var int + */ protected $failures = 0; + /** + * @var array + */ + protected $lines; + + /** + * @var integer + */ + protected $lineNumber; + + /** + * @var integer + */ + protected $testCount; + + /** + * @var array + */ + protected $results; + /** * Create a new TAP parser for a given string. * @param string $tapString The TAP format string to be parsed. @@ -38,81 +63,175 @@ class TapParser { // Split up the TAP string into an array of lines, then // trim all of the lines so there's no leading or trailing whitespace. - $lines = explode("\n", $this->tapString); - $lines = array_map(function ($line) { - return trim($line); - }, $lines); + $this->lines = array_map('rtrim', explode("\n", $this->tapString)); + $this->lineNumber = 0; - // Check TAP version: - $versionLine = array_shift($lines); + $this->testCount = false; + $this->results = array(); - if ($versionLine != 'TAP version 13') { - throw new \Exception(Lang::get('tap_version')); + $header = $this->findTapLog(); + + $line = $this->nextLine(); + if ($line === $header) { + throw new Exception("Duplicated TAP log, please check the configration."); } - if (isset($lines[count($lines) - 1]) && preg_match(self::TEST_COVERAGE_PATTERN, $lines[count($lines) - 1])) { - array_pop($lines); - if ($lines[count($lines) - 1] == "") { - array_pop($lines); + while ($line !== false && ($this->testCount === false || count($this->results) < $this->testCount)) { + $this->parseLine($line); + $line = $this->nextLine(); + } + + if (count($this->results) !== $this->testCount) { + throw new Exception(Lang::get('tap_error')); + } + + return $this->results; + } + + /** Looks for the start of the TAP log in the string. + * + * @return string The TAP header line. + * + * @throws Exception if no TAP log is found or versions mismatch. + */ + protected function findTapLog() + { + // Look for the beggning of the TAP output + do { + $header = $this->nextLine(); + } while ($header !== false && substr($header, 0, 12) !== 'TAP version '); + + // + if ($header === false) { + throw new Exception('No TAP log found, please check the configuration.'); + } elseif ($header !== 'TAP version 13') { + throw new Exception(Lang::get('tap_version')); + } + + return $header; + } + + /** Fetch the next line. + * + * @return string|false The next line or false if the end has been reached. + */ + protected function nextLine() + { + if ($this->lineNumber < count($this->lines)) { + return $this->lines[$this->lineNumber++]; + } + return false; + } + + /** Parse a single line. + * + * @param string $line + */ + protected function parseLine($line) + { + if (preg_match(self::TEST_COUNTS_PATTERN, $line, $matches)) { + $this->testCount = intval($matches[1]); + + } elseif (preg_match(self::TEST_DIAGNOSTIC, $line)) { + return; + + } elseif (preg_match(self::TEST_LINE_PATTERN, $line, $matches)) { + $this->results[] = $this->processTestLine( + $matches[1], + isset($matches[2]) ? $matches[2] : '', + isset($matches[3]) ? $matches[3] : null, + isset($matches[4]) ? $matches[4] : null + ); + + } elseif (preg_match(self::TEST_YAML_START, $line, $matches)) { + $diagnostic = $this->processYamlBlock($matches[1]); + $test = array_pop($this->results); + if (isset($test['message'], $diagnostic['message'])) { + $test['message'] .= PHP_EOL . $diagnostic['message']; + unset($diagnostic['message']); } + $this->results[] = array_replace($test, $diagnostic); + + } else { + throw new Exception(sprintf('Incorrect TAP data, line %d: %s', $this->lineNumber, $line)); } - - $matches = array(); - $totalTests = 0; - if (preg_match(self::TEST_COUNTS_PATTERN, $lines[0], $matches)) { - array_shift($lines); - $totalTests = (int) $matches[2]; - } - - if (isset($lines[count($lines) - 1]) && - preg_match(self::TEST_COUNTS_PATTERN, $lines[count($lines) - 1], $matches)) { - array_pop($lines); - $totalTests = (int) $matches[2]; - } - - $rtn = $this->processTestLines($lines); - - if ($totalTests != count($rtn)) { - throw new \Exception(Lang::get('tap_error')); - } - - return $rtn; } /** - * Process the individual test lines within a TAP string. - * @param $lines + * Process an individual test line. + * + * @param string $result + * @param string $message + * @param string $directive + * @param string $reason + * * @return array */ - protected function processTestLines($lines) + protected function processTestLine($result, $message, $directive, $reason) { - $rtn = array(); + $test = array( + 'pass' => true, + 'message' => $message, + 'severity' => 'success', + ); - foreach ($lines as $line) { - $matches = array(); - - if (preg_match(self::TEST_LINE_PATTERN, $line, $matches)) { - $ok = ($matches[1] == 'ok' ? true : false); - - if (!$ok) { - $this->failures++; - } - - $item = array( - 'pass' => $ok, - 'suite' => $matches[2], - 'test' => $matches[3], - ); - - $rtn[] = $item; - } elseif (preg_match(self::TEST_SKIP_PATTERN, $line, $matches)) { - $rtn[] = array('message' => 'SKIP'); - } elseif (preg_match(self::TEST_MESSAGE_PATTERN, $line, $matches)) { - $rtn[count($rtn) - 1]['message'] = $matches[1]; - } + if ($result !== 'ok') { + $test['pass'] = false; + $test['severity'] = substr($message, 0, 6) === 'Error:' ? 'error' : 'fail'; + $this->failures++; } - return $rtn; + if ($directive) { + $test = $this->processDirective($test, $directive, $reason); + } + + return $test; + } + + /** Process an indented Yaml block. + * + * @param string $indent The block indentation to ignore. + * + * @return array The processed Yaml content. + */ + protected function processYamlBlock($indent) + { + $startLine = $this->lineNumber+1; + $endLine = $indent.'...'; + $yamlLines = array(); + do { + $line = $this->nextLine(); + if ($line === false) { + throw new Exception(Lang::get('tap_error_endless_yaml', $startLine)); + } elseif ($line === $endLine) { + break; + } + $yamlLines[] = substr($line, strlen($indent)); + + } while (true); + + return Yaml::parse(join("\n", $yamlLines)); + } + + /** Process a TAP directive + * + * @param array $test + * @param string $directive + * @param string $reason + * @return array + */ + protected function processDirective($test, $directive, $reason) + { + $test['severity'] = strtolower($directive) === 'skip' ? 'skipped' : 'todo'; + + if (!empty($reason)) { + if (!empty($test['message'])) { + $test['message'] .= ', '.$test['severity'].': '; + } + $test['message'] .= $reason; + } + + return $test; } /** diff --git a/Tests/PHPCI/Plugin/Util/TapParserTest.php b/Tests/PHPCI/Plugin/Util/TapParserTest.php index c4438b79..75c04bfa 100644 --- a/Tests/PHPCI/Plugin/Util/TapParserTest.php +++ b/Tests/PHPCI/Plugin/Util/TapParserTest.php @@ -5,22 +5,254 @@ use PHPCI\Plugin\Util\TapParser; class TapParserTest extends \PHPUnit_Framework_TestCase { - public function testSkipped() + public function testSimple() + { + $content = <<parse(); + + $this->assertEquals(array( + array('pass' => true, 'severity' => 'success', 'message' => 'SomeTest::testAnother'), + array('pass' => false, 'severity' => 'fail', 'message' => ''), + ), $result); + + $this->assertEquals(1, $parser->getTotalFailures()); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessageRegExp /No TAP/ + */ + public function testNoTapData() + { + $content = <<parse(); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessageRegExp /Duplicated TAP/ + */ + public function testDuplicateOutput() + { + $content = <<parse(); + } + + public function testSuiteAndTest() { $content = <<parse(); + + $this->assertEquals(array( + array('pass' => true, 'severity' => 'success', 'message' => 'SomeTest::testAnother',), + array('pass' => false, 'severity' => 'fail', 'message' => 'Failure: SomeTest::testAnother'), + array('pass' => false, 'severity' => 'error', 'message' => 'Error: SomeTest::testAnother'), + ), $result); + + $this->assertEquals(2, $parser->getTotalFailures()); + } + + + public function testSkipped() + { + $content = <<parse(); + + $this->assertEquals(array( + array('pass' => true, 'severity' => 'skipped', 'message' => ''), + array('pass' => true, 'severity' => 'skipped', 'message' => 'foobar'), + array('pass' => true, 'severity' => 'skipped', 'message' => 'foo, skipped: bar'), + ), $result); + + $this->assertEquals(0, $parser->getTotalFailures()); + } + + public function testTodo() + { + $content = <<parse(); + + $this->assertEquals(array( + array('pass' => true, 'severity' => 'todo', 'message' => 'SomeTest::testAnother, todo: really implement this test'), + array('pass' => true, 'severity' => 'todo', 'message' => 'really implement this test'), + array('pass' => true, 'severity' => 'todo', 'message' => 'this is a message, todo: really implement this test'), + array('pass' => true, 'severity' => 'todo', 'message' => ''), + ), $result); + + $this->assertEquals(0, $parser->getTotalFailures()); + } + + public function testYamlDiagnostic() + { + // From https://phpunit.de/manual/current/en/logging.html#logging.tap + $content = <<parse(); + + $this->assertEquals(array( + array( + 'pass' => false, + 'severity' => 'fail', + 'message' => 'FOO' . PHP_EOL . 'BAR', + ), + ), $result); + + $this->assertEquals(1, $parser->getTotalFailures()); + } + + public function testFailureAndError() + { + // From https://phpunit.de/manual/current/en/logging.html#logging.tap + $content = <<parse(); $this->assertEquals(array( - array('pass' => true, 'suite' => 'SomeTest', 'test' => 'testAnother'), - array('message' => 'SKIP'), + array( + 'pass' => false, + 'severity' => 'fail', + 'message' => 'Failure: testFailure::FailureErrorTest', + ), + array( + 'pass' => false, + 'severity' => 'error', + 'message' => 'Error: testError::FailureErrorTest', + ) ), $result); + $this->assertEquals(2, $parser->getTotalFailures()); + } + + /** + * @expectedException \Exception + */ + public function testGarbage() + { + $content = "Garbage !"; + + $parser = new TapParser($content); + $parser->parse(); + } + + /** + * @expectedException \Exception + */ + public function testInvalidTestCount() + { + $content = <<parse(); + } + + /** + * @expectedException \Exception + */ + public function testEndlessYaml() + { + $content = <<parse(); + } + + public function testCodeception() + { + $content = <<< TAP +TAP version 13 +ok 1 - try to access the dashboard as a guest (Auth/GuestAccessDashboardAndRedirectCept) +ok 2 - see the login page (Auth/SeeLoginCept) +ok 3 - click forgot password and see the email form (Auth/SeeLoginForgotPasswordCept) +ok 4 - see powered by runmybusiness branding (Auth/ShouldSeePoweredByBrandingCept) +ok 5 - submit invalid credentials (Auth/SubmitLoginAndFailCept) +ok 6 - submit valid credentials and see the dashboard (Auth/SubmitLoginAndSucceedCept) +1..6 +TAP; + + $parser = new TapParser($content); + $result = $parser->parse(); + + $this->assertEquals( + array( + array('pass' => true, 'severity' => 'success', 'message' => 'try to access the dashboard as a guest (Auth/GuestAccessDashboardAndRedirectCept)'), + array('pass' => true, 'severity' => 'success', 'message' => 'see the login page (Auth/SeeLoginCept)'), + array('pass' => true, 'severity' => 'success', 'message' => 'click forgot password and see the email form (Auth/SeeLoginForgotPasswordCept)'), + array('pass' => true, 'severity' => 'success', 'message' => 'see powered by runmybusiness branding (Auth/ShouldSeePoweredByBrandingCept)'), + array('pass' => true, 'severity' => 'success', 'message' => 'submit invalid credentials (Auth/SubmitLoginAndFailCept)'), + array('pass' => true, 'severity' => 'success', 'message' => 'submit valid credentials and see the dashboard (Auth/SubmitLoginAndSucceedCept)'), + ), + $result + ); + $this->assertEquals(0, $parser->getTotalFailures()); + } } diff --git a/public/assets/css/AdminLTE-custom.css b/public/assets/css/AdminLTE-custom.css index b95f7b70..e37a0f48 100644 --- a/public/assets/css/AdminLTE-custom.css +++ b/public/assets/css/AdminLTE-custom.css @@ -84,4 +84,11 @@ padding: 0; font-size: inherit; line-height: inherit; -} \ No newline at end of file +} + +#phpunit-data th div { margin: 0 0.5em; } +#phpunit-data .success td { background: none; color: #00a65a; } +#phpunit-data .fail td { background: none; color: #f56954; } +#phpunit-data .error td { background: none; color: #f56954; } +#phpunit-data .skipped td { background: none; color: #e08e0b; } +#phpunit-data .todo td { background: none; color: #00c0ef; } diff --git a/public/assets/js/build-plugins/phpunit.js b/public/assets/js/build-plugins/phpunit.js index 4a55d905..66f010a1 100644 --- a/public/assets/js/build-plugins/phpunit.js +++ b/public/assets/js/build-plugins/phpunit.js @@ -6,6 +6,13 @@ var phpunitPlugin = ActiveBuild.UiPlugin.extend({ displayOnUpdate: false, box: true, rendered: false, + statusMap: { + success : 'ok', + fail: 'remove', + error: 'warning-sign', + todo: 'info-sign', + skipped: 'exclamation-sign' + }, register: function() { var self = this; @@ -21,6 +28,11 @@ var phpunitPlugin = ActiveBuild.UiPlugin.extend({ query(); } }); + + $(document).on('click', '#phpunit-data .test-toggle', function(ev) { + var input = $(ev.target); + $('#phpunit-data tbody ' + input.data('target')).toggle(input.prop('checked')); + }); }, render: function() { @@ -28,7 +40,7 @@ var phpunitPlugin = ActiveBuild.UiPlugin.extend({ return $('
    #getId(), 6, '0', STR_PAD_LEFT); ?>getCreated()->format('Y-m-d H:i:s'); ?> getProject())) { diff --git a/PHPCI/View/Project/view.phtml b/PHPCI/View/Project/view.phtml index 900349c8..2e79c017 100644 --- a/PHPCI/View/Project/view.phtml +++ b/PHPCI/View/Project/view.phtml @@ -42,6 +42,7 @@
    ' + '' + '' + - ' ' + + ' ' + '' + '
    '+Lang.get('test')+''+Lang.get('test_message')+'
    '); }, @@ -43,7 +55,9 @@ var phpunitPlugin = ActiveBuild.UiPlugin.extend({ this.lastData = e.queryData; var tests = this.lastData[0].meta_value; + var thead = $('#phpunit-data thead tr'); var tbody = $('#phpunit-data tbody'); + thead.empty().append(''+Lang.get('test_message')+''); tbody.empty(); if (tests.length == 0) { @@ -51,24 +65,74 @@ var phpunitPlugin = ActiveBuild.UiPlugin.extend({ return; } + var counts = { success: 0, fail: 0, error: 0, skipped: 0, todo: 0 }, total = 0; + for (var i in tests) { - - var row = $('' + - ''+tests[i].suite+'' + - '::'+tests[i].test+'
    ' + - ''+(tests[i].message || '')+'' + - ''); - - if (!tests[i].pass) { - row.addClass('danger'); - } else { - row.addClass('success'); - } - - tbody.append(row); + var severity = tests[i].severity || 'success', + message = tests[i].message || ('' + Lang.get('test_no_message') + ''); + counts[severity]++; + total++; + tbody.append( + '' + + '' + + '
    ' + message + '
    ' + + (tests[i].data ? '
    ' + this.repr(tests[i].data) + '
    ' : '') + + '' + + '' + ); } + var checkboxes = $(''); + thead.append(checkboxes).append('' + Lang.get('test_total', total) + ''); + + for (var key in counts) { + var count = counts[key]; + if(count > 0) { + checkboxes.append( + '
     ' + + Lang.get('test_'+key, count)+ '
    ' + ); + } + } + + tbody.find('.success').hide(); + $('#build-phpunit-errors').show(); + }, + + repr: function(data) + { + switch(typeof(data)) { + case 'boolean': + return '' + (data ? 'true' : 'false') + ''; + case 'string': + return '"' + data + '"'; + case 'undefined': case null: + return 'null'; + case 'object': + var rows = []; + if(data instanceof Array) { + for(var i in data) { + rows.push('' + this.repr(data[i]) + ','); + } + } else { + for(var key in data) { + rows.push( + '' + + '' + this.repr(key) + '' + + '=>' + + '' + this.repr(data[key]) + ',' + + ''); + } + } + return '' + + '' + + rows.join('') + + '' + + '
    array(
    )
    '; + } + return '???'; } }); From 01911f11aa7c2e20087aa139df25cc081eabf41c Mon Sep 17 00:00:00 2001 From: Tobias van Beek Date: Wed, 25 Mar 2015 11:10:47 +0100 Subject: [PATCH 174/329] Add the --recursive parameter to the git clone to get the submodules --- PHPCI/Model/Build/RemoteGitBuild.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PHPCI/Model/Build/RemoteGitBuild.php b/PHPCI/Model/Build/RemoteGitBuild.php index 80718011..5d0c9419 100644 --- a/PHPCI/Model/Build/RemoteGitBuild.php +++ b/PHPCI/Model/Build/RemoteGitBuild.php @@ -54,7 +54,7 @@ class RemoteGitBuild extends Build */ protected function cloneByHttp(Builder $builder, $cloneTo) { - $cmd = 'git clone '; + $cmd = 'git clone --recursive '; $depth = $builder->getConfig('clone_depth'); @@ -84,7 +84,7 @@ class RemoteGitBuild extends Build } // Do the git clone: - $cmd = 'git clone '; + $cmd = 'git clone --recursive '; $depth = $builder->getConfig('clone_depth'); From 524341a50b33c47b6d7ba1983d8446da9e57a7f5 Mon Sep 17 00:00:00 2001 From: Angel Koilov Date: Fri, 27 Mar 2015 14:09:03 +0200 Subject: [PATCH 175/329] remove unnecessary code --- PHPCI/Plugin/Behat.php | 1 - 1 file changed, 1 deletion(-) diff --git a/PHPCI/Plugin/Behat.php b/PHPCI/Plugin/Behat.php index 23686270..445d7191 100644 --- a/PHPCI/Plugin/Behat.php +++ b/PHPCI/Plugin/Behat.php @@ -98,7 +98,6 @@ class Behat implements \PHPCI\Plugin $lines = explode(PHP_EOL, $parts[1]); - $errorCount = 0; $storeFailures = false; $data = array(); From 731fd65453c7663129dc0175b208b61ce9e06d39 Mon Sep 17 00:00:00 2001 From: zviryatko Date: Sat, 28 Mar 2015 16:50:40 +0200 Subject: [PATCH 176/329] Change xmpp config and message files directory --- PHPCI/Plugin/Xmpp.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/PHPCI/Plugin/Xmpp.php b/PHPCI/Plugin/Xmpp.php index 8614e208..add778cb 100644 --- a/PHPCI/Plugin/Xmpp.php +++ b/PHPCI/Plugin/Xmpp.php @@ -132,8 +132,8 @@ class XMPP implements \PHPCI\Plugin */ public function findConfigFile() { - if (file_exists('.sendxmpprc')) { - if (md5(file_get_contents('.sendxmpprc')) !== md5($this->getConfigFormat())) { + if (file_exists($this->phpci->buildPath . '/.sendxmpprc')) { + if (md5(file_get_contents($this->phpci->buildPath . '/.sendxmpprc')) !== md5($this->getConfigFormat())) { return null; } @@ -165,9 +165,10 @@ class XMPP implements \PHPCI\Plugin /* * Try to build conf file */ + $config_file = $this->phpci->buildPath . '/.sendxmpprc'; if (is_null($this->findConfigFile())) { - file_put_contents('.sendxmpprc', $this->getConfigFormat()); - chmod('.sendxmpprc', 0600); + file_put_contents($config_file, $this->getConfigFormat()); + chmod($config_file, 0600); } /* @@ -178,7 +179,7 @@ class XMPP implements \PHPCI\Plugin $tls = ' -t'; } - $message_file = uniqid('xmppmessage'); + $message_file = $this->phpci->buildPath . '/' . uniqid('xmppmessage'); if ($this->buildMessage($message_file) === false) { return false; } @@ -186,10 +187,10 @@ class XMPP implements \PHPCI\Plugin /* * Send XMPP notification for all recipients */ - $cmd = $sendxmpp . "%s -f .sendxmpprc -m %s %s"; + $cmd = $sendxmpp . "%s -f %s -m %s %s"; $recipients = implode(' ', $this->recipients); - $success = $this->phpci->executeCommand($cmd, $tls, $message_file, $recipients); + $success = $this->phpci->executeCommand($cmd, $tls, $config_file, $message_file, $recipients); print $this->phpci->getLastOutput(); From f3c1a98cf10871eadd62bc461e4e02382384ba2f Mon Sep 17 00:00:00 2001 From: Adirelle Date: Wed, 25 Mar 2015 15:15:53 +0100 Subject: [PATCH 177/329] Detailed webhook responses. Fixed docblocks. Reworked WebhookController to enforce Json responses in ::handleAction. Check the project type match the webhook. When creating several builds, do not stop on first error. Try to create every builds and report 'ok' if at least one succeeds. CS fix. Fixed Uses. Fixed the types accepted by the git webhook. Added some really basic test. --- PHPCI/Controller/WebhookController.php | 378 ++++++++++-------- .../Controller/WebhookControllerTest.php | 41 ++ 2 files changed, 260 insertions(+), 159 deletions(-) create mode 100644 Tests/PHPCI/Controller/WebhookControllerTest.php diff --git a/PHPCI/Controller/WebhookController.php b/PHPCI/Controller/WebhookController.php index 78824ddb..e05cfbda 100644 --- a/PHPCI/Controller/WebhookController.php +++ b/PHPCI/Controller/WebhookController.php @@ -2,7 +2,7 @@ /** * PHPCI - Continuous Integration for PHP * - * @copyright Copyright 2014, Block 8 Limited. + * @copyright Copyright 2014-2015, Block 8 Limited. * @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md * @link https://www.phptesting.org/ */ @@ -11,31 +11,37 @@ namespace PHPCI\Controller; use b8; use b8\Store; +use Exception; use PHPCI\BuildFactory; +use PHPCI\Model\Project; use PHPCI\Service\BuildService; +use PHPCI\Store\BuildStore; +use PHPCI\Store\ProjectStore; /** * Webhook Controller - Processes webhook pings from BitBucket, Github, Gitlab, etc. + * * @author Dan Cryer * @author Sami Tikka * @author Alex Russell + * @author Guillaume Perréal * @package PHPCI * @subpackage Web */ -class WebhookController extends \PHPCI\Controller +class WebhookController extends \b8\Controller { /** - * @var \PHPCI\Store\BuildStore + * @var BuildStore */ protected $buildStore; /** - * @var \PHPCI\Store\ProjectStore + * @var ProjectStore */ protected $projectStore; /** - * @var \PHPCI\Service\BuildService + * @var BuildService */ protected $buildService; @@ -49,194 +55,207 @@ class WebhookController extends \PHPCI\Controller $this->buildService = new BuildService($this->buildStore); } + /** Handle the action, Ensuring to return a JsonResponse. + * + * @param string $action + * @param mixed $actionParams + * + * @return \b8\Http\Response + */ + public function handleAction($action, $actionParams) + { + $response = new b8\Http\Response\JsonResponse(); + try { + $data = parent::handleAction($action, $actionParams); + if (isset($data['responseCode'])) { + $response->setResponseCode($data['responseCode']); + unset($data['responseCode']); + } + $response->setContent($data); + } catch (Exception $ex) { + $response->setResponseCode(500); + $response->setContent(array('status' => 'failed', 'error' => $ex->getMessage())); + } + return $response; + } + /** * Called by Bitbucket POST service. */ - public function bitbucket($project) + public function bitbucket($projectId) { - $response = new b8\Http\Response\JsonResponse(); - $response->setContent(array('status' => 'ok')); - + $project = $this->fetchProject($projectId, 'bitbucket'); $payload = json_decode($this->getParam('payload'), true); + $results = array(); + $status = 'failed'; foreach ($payload['commits'] as $commit) { try { $email = $commit['raw_author']; $email = substr($email, 0, strpos($email, '>')); $email = substr($email, strpos($email, '<') + 1); - $this->createBuild($project, $commit['raw_node'], $commit['branch'], $email, $commit['message']); - } catch (\Exception $ex) { - $response->setResponseCode(500); - $response->setContent(array('status' => 'failed', 'error' => $ex->getMessage())); - break; + $results[$commit['raw_node']] = $this->createBuild( + $project, + $commit['raw_node'], + $commit['branch'], + $email, + $commit['message'] + ); + $status = 'ok'; + } catch (Exception $ex) { + $results[$commit['raw_node']] = array('status' => 'failed', 'error' => $ex->getMessage()); } } - return $response; + return array('status' => $status, 'commits' => $results); } /** * Called by POSTing to /webhook/git/?branch=&commit= * - * @param string $project + * @param string $projectId */ - public function git($project) + public function git($projectId) { - $response = new b8\Http\Response\JsonResponse(); - $response->setContent(array('status' => 'ok')); - - $branch = $this->getParam('branch'); + $project = $this->fetchProject($projectId, array('local', 'remote')); + $branch = $this->getParam('branch', $project->getBranch()); $commit = $this->getParam('commit'); $commitMessage = $this->getParam('message'); $committer = $this->getParam('committer'); - try { - if (empty($branch)) { - $branch = 'master'; - } - - if (empty($commit)) { - $commit = null; - } - - if (empty($commitMessage)) { - $commitMessage = null; - } - - if (empty($committer)) { - $committer = null; - } - - $this->createBuild($project, $commit, $branch, $committer, $commitMessage); - } catch (\Exception $ex) { - $response->setResponseCode(500); - $response->setContent(array('status' => 'failed', 'error' => $ex->getMessage())); - } - - return $response; + return $this->createBuild($project, $commit, $branch, $committer, $commitMessage); } /** * Called by Github Webhooks: */ - public function github($project) + public function github($projectId) { - $response = new b8\Http\Response\JsonResponse(); - $response->setContent(array('status' => 'ok')); + $project = $this->fetchProject($projectId, 'github'); switch ($_SERVER['CONTENT_TYPE']) { case 'application/json': $payload = json_decode(file_get_contents('php://input'), true); break; - case 'application/x-www-form-urlencoded': $payload = json_decode($this->getParam('payload'), true); break; - default: - $response->setResponseCode(401); - $response->setContent(array('status' => 'failed', 'error' => 'Content type not supported.')); - return $response; + return array('status' => 'failed', 'error' => 'Content type not supported.', 'responseCode' => 401); } // Handle Pull Request web hooks: if (array_key_exists('pull_request', $payload)) { - return $this->githubPullRequest($project, $payload, $response); + return $this->githubPullRequest($project, $payload); } // Handle Push web hooks: if (array_key_exists('commits', $payload)) { - return $this->githubCommitRequest($project, $payload, $response); + return $this->githubCommitRequest($project, $payload); } - return $response; + return array('status' => 'ignored', 'message' => 'Unusable payload.'); } /** * Handle the payload when Github sends a commit webhook. - * @param $project + * + * @param Project $project * @param array $payload * @param b8\Http\Response\JsonResponse $response + * * @return b8\Http\Response\JsonResponse */ - protected function githubCommitRequest($project, array $payload, b8\Http\Response\JsonResponse $response) + protected function githubCommitRequest(Project $project, array $payload) { // Github sends a payload when you close a pull request with a // non-existant commit. We don't want this. if (array_key_exists('after', $payload) && $payload['after'] === '0000000000000000000000000000000000000000') { - return $response; + return array('status' => 'ignored'); } - try { - if (isset($payload['commits']) && is_array($payload['commits'])) { - // If we have a list of commits, then add them all as builds to be tested: + if (isset($payload['commits']) && is_array($payload['commits'])) { + // If we have a list of commits, then add them all as builds to be tested: - foreach ($payload['commits'] as $commit) { - if (!$commit['distinct']) { - continue; - } + $results = array(); + $status = 'failed'; + foreach ($payload['commits'] as $commit) { + if (!$commit['distinct']) { + $results[$commit['id']] = array('status' => 'ignored'); + continue; + } + try { $branch = str_replace('refs/heads/', '', $payload['ref']); $committer = $commit['committer']['email']; - $this->createBuild($project, $commit['id'], $branch, $committer, $commit['message']); + $results[$commit['id']] = $this->createBuild( + $project, + $commit['id'], + $branch, + $committer, + $commit['message'] + ); + $status = 'ok'; + } catch (Exception $ex) { + $results[$commit['id']] = array('status' => 'failed', 'error' => $ex->getMessage()); } - } elseif (substr($payload['ref'], 0, 10) == 'refs/tags/') { - // If we don't, but we're dealing with a tag, add that instead: - $branch = str_replace('refs/tags/', 'Tag: ', $payload['ref']); - $committer = $payload['pusher']['email']; - $message = $payload['head_commit']['message']; - $this->createBuild($project, $payload['after'], $branch, $committer, $message); } - - } catch (\Exception $ex) { - $response->setResponseCode(500); - $response->setContent(array('status' => 'failed', 'error' => $ex->getMessage())); - + return array('status' => $status, 'commits' => $results); } - return $response; + if (substr($payload['ref'], 0, 10) == 'refs/tags/') { + // If we don't, but we're dealing with a tag, add that instead: + $branch = str_replace('refs/tags/', 'Tag: ', $payload['ref']); + $committer = $payload['pusher']['email']; + $message = $payload['head_commit']['message']; + return $this->createBuild($project, $payload['after'], $branch, $committer, $message); + } + + return array('status' => 'ignored', 'message' => 'Unusable payload.'); } /** * Handle the payload when Github sends a Pull Request webhook. - * @param $projectId + * + * @param Project $project * @param array $payload */ - protected function githubPullRequest($projectId, array $payload, b8\Http\Response\JsonResponse $response) + protected function githubPullRequest(Project $project, array $payload) { // We only want to know about open pull requests: if (!in_array($payload['action'], array('opened', 'synchronize', 'reopened'))) { - return $response; + return array('status' => 'ok'); } - try { - $headers = array(); - $token = \b8\Config::getInstance()->get('phpci.github.token'); + $headers = array(); + $token = \b8\Config::getInstance()->get('phpci.github.token'); - if (!empty($token)) { - $headers[] = 'Authorization: token ' . $token; + if (!empty($token)) { + $headers[] = 'Authorization: token ' . $token; + } + + $url = $payload['pull_request']['commits_url']; + $http = new \b8\HttpClient(); + $http->setHeaders($headers); + $response = $http->get($url); + + // Check we got a success response: + if (!$response['success']) { + throw new Exception('Could not get commits, failed API request.'); + } + + $results = array(); + $status = 'failed'; + foreach ($response['body'] as $commit) { + // Skip all but the current HEAD commit ID: + $id = $commit['sha']; + if ($id != $payload['pull_request']['head']['sha']) { + $results[$id] = array('status' => 'ignored', 'message' => 'not branch head'); + continue; } - $url = $payload['pull_request']['commits_url']; - $http = new \b8\HttpClient(); - $http->setHeaders($headers); - $response = $http->get($url); - - // Check we got a success response: - if (!$response['success']) { - $message = 'Could not get commits, failed API request.'; - $response->setResponseCode(500); - $response->setContent(array('status' => 'failed', 'error' => $message)); - return $response; - } - - foreach ($response['body'] as $commit) { - // Skip all but the current HEAD commit ID: - if ($commit['sha'] != $payload['pull_request']['head']['sha']) { - continue; - } - + try { $branch = str_replace('refs/heads/', '', $payload['pull_request']['base']['ref']); $committer = $commit['commit']['author']['email']; $message = $commit['commit']['message']; @@ -249,83 +268,96 @@ class WebhookController extends \PHPCI\Controller 'remote_url' => $payload['pull_request']['head']['repo']['clone_url'], ); - $this->createBuild($projectId, $commit['sha'], $branch, $committer, $message, $extra); + $results[$id] = $this->createBuild($project, $id, $branch, $committer, $message, $extra); + $status = 'ok'; + } catch (Exception $ex) { + $results[$id] = array('status' => 'failed', 'error' => $ex->getMessage()); } - } catch (\Exception $ex) { - $response->setResponseCode(500); - $response->setContent(array('status' => 'failed', 'error' => $ex->getMessage())); } - return $response; + return array('status' => $status, 'commits' => $results); } /** * Called by Gitlab Webhooks: */ - public function gitlab($project) + public function gitlab($projectId) { - $response = new b8\Http\Response\JsonResponse(); - $response->setContent(array('status' => 'ok')); + $project = $this->fetchProject($projectId, 'gitlab'); $payloadString = file_get_contents("php://input"); $payload = json_decode($payloadString, true); - try { - // build on merge request events - if (isset($payload['object_kind']) && $payload['object_kind'] == 'merge_request') { - $attributes = $payload['object_attributes']; - if ($attributes['state'] == 'opened' || $attributes['state'] == 'reopened') { - $branch = $attributes['source_branch']; - $commit = $attributes['last_commit']; - $committer = $commit['author']['email']; + // build on merge request events + if (isset($payload['object_kind']) && $payload['object_kind'] == 'merge_request') { + $attributes = $payload['object_attributes']; + if ($attributes['state'] == 'opened' || $attributes['state'] == 'reopened') { + $branch = $attributes['source_branch']; + $commit = $attributes['last_commit']; + $committer = $commit['author']['email']; - $this->createBuild($project, $commit['id'], $branch, $committer, $commit['message']); - } + return $this->createBuild($project, $commit['id'], $branch, $committer, $commit['message']); } - - // build on push events - if (isset($payload['commits']) && is_array($payload['commits'])) { - // If we have a list of commits, then add them all as builds to be tested: - - foreach ($payload['commits'] as $commit) { - $branch = str_replace('refs/heads/', '', $payload['ref']); - $committer = $commit['author']['email']; - $this->createBuild($project, $commit['id'], $branch, $committer, $commit['message']); - } - } - - } catch (\Exception $ex) { - $response->setResponseCode(500); - $response->setContent(array('status' => 'failed', 'error' => $ex->getMessage())); } - return $response; + // build on push events + if (isset($payload['commits']) && is_array($payload['commits'])) { + // If we have a list of commits, then add them all as builds to be tested: + + $results = array(); + $status = 'failed'; + foreach ($payload['commits'] as $commit) { + try { + $branch = str_replace('refs/heads/', '', $payload['ref']); + $committer = $commit['author']['email']; + $results[$commit['id']] = $this->createBuild( + $project, + $commit['id'], + $branch, + $committer, + $commit['message'] + ); + $status = 'ok'; + } catch (Exception $ex) { + $results[$commit['id']] = array('status' => 'failed', 'error' => $ex->getMessage()); + } + } + return array('status' => $status, 'commits' => $results); + } + + return array('status' => 'ignored', 'message' => 'Unusable payload.'); } /** * Wrapper for creating a new build. - * @param $projectId - * @param $commitId - * @param $branch - * @param $committer - * @param $commitMessage - * @param null $extra - * @return bool - * @throws \Exception + * + * @param Project $project + * @param string $commitId + * @param string $branch + * @param string $committer + * @param string $commitMessage + * @param array $extra + * + * @return array + * + * @throws Exception */ - protected function createBuild($projectId, $commitId, $branch, $committer, $commitMessage, $extra = null) - { + protected function createBuild( + Project $project, + $commitId, + $branch, + $committer, + $commitMessage, + array $extra = null + ) { // Check if a build already exists for this commit ID: - $builds = $this->buildStore->getByProjectAndCommit($projectId, $commitId); + $builds = $this->buildStore->getByProjectAndCommit($project->getId(), $commitId); if ($builds['count']) { - return true; - } - - $project = $this->projectStore->getById($projectId); - - if (empty($project)) { - throw new \Exception('Project does not exist:' . $projectId); + return array( + 'status' => 'ignored', + 'message' => sprintf('Duplicate of build #%d', $builds['items'][0]->getId()) + ); } // If not, create a new build job for it: @@ -335,6 +367,34 @@ class WebhookController extends \PHPCI\Controller // Send a status postback if the build type provides one: $build->sendStatusPostback(); - return true; + return array('status' => 'ok', 'buildID' => $build->getID()); + } + + /** + * Fetch a project and check its type. + * + * @param int $projectId + * @param array|string $expectedType + * + * @return Project + * + * @throws Exception If the project does not exist or is not of the expected type. + */ + protected function fetchProject($projectId, $expectedType) + { + $project = $this->projectStore->getById($projectId); + + if (empty($projectId)) { + throw new Exception('Project does not exist: ' . $projectId); + } + + if (is_array($expectedType) + ? !in_array($project->getType(), $expectedType) + : $project->getType() !== $expectedType + ) { + throw new Exception('Wrong project type: ' . $project->getType()); + } + + return $project; } } diff --git a/Tests/PHPCI/Controller/WebhookControllerTest.php b/Tests/PHPCI/Controller/WebhookControllerTest.php new file mode 100644 index 00000000..ddd13fc0 --- /dev/null +++ b/Tests/PHPCI/Controller/WebhookControllerTest.php @@ -0,0 +1,41 @@ +prophesize('b8\Config')->reveal(), + $this->prophesize('b8\Http\Request')->reveal(), + $this->prophesize('b8\Http\Response')->reveal() + ); + + $error = $webController->handleAction('test', []); + + $this->assertInstanceOf('b8\Http\Response\JsonResponse', $error); + + $responseData = $error->getData(); + $this->assertEquals(500, $responseData['code']); + + $this->assertEquals('failed', $responseData['body']['status']); + + $this->assertEquals('application/json', $responseData['headers']['Content-Type']); + + // @todo: we can't text the result is JSON file with + // $this->assertJson((string) $error); + // since the flush method automatically add the header and break the + // testing framework. + } +} From 067a60983fce1497482adf1802901df5820ddcb6 Mon Sep 17 00:00:00 2001 From: zviryatko Date: Wed, 25 Feb 2015 16:16:23 +0200 Subject: [PATCH 178/329] Fix archive link. --- PHPCI/View/layout.phtml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PHPCI/View/layout.phtml b/PHPCI/View/layout.phtml index 72ef27ff..82eabfe5 100644 --- a/PHPCI/View/layout.phtml +++ b/PHPCI/View/layout.phtml @@ -138,7 +138,7 @@
    From f25b1d25dcea411f8273f4cb40dfc94a8e0c187b Mon Sep 17 00:00:00 2001 From: Adirelle Date: Tue, 14 Apr 2015 12:17:52 +0200 Subject: [PATCH 185/329] Let CommandExecutor::findBinary throw an exception when the binary is missing. Close #910 --- PHPCI/Builder.php | 8 +++--- PHPCI/Helper/BaseCommandExecutor.php | 31 +++++++++++----------- PHPCI/Helper/CommandExecutor.php | 7 +++-- PHPCI/Plugin/Codeception.php | 6 ----- PHPCI/Plugin/Composer.php | 5 ---- PHPCI/Plugin/Pdepend.php | 7 +---- PHPCI/Plugin/Phing.php | 5 ---- PHPCI/Plugin/PhpCodeSniffer.php | 5 ---- PHPCI/Plugin/PhpCpd.php | 7 +---- PHPCI/Plugin/PhpCsFixer.php | 5 ---- PHPCI/Plugin/PhpDocblockChecker.php | 5 ---- PHPCI/Plugin/PhpLoc.php | 5 ---- PHPCI/Plugin/PhpMessDetector.php | 5 ---- PHPCI/Plugin/PhpParallelLint.php | 5 ---- PHPCI/Plugin/PhpSpec.php | 5 ---- PHPCI/Plugin/PhpUnit.php | 12 --------- PHPCI/Plugin/Xmpp.php | 7 +---- Tests/PHPCI/Helper/CommandExecutorTest.php | 19 ++++++++++++- 18 files changed, 46 insertions(+), 103 deletions(-) diff --git a/PHPCI/Builder.php b/PHPCI/Builder.php index b2a135f4..39e8ca41 100644 --- a/PHPCI/Builder.php +++ b/PHPCI/Builder.php @@ -259,12 +259,14 @@ class Builder implements LoggerAwareInterface /** * Find a binary required by a plugin. - * @param $binary + * @param string $binary + * @param bool $quiet + * * @return null|string */ - public function findBinary($binary) + public function findBinary($binary, $quiet = false) { - return $this->commandExecutor->findBinary($binary, $this->buildPath); + return $this->commandExecutor->findBinary($binary, $quiet = false); } /** diff --git a/PHPCI/Helper/BaseCommandExecutor.php b/PHPCI/Helper/BaseCommandExecutor.php index 5435d597..bd948834 100644 --- a/PHPCI/Helper/BaseCommandExecutor.php +++ b/PHPCI/Helper/BaseCommandExecutor.php @@ -9,9 +9,9 @@ namespace PHPCI\Helper; -use \PHPCI\Logging\BuildLogger; +use Exception; +use PHPCI\Logging\BuildLogger; use Psr\Log\LogLevel; -use PHPCI\Helper\Lang; /** * Handles running system commands with variables. @@ -20,7 +20,7 @@ use PHPCI\Helper\Lang; abstract class BaseCommandExecutor implements CommandExecutor { /** - * @var \PHPCI\Logging\BuildLogger + * @var BuildLogger */ protected $logger; @@ -144,13 +144,12 @@ abstract class BaseCommandExecutor implements CommandExecutor /** * Find a binary required by a plugin. * @param string $binary - * @param null $buildPath + * @param bool $quiet * @return null|string */ - public function findBinary($binary, $buildPath = null) + public function findBinary($binary, $quiet = false) { - $binaryPath = null; - $composerBin = $this->getComposerBinDir(realpath($buildPath)); + $composerBin = $this->getComposerBinDir(realpath($this->buildPath)); if (is_string($binary)) { $binary = array($binary); @@ -161,30 +160,30 @@ abstract class BaseCommandExecutor implements CommandExecutor if (is_dir($composerBin) && is_file($composerBin.'/'.$bin)) { $this->logger->log(Lang::get('found_in_path', $composerBin, $bin), LogLevel::DEBUG); - $binaryPath = $composerBin . '/' . $bin; - break; + return $composerBin . '/' . $bin; } if (is_file($this->rootDir . $bin)) { $this->logger->log(Lang::get('found_in_path', 'root', $bin), LogLevel::DEBUG); - $binaryPath = $this->rootDir . $bin; - break; + return $this->rootDir . $bin; } if (is_file($this->rootDir . 'vendor/bin/' . $bin)) { $this->logger->log(Lang::get('found_in_path', 'vendor/bin', $bin), LogLevel::DEBUG); - $binaryPath = $this->rootDir . 'vendor/bin/' . $bin; - break; + return $this->rootDir . 'vendor/bin/' . $bin; } $findCmdResult = $this->findGlobalBinary($bin); if (is_file($findCmdResult)) { $this->logger->log(Lang::get('found_in_path', '', $bin), LogLevel::DEBUG); - $binaryPath = $findCmdResult; - break; + return $findCmdResult; } } - return $binaryPath; + + if ($quiet) { + return; + } + throw new Exception(Lang::get('could_not_find', implode('/', $binary))); } /** diff --git a/PHPCI/Helper/CommandExecutor.php b/PHPCI/Helper/CommandExecutor.php index bacd1a2e..4f0028eb 100644 --- a/PHPCI/Helper/CommandExecutor.php +++ b/PHPCI/Helper/CommandExecutor.php @@ -26,10 +26,13 @@ interface CommandExecutor /** * Find a binary required by a plugin. * @param string $binary - * @param string $buildPath the current build path + * @param bool $quiet Returns null instead of throwing an execption. + * * @return null|string + * + * @throws \Exception when no binary has been found and $quiet is false. */ - public function findBinary($binary, $buildPath = null); + public function findBinary($binary, $quiet = false); /** * Set the buildPath property. diff --git a/PHPCI/Plugin/Codeception.php b/PHPCI/Plugin/Codeception.php index 7d7fd81e..9a651bec 100644 --- a/PHPCI/Plugin/Codeception.php +++ b/PHPCI/Plugin/Codeception.php @@ -133,12 +133,6 @@ class Codeception implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin } else { $codecept = $this->phpci->findBinary('codecept'); - if (!$codecept) { - $this->phpci->logFailure(Lang::get('could_not_find', 'codecept')); - - return false; - } - $cmd = 'cd "%s" && ' . $codecept . ' run -c "%s" --tap ' . $this->args; if (IS_WIN) { diff --git a/PHPCI/Plugin/Composer.php b/PHPCI/Plugin/Composer.php index 3930f3d6..87a558fe 100644 --- a/PHPCI/Plugin/Composer.php +++ b/PHPCI/Plugin/Composer.php @@ -81,11 +81,6 @@ class Composer implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin { $composerLocation = $this->phpci->findBinary(array('composer', 'composer.phar')); - if (!$composerLocation) { - $this->phpci->logFailure(Lang::get('could_not_find', 'composer')); - return false; - } - $cmd = ''; if (IS_WIN) { diff --git a/PHPCI/Plugin/Pdepend.php b/PHPCI/Plugin/Pdepend.php index d1039554..73b669c1 100644 --- a/PHPCI/Plugin/Pdepend.php +++ b/PHPCI/Plugin/Pdepend.php @@ -79,15 +79,10 @@ class Pdepend implements \PHPCI\Plugin $pdepend = $this->phpci->findBinary('pdepend'); - if (!$pdepend) { - $this->phpci->logFailure(Lang::get('could_not_find', 'pdepend')); - return false; - } - $cmd = $pdepend . ' --summary-xml="%s" --jdepend-chart="%s" --overview-pyramid="%s" %s "%s"'; $this->removeBuildArtifacts(); - + // If we need to ignore directories if (count($this->phpci->ignore)) { $ignore = ' --ignore=' . implode(',', $this->phpci->ignore); diff --git a/PHPCI/Plugin/Phing.php b/PHPCI/Plugin/Phing.php index 2bf64cca..3a5bd834 100644 --- a/PHPCI/Plugin/Phing.php +++ b/PHPCI/Plugin/Phing.php @@ -81,11 +81,6 @@ class Phing implements \PHPCI\Plugin { $phingExecutable = $this->phpci->findBinary('phing'); - if (!$phingExecutable) { - $this->phpci->logFailure(Lang::get('could_not_find', 'phing')); - return false; - } - $cmd[] = $phingExecutable . ' -f ' . $this->getBuildFilePath(); if ($this->getPropertyFile()) { diff --git a/PHPCI/Plugin/PhpCodeSniffer.php b/PHPCI/Plugin/PhpCodeSniffer.php index df276eac..877d24c6 100644 --- a/PHPCI/Plugin/PhpCodeSniffer.php +++ b/PHPCI/Plugin/PhpCodeSniffer.php @@ -149,11 +149,6 @@ class PhpCodeSniffer implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin $phpcs = $this->phpci->findBinary('phpcs'); - if (!$phpcs) { - $this->phpci->logFailure(PHPCI\Helper\Lang::get('could_not_find', 'phpcs')); - return false; - } - $this->phpci->logExecOutput(false); $cmd = $phpcs . ' --report=json %s %s %s %s %s "%s"'; diff --git a/PHPCI/Plugin/PhpCpd.php b/PHPCI/Plugin/PhpCpd.php index c663db5a..6433308f 100644 --- a/PHPCI/Plugin/PhpCpd.php +++ b/PHPCI/Plugin/PhpCpd.php @@ -90,18 +90,13 @@ class PhpCpd implements \PHPCI\Plugin $phpcpd = $this->phpci->findBinary('phpcpd'); - if (!$phpcpd) { - $this->phpci->logFailure(Lang::get('could_not_find', 'phpcpd')); - return false; - } - $tmpfilename = tempnam('/tmp', 'phpcpd'); $cmd = $phpcpd . ' --log-pmd "%s" %s "%s"'; $success = $this->phpci->executeCommand($cmd, $tmpfilename, $ignore, $this->path); print $this->phpci->getLastOutput(); - + list($errorCount, $data) = $this->processReport(file_get_contents($tmpfilename)); $this->build->storeMeta('phpcpd-warnings', $errorCount); $this->build->storeMeta('phpcpd-data', $data); diff --git a/PHPCI/Plugin/PhpCsFixer.php b/PHPCI/Plugin/PhpCsFixer.php index 1374bc71..e07db718 100644 --- a/PHPCI/Plugin/PhpCsFixer.php +++ b/PHPCI/Plugin/PhpCsFixer.php @@ -69,11 +69,6 @@ class PhpCsFixer implements \PHPCI\Plugin $phpcsfixer = $this->phpci->findBinary('php-cs-fixer'); - if (!$phpcsfixer) { - $this->phpci->logFailure(Lang::get('could_not_find', 'php-cs-fixer')); - return false; - } - $cmd = $phpcsfixer . ' fix . %s %s %s'; $success = $this->phpci->executeCommand($cmd, $this->verbose, $this->diff, $this->level); diff --git a/PHPCI/Plugin/PhpDocblockChecker.php b/PHPCI/Plugin/PhpDocblockChecker.php index 9f71afef..7fb0fe11 100644 --- a/PHPCI/Plugin/PhpDocblockChecker.php +++ b/PHPCI/Plugin/PhpDocblockChecker.php @@ -104,11 +104,6 @@ class PhpDocblockChecker implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin // Check that the binary exists: $checker = $this->phpci->findBinary('phpdoccheck'); - if (!$checker) { - $this->phpci->logFailure(PHPCI\Helper\Lang::get('could_not_find', 'phpdoccheck')); - return false; - } - // Build ignore string: $ignore = ''; if (count($this->ignore)) { diff --git a/PHPCI/Plugin/PhpLoc.php b/PHPCI/Plugin/PhpLoc.php index 65398e05..76887493 100644 --- a/PHPCI/Plugin/PhpLoc.php +++ b/PHPCI/Plugin/PhpLoc.php @@ -80,11 +80,6 @@ class PhpLoc implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin $phploc = $this->phpci->findBinary('phploc'); - if (!$phploc) { - $this->phpci->logFailure(PHPCI\Helper\Lang::get('could_not_find', 'phploc')); - return false; - } - $success = $this->phpci->executeCommand($phploc . ' %s "%s"', $ignore, $this->directory); $output = $this->phpci->getLastOutput(); diff --git a/PHPCI/Plugin/PhpMessDetector.php b/PHPCI/Plugin/PhpMessDetector.php index 21c171cb..b02f5f09 100644 --- a/PHPCI/Plugin/PhpMessDetector.php +++ b/PHPCI/Plugin/PhpMessDetector.php @@ -121,11 +121,6 @@ class PhpMessDetector implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin $phpmdBinaryPath = $this->phpci->findBinary('phpmd'); - if (!$phpmdBinaryPath) { - $this->phpci->logFailure(PHPCI\Helper\Lang::get('could_not_find', 'phpmd')); - return false; - } - $this->executePhpMd($phpmdBinaryPath); list($errorCount, $data) = $this->processReport(trim($this->phpci->getLastOutput())); diff --git a/PHPCI/Plugin/PhpParallelLint.php b/PHPCI/Plugin/PhpParallelLint.php index fdaeaf65..8b7528de 100644 --- a/PHPCI/Plugin/PhpParallelLint.php +++ b/PHPCI/Plugin/PhpParallelLint.php @@ -78,11 +78,6 @@ class PhpParallelLint implements \PHPCI\Plugin $phplint = $this->phpci->findBinary('parallel-lint'); - if (!$phplint) { - $this->phpci->logFailure(Lang::get('could_not_find', 'parallel-lint')); - return false; - } - $cmd = $phplint . ' %s "%s"'; $success = $this->phpci->executeCommand( $cmd, diff --git a/PHPCI/Plugin/PhpSpec.php b/PHPCI/Plugin/PhpSpec.php index 681a5a6c..e468a718 100644 --- a/PHPCI/Plugin/PhpSpec.php +++ b/PHPCI/Plugin/PhpSpec.php @@ -59,11 +59,6 @@ class PhpSpec implements PHPCI\Plugin $phpspec = $this->phpci->findBinary(array('phpspec', 'phpspec.php')); - if (!$phpspec) { - $this->phpci->logFailure(PHPCI\Helper\Lang::get('could_not_find', 'phpspec')); - return false; - } - $success = $this->phpci->executeCommand($phpspec . ' --format=junit --no-code-generation run'); $output = $this->phpci->getLastOutput(); diff --git a/PHPCI/Plugin/PhpUnit.php b/PHPCI/Plugin/PhpUnit.php index 70e0e74f..7fb626df 100644 --- a/PHPCI/Plugin/PhpUnit.php +++ b/PHPCI/Plugin/PhpUnit.php @@ -197,15 +197,8 @@ class PhpUnit implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin chdir($this->phpci->buildPath.'/'.$this->runFrom); } - $phpunit = $this->phpci->findBinary('phpunit'); - if (!$phpunit) { - $this->phpci->logFailure(PHPCI\Helper\Lang::get('could_not_find', 'phpunit')); - return false; - } - - $cmd = $phpunit . ' --tap %s -c "%s" ' . $this->coverage . $this->path; $success = $this->phpci->executeCommand($cmd, $this->args, $this->phpci->buildPath . $configPath); @@ -232,11 +225,6 @@ class PhpUnit implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin $phpunit = $this->phpci->findBinary('phpunit'); - if (!$phpunit) { - $this->phpci->logFailure(PHPCI\Helper\Lang::get('could_not_find', 'phpunit')); - return false; - } - $cmd = $phpunit . ' --tap %s "%s"'; $success = $this->phpci->executeCommand($cmd, $this->args, $this->phpci->buildPath . $directory); chdir($curdir); diff --git a/PHPCI/Plugin/Xmpp.php b/PHPCI/Plugin/Xmpp.php index add778cb..0362aa85 100644 --- a/PHPCI/Plugin/Xmpp.php +++ b/PHPCI/Plugin/Xmpp.php @@ -148,12 +148,7 @@ class XMPP implements \PHPCI\Plugin */ public function execute() { - $sendxmpp = $this->phpci->findBinary('/usr/bin/sendxmpp'); - - if (!$sendxmpp) { - $this->phpci->logFailure('Could not find sendxmpp.'); - return false; - } + $sendxmpp = $this->phpci->findBinary('sendxmpp'); /* * Without recipients we can't send notification diff --git a/Tests/PHPCI/Helper/CommandExecutorTest.php b/Tests/PHPCI/Helper/CommandExecutorTest.php index 4cf14887..5d5dc08b 100644 --- a/Tests/PHPCI/Helper/CommandExecutorTest.php +++ b/Tests/PHPCI/Helper/CommandExecutorTest.php @@ -27,7 +27,8 @@ class CommandExecutorTest extends \PHPUnit_Framework_TestCase } parent::setUp(); $mockBuildLogger = $this->prophesize('PHPCI\Logging\BuildLogger'); - $this->testedExecutor = new UnixCommandExecutor($mockBuildLogger->reveal(), __DIR__ . "/"); + $class = IS_WIN ? 'PHPCI\Helper\WindowsCommandExecutor' : 'PHPCI\Helper\UnixCommandExecutor'; + $this->testedExecutor = new $class($mockBuildLogger->reveal(), __DIR__ . "/"); } public function testGetLastOutput_ReturnsOutputOfCommand() @@ -63,4 +64,20 @@ class CommandExecutorTest extends \PHPUnit_Framework_TestCase $returnValue = $this->testedExecutor->findBinary($thisFileName); $this->assertEquals(__DIR__ . "/" . $thisFileName, $returnValue); } + + /** + * @expectedException \Exception + * @expectedMessageRegex WorldWidePeace + */ + public function testFindBinary_ThrowsWhenNotFound() + { + $thisFileName = "WorldWidePeace"; + $this->testedExecutor->findBinary($thisFileName); + } + + public function testFindBinary_ReturnsNullWihQuietArgument() + { + $thisFileName = "WorldWidePeace"; + $this->assertNull($this->testedExecutor->findBinary($thisFileName, true)); + } } From 290c34a27da14b8632bcc3a2a5cc05e68f0ab3b3 Mon Sep 17 00:00:00 2001 From: Adirelle Date: Tue, 14 Apr 2015 09:43:21 +0200 Subject: [PATCH 186/329] Updated the php_codesniffer required version and added a default phpcs.xml. Added a PHPMD configuration file. Updated phpci.yml to use the configuration files. Close #913 --- composer.json | 2 +- composer.lock | 18 ++++++++++++------ phpci.yml | 4 +++- phpcs.xml | 19 +++++++++++++++++++ phpmd.xml | 17 +++++++++++++++++ 5 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 phpcs.xml create mode 100644 phpmd.xml diff --git a/composer.json b/composer.json index 03d752a5..c6d8e92b 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ "require-dev": { "phpunit/phpunit": "~4.5", "phpmd/phpmd": "~2.0", - "squizlabs/php_codesniffer": "~2.0", + "squizlabs/php_codesniffer": "~2.3", "block8/php-docblock-checker": "~1.0", "phploc/phploc": "~2.0" }, diff --git a/composer.lock b/composer.lock index 60b54a4e..dbbedd22 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "0c9efd3a0b9e924c7465e03fc97ffed7", + "hash": "a4a95f2b83e336b9e1b285c04af26ce7", "packages": [ { "name": "block8/b8framework", @@ -1851,20 +1851,21 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "2.2.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "b301c98f19414d836fdaa678648745fcca5aeb4f" + "reference": "5046b0e01c416fc2b06df961d0673c85bcdc896c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/b301c98f19414d836fdaa678648745fcca5aeb4f", - "reference": "b301c98f19414d836fdaa678648745fcca5aeb4f", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5046b0e01c416fc2b06df961d0673c85bcdc896c", + "reference": "5046b0e01c416fc2b06df961d0673c85bcdc896c", "shasum": "" }, "require": { "ext-tokenizer": "*", + "ext-xmlwriter": "*", "php": ">=5.1.2" }, "bin": [ @@ -1872,6 +1873,11 @@ "scripts/phpcbf" ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "classmap": [ "CodeSniffer.php", @@ -1915,7 +1921,7 @@ "phpcs", "standards" ], - "time": "2015-01-21 22:44:05" + "time": "2015-03-04 02:07:03" }, { "name": "symfony/dependency-injection", diff --git a/phpci.yml b/phpci.yml index 8f51c88f..e794939e 100644 --- a/phpci.yml +++ b/phpci.yml @@ -21,8 +21,10 @@ test: - vendor/ php_mess_detector: allowed_warnings: 0 + rules: + - phpmd.xml php_code_sniffer: - standard: "PSR2" + standard: phpcs.xml allowed_warnings: 0 allowed_errors: 0 php_loc: diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 00000000..a392a201 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,19 @@ + + + + Codestyle ruleset for PHPCI + + + + PHPCI + + + + + PHPCI/Migrations/* + PHPCI/Model/Base/* + PHPCI/Languages/* + Tests/* + vendor/* + + diff --git a/phpmd.xml b/phpmd.xml new file mode 100644 index 00000000..69b822fd --- /dev/null +++ b/phpmd.xml @@ -0,0 +1,17 @@ + + + + PHPCI rule set + + + + + + + From 81fbc6a5a0d4e4ae693f209a7bbb4a7882577310 Mon Sep 17 00:00:00 2001 From: rm3nchaca Date: Wed, 15 Apr 2015 15:17:06 -0500 Subject: [PATCH 187/329] fix file link in plugins Running builds leave a file link with an error like "http://gitlab.example.com/root/project/blob/master/index.php#L6" but it is pointing to the actual file, not the file with a bug, example "http://gitlab.example.com/root/project/blob/97f0a6453d5913f4b55d660fbf2d629240ca7237/index.php#L6" Close #915 --- PHPCI/Model/Build/GitlabBuild.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/Model/Build/GitlabBuild.php b/PHPCI/Model/Build/GitlabBuild.php index 086bc046..a1e0562c 100644 --- a/PHPCI/Model/Build/GitlabBuild.php +++ b/PHPCI/Model/Build/GitlabBuild.php @@ -47,7 +47,7 @@ class GitlabBuild extends RemoteGitBuild 'http://%s/%s/blob/%s/{FILE}#L{LINE}', $this->getProject()->getAccessInformation("domain"), $this->getProject()->getReference(), - $this->getBranch() + $this->getCommitId() ); } From f7ca64bf6de55d6556225c7207e46fe35ecdbf12 Mon Sep 17 00:00:00 2001 From: Adam Henley Date: Sun, 19 Apr 2015 20:32:55 +1200 Subject: [PATCH 188/329] SMTP Password not masked PR #921 Signed-off-by: Adam Henley Close #923 --- PHPCI/Controller/SettingsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PHPCI/Controller/SettingsController.php b/PHPCI/Controller/SettingsController.php index aaaa637f..ea15bc3b 100644 --- a/PHPCI/Controller/SettingsController.php +++ b/PHPCI/Controller/SettingsController.php @@ -329,7 +329,7 @@ class SettingsController extends Controller $field->setContainerClass('form-group'); $form->addField($field); - $field = new Form\Element\Text('smtp_password'); + $field = new Form\Element\Password('smtp_password'); $field->setRequired(false); $field->setLabel(Lang::get('smtp_password')); $field->setClass('form-control'); From c0568d3a4b93b104061c3ad6216b693068b35a92 Mon Sep 17 00:00:00 2001 From: Alexander Garzon Date: Tue, 21 Apr 2015 18:52:42 -0400 Subject: [PATCH 189/329] Update lang.es.php Missing translation for "archived" Close #931 --- PHPCI/Languages/lang.es.php | 1 + 1 file changed, 1 insertion(+) diff --git a/PHPCI/Languages/lang.es.php b/PHPCI/Languages/lang.es.php index 7c9f5a62..95a6eb73 100644 --- a/PHPCI/Languages/lang.es.php +++ b/PHPCI/Languages/lang.es.php @@ -114,6 +114,7 @@ PHPCI', (en caso que no puedas agregar el archivo phpci.yml al repositorio)', 'default_branch' => 'Nombre de la rama por defecto', 'allow_public_status' => '¿Activar página pública con el estado del proyecto?', + 'archived' => 'Archivado', 'save_project' => 'Guardar Proyecto', 'error_mercurial' => 'La URL del repositorio de Mercurial debe comenzar con http:// or https://', From 1b7f0bbb4b79500b45dedf768e332f9cf187937a Mon Sep 17 00:00:00 2001 From: corpsee Date: Tue, 17 Mar 2015 15:11:05 +0600 Subject: [PATCH 190/329] Improved login: now you can login using name or email Close #873 --- PHPCI/Controller/SessionController.php | 4 +-- PHPCI/Languages/lang.da.php | 1 + PHPCI/Languages/lang.de.php | 1 + PHPCI/Languages/lang.el.php | 1 + PHPCI/Languages/lang.en.php | 1 + PHPCI/Languages/lang.fr.php | 1 + PHPCI/Languages/lang.it.php | 1 + PHPCI/Languages/lang.nl.php | 1 + PHPCI/Languages/lang.pl.php | 1 + PHPCI/Languages/lang.ru.php | 1 + PHPCI/Languages/lang.uk.php | 1 + ...4958_unique_email_and_name_user_fields.php | 30 +++++++++++++++++++ PHPCI/Store/Base/UserStoreBase.php | 28 ++++++++++++++++- 13 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 PHPCI/Migrations/20150324174958_unique_email_and_name_user_fields.php diff --git a/PHPCI/Controller/SessionController.php b/PHPCI/Controller/SessionController.php index eb7001df..a5f40ffb 100644 --- a/PHPCI/Controller/SessionController.php +++ b/PHPCI/Controller/SessionController.php @@ -49,7 +49,7 @@ class SessionController extends \PHPCI\Controller } else { unset($_SESSION['login_token']); - $user = $this->userStore->getByEmail($this->getParam('email')); + $user = $this->userStore->getByLoginOrEmail($this->getParam('email')); if ($user && password_verify($this->getParam('password', ''), $user->getHash())) { session_regenerate_id(true); @@ -68,7 +68,7 @@ class SessionController extends \PHPCI\Controller $form->setAction(PHPCI_URL.'session/login'); $email = new b8\Form\Element\Email('email'); - $email->setLabel(Lang::get('email_address')); + $email->setLabel(Lang::get('login')); $email->setRequired(true); $email->setContainerClass('form-group'); $email->setClass('form-control'); diff --git a/PHPCI/Languages/lang.da.php b/PHPCI/Languages/lang.da.php index b65f9ef9..20e403e1 100644 --- a/PHPCI/Languages/lang.da.php +++ b/PHPCI/Languages/lang.da.php @@ -39,6 +39,7 @@ PHPCI', 'reset_email_title' => 'PHPCI Adgangskode-nulstilling for %s', 'reset_invalid' => 'Ugyldig anmodning om adgangskode-nulstilling.', 'email_address' => 'Email-addresse', + 'login' => 'Login / Email Address', 'password' => 'Adgangskode', 'log_in' => 'Log ind', diff --git a/PHPCI/Languages/lang.de.php b/PHPCI/Languages/lang.de.php index 393459a1..8a08e37d 100644 --- a/PHPCI/Languages/lang.de.php +++ b/PHPCI/Languages/lang.de.php @@ -39,6 +39,7 @@ PHPCI', 'reset_email_title' => 'PHPCI Passwort zurücksetzen für %s', 'reset_invalid' => 'Fehlerhafte Anfrage für das Zurücksetzen eines Passwortes', 'email_address' => 'Emailadresse', + 'login' => 'Login / Email Address', 'password' => 'Passwort', 'log_in' => 'Einloggen', diff --git a/PHPCI/Languages/lang.el.php b/PHPCI/Languages/lang.el.php index e97da6d3..3e3a3b84 100644 --- a/PHPCI/Languages/lang.el.php +++ b/PHPCI/Languages/lang.el.php @@ -39,6 +39,7 @@ PHPCI', 'reset_email_title' => 'PHPCI Επαναφορά Κωδικού για %s', 'reset_invalid' => 'Μη έγκυρο αίτημα επαναφοράς κωδικού πρόσβασης.', 'email_address' => 'Διεύθυνση email', + 'login' => 'Login / Email Address', 'password' => 'Κωδικός πρόσβασης', 'log_in' => 'Είσοδος', diff --git a/PHPCI/Languages/lang.en.php b/PHPCI/Languages/lang.en.php index 19d41cbe..d11a8b39 100644 --- a/PHPCI/Languages/lang.en.php +++ b/PHPCI/Languages/lang.en.php @@ -39,6 +39,7 @@ PHPCI', 'reset_email_title' => 'PHPCI Password Reset for %s', 'reset_invalid' => 'Invalid password reset request.', 'email_address' => 'Email Address', + 'login' => 'Login / Email Address', 'password' => 'Password', 'log_in' => 'Log in', diff --git a/PHPCI/Languages/lang.fr.php b/PHPCI/Languages/lang.fr.php index a8de5991..f1ad7129 100644 --- a/PHPCI/Languages/lang.fr.php +++ b/PHPCI/Languages/lang.fr.php @@ -39,6 +39,7 @@ PHPCI', 'reset_email_title' => 'Réinitialisation du mot de passe PHPCI pour %s', 'reset_invalid' => 'Requête de réinitialisation de mot de passe invalide.', 'email_address' => 'Adresse email', + 'login' => 'Login / Email Address', 'password' => 'Mot de passe', 'log_in' => 'Connexion', diff --git a/PHPCI/Languages/lang.it.php b/PHPCI/Languages/lang.it.php index f8f41ea8..ff326d14 100644 --- a/PHPCI/Languages/lang.it.php +++ b/PHPCI/Languages/lang.it.php @@ -39,6 +39,7 @@ PHPCI', 'reset_email_title' => 'Ripristino della password di PHPCI per %s', 'reset_invalid' => 'Richeista di ripristino password non valida.', 'email_address' => 'Indirizzo Email', + 'login' => 'Login / Email Address', 'password' => 'Password', 'log_in' => 'Accedi', diff --git a/PHPCI/Languages/lang.nl.php b/PHPCI/Languages/lang.nl.php index f3c0a7c5..e0638aaf 100644 --- a/PHPCI/Languages/lang.nl.php +++ b/PHPCI/Languages/lang.nl.php @@ -39,6 +39,7 @@ PHPCI', 'reset_email_title' => 'PHPCI wachtwoord reset voor %s', 'reset_invalid' => 'Ongeldig wachtwoord reset verzoek', 'email_address' => 'E-mailadres', + 'login' => 'Login / Email Address', 'password' => 'Wachtwoord', 'log_in' => 'Log in', diff --git a/PHPCI/Languages/lang.pl.php b/PHPCI/Languages/lang.pl.php index 37a78fb6..d6f95ad6 100644 --- a/PHPCI/Languages/lang.pl.php +++ b/PHPCI/Languages/lang.pl.php @@ -39,6 +39,7 @@ PHPCI', 'reset_email_title' => 'Reset Hasła PHPCI dla %s', 'reset_invalid' => 'Prośba o zmianę hasła jest nieważna.', 'email_address' => 'Adres email', + 'login' => 'Login / Email Address', 'password' => 'Hasło', 'log_in' => 'Zaloguj się', diff --git a/PHPCI/Languages/lang.ru.php b/PHPCI/Languages/lang.ru.php index 7627db85..a13c4a18 100644 --- a/PHPCI/Languages/lang.ru.php +++ b/PHPCI/Languages/lang.ru.php @@ -38,6 +38,7 @@ PHPCI', 'reset_email_title' => 'Сброс пароля PHPCI для %s', 'reset_invalid' => 'Некорректный запрос на сброс пароля.', 'email_address' => 'Email', + 'login' => 'Логин / Email', 'password' => 'Пароль', 'log_in' => 'Войти', diff --git a/PHPCI/Languages/lang.uk.php b/PHPCI/Languages/lang.uk.php index 2b2cc027..a3ca1b37 100644 --- a/PHPCI/Languages/lang.uk.php +++ b/PHPCI/Languages/lang.uk.php @@ -39,6 +39,7 @@ PHPCI', 'reset_email_title' => 'Скидання пароль PHPCI для %s', 'reset_invalid' => 'Невірний запит скидання паролю.', 'email_address' => 'Email адреса', + 'login' => 'Логин / Email адреса', 'password' => 'Пароль', 'log_in' => 'Увійти', diff --git a/PHPCI/Migrations/20150324174958_unique_email_and_name_user_fields.php b/PHPCI/Migrations/20150324174958_unique_email_and_name_user_fields.php new file mode 100644 index 00000000..f8d24ba0 --- /dev/null +++ b/PHPCI/Migrations/20150324174958_unique_email_and_name_user_fields.php @@ -0,0 +1,30 @@ +table('user'); + $user_table + ->addIndex('email', array('unique' => true)) + ->addIndex('name', array('unique' => true)) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + $user_table = $this->table('user'); + $user_table + ->removeIndex('email', array('unique' => true)) + ->removeIndex('name', array('unique' => true)) + ->save(); + } +} diff --git a/PHPCI/Store/Base/UserStoreBase.php b/PHPCI/Store/Base/UserStoreBase.php index d91271d0..f2893ac5 100644 --- a/PHPCI/Store/Base/UserStoreBase.php +++ b/PHPCI/Store/Base/UserStoreBase.php @@ -59,7 +59,7 @@ class UserStoreBase extends Store /** * Returns a User model by Email. - * @param mixed $value + * @param string $value * @param string $useConnection * @throws HttpException * @return \@appNamespace\Model\User|null @@ -82,4 +82,30 @@ class UserStoreBase extends Store return null; } + + /** + * Returns a User model by Email. + * @param string $value + * @param string $useConnection + * @throws HttpException + * @return \@appNamespace\Model\User|null + */ + public function getByLoginOrEmail($value, $useConnection = 'read') + { + if (is_null($value)) { + throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); + } + + $query = 'SELECT * FROM `user` WHERE `name` = :value OR `email` = :value LIMIT 1'; + $stmt = Database::getConnection($useConnection)->prepare($query); + $stmt->bindValue(':value', $value); + + if ($stmt->execute()) { + if ($data = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return new User($data); + } + } + + return null; + } } From 634b246ed5eb4b395fecd9a282423190d9232168 Mon Sep 17 00:00:00 2001 From: corpsee Date: Wed, 22 Apr 2015 16:24:03 +0600 Subject: [PATCH 191/329] Fixed ru strings for create build command (https://github.com/Block8/PHPCI/pull/889) Fixed ru strings for 'archived' Close #932 --- PHPCI/Languages/lang.ru.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/PHPCI/Languages/lang.ru.php b/PHPCI/Languages/lang.ru.php index a13c4a18..aa0f40fb 100644 --- a/PHPCI/Languages/lang.ru.php +++ b/PHPCI/Languages/lang.ru.php @@ -113,7 +113,7 @@ PHPCI', (если вы не добавили файл phpci.yml в репозиторий вашего проекта)', 'default_branch' => 'Ветка по умолчанию', 'allow_public_status' => 'Разрешить публичный статус и изображение (статуса) для проекта', - 'archived' => 'Archived', + 'archived' => 'Запакован', 'save_project' => 'Сохранить проект', 'error_mercurial' => 'URL репозитория Mercurial должен начинаться с http:// или https://', @@ -338,10 +338,10 @@ PHPCI', 'incorrect_format' => 'Неверный формат', // Create Build Command - 'create_build_project' => 'Create a build for a project', - 'project_id_argument' => 'A project ID', - 'commit_id_option' => 'Commit ID to build', - 'branch_name_option' => 'Branch to build', + 'create_build_project' => 'Создать сборку проекта', + 'project_id_argument' => 'ID проекта', + 'commit_id_option' => 'ID коммита для сборки', + 'branch_name_option' => 'Ветка для сборки', // Run Command 'run_all_pending' => 'Запустить все ожидающие PHPCI сборки.', From 209454c5f67e9fa8d1042b2410cd8350eeb4fcb7 Mon Sep 17 00:00:00 2001 From: Adirelle Date: Tue, 21 Apr 2015 14:51:55 +0200 Subject: [PATCH 192/329] When starting a manual build, replace the "Manual" commit id with the HEAD hash. Close #928 --- PHPCI/Model/Build/RemoteGitBuild.php | 16 ++++++++-------- PHPCI/Service/BuildService.php | 2 ++ Tests/PHPCI/Service/BuildServiceTest.php | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/PHPCI/Model/Build/RemoteGitBuild.php b/PHPCI/Model/Build/RemoteGitBuild.php index 5d0c9419..faaccfb5 100644 --- a/PHPCI/Model/Build/RemoteGitBuild.php +++ b/PHPCI/Model/Build/RemoteGitBuild.php @@ -124,16 +124,16 @@ class RemoteGitBuild extends Build $success = true; $commit = $this->getCommitId(); + $chdir = IS_WIN ? 'cd /d "%s"' : 'cd "%s"'; + if (!empty($commit) && $commit != 'Manual') { - $cmd = 'cd "%s"'; + $cmd = $chdir . ' && git checkout %s --quiet'; + $success = $builder->executeCommand($cmd, $cloneTo, $commit); + } - if (IS_WIN) { - $cmd = 'cd /d "%s"'; - } - - $cmd .= ' && git checkout %s --quiet'; - - $success = $builder->executeCommand($cmd, $cloneTo, $this->getCommitId()); + // Always update the commit hash with the actual HEAD hash + if ($builder->executeCommand($chdir . ' && git rev-parse HEAD', $cloneTo)) { + $this->setCommitId(trim($builder->getLastOutput())); } return $success; diff --git a/PHPCI/Service/BuildService.php b/PHPCI/Service/BuildService.php index a95ac44f..ca2fae4d 100644 --- a/PHPCI/Service/BuildService.php +++ b/PHPCI/Service/BuildService.php @@ -9,6 +9,7 @@ namespace PHPCI\Service; +use PHPCI\Helper\Lang; use PHPCI\Model\Build; use PHPCI\Model\Project; use PHPCI\Store\BuildStore; @@ -59,6 +60,7 @@ class BuildService $build->setCommitId($commitId); } else { $build->setCommitId('Manual'); + $build->setCommitMessage(Lang::get('manual_build')); } if (!is_null($branch)) { diff --git a/Tests/PHPCI/Service/BuildServiceTest.php b/Tests/PHPCI/Service/BuildServiceTest.php index 426845a9..985f3088 100644 --- a/Tests/PHPCI/Service/BuildServiceTest.php +++ b/Tests/PHPCI/Service/BuildServiceTest.php @@ -57,7 +57,7 @@ class BuildServiceTest extends \PHPUnit_Framework_TestCase $this->assertNull($returnValue->getStarted()); $this->assertNull($returnValue->getFinished()); $this->assertNull($returnValue->getLog()); - $this->assertEmpty($returnValue->getCommitMessage()); + $this->assertEquals(\PHPCI\Helper\Lang::get('manual_build'), $returnValue->getCommitMessage()); $this->assertNull($returnValue->getCommitterEmail()); $this->assertNull($returnValue->getExtra()); $this->assertEquals('master', $returnValue->getBranch()); From 408eb5b974cb1af734053d78822621bc06ddd843 Mon Sep 17 00:00:00 2001 From: Adam Cooper Date: Mon, 8 Sep 2014 11:45:19 +0100 Subject: [PATCH 193/329] An attempt at making the codeception plugin a little more complete. Codeception JS plugin and theme changes. Improvements to the display. Extra total information plus some test file locations. Close #588 --- PHPCI/Languages/lang.el.php | 1 + PHPCI/Languages/lang.en.php | 6 + PHPCI/Plugin/Codeception.php | 178 ++++++++++-------- .../Util/TestResultParsers/Codeception.php | 108 +++++++++++ .../TestResultParsers/ParserInterface.php | 16 ++ public/assets/css/bootstrap-theme.min.css | 4 +- public/assets/css/bootstrap.min.css | 4 +- public/assets/js/bootstrap.min.js | 8 +- public/assets/js/build-plugins/codeception.js | 72 +++++-- 9 files changed, 294 insertions(+), 103 deletions(-) create mode 100644 PHPCI/Plugin/Util/TestResultParsers/Codeception.php create mode 100644 PHPCI/Plugin/Util/TestResultParsers/ParserInterface.php diff --git a/PHPCI/Languages/lang.el.php b/PHPCI/Languages/lang.el.php index 3e3a3b84..cb7d169f 100644 --- a/PHPCI/Languages/lang.el.php +++ b/PHPCI/Languages/lang.el.php @@ -172,6 +172,7 @@ Services του Bitbucket αποθετηρίου σας.', 'codeception_errors' => 'Λάθη Codeception', 'phpmd_warnings' => 'Προειδοποιήσεις PHPMD', 'phpcs_warnings' => 'Προειδοποιήσεις PHPCS ', + 'codeception_errors' => 'Λάθη Codeception', 'phpcs_errors' => 'Λάθη PHPCS', 'phplint_errors' => 'Λάθη Lint', 'phpunit_errors' => 'Λάθη PHPUnit ', diff --git a/PHPCI/Languages/lang.en.php b/PHPCI/Languages/lang.en.php index d11a8b39..d6c48754 100644 --- a/PHPCI/Languages/lang.en.php +++ b/PHPCI/Languages/lang.en.php @@ -190,6 +190,12 @@ PHPCI', 'technical_debt' => 'Technical Debt', 'behat' => 'Behat', + 'codeception_feature' => 'Feature', + 'codeception_suite' => 'Suite', + 'codeception_time' => 'Time', + 'codeception_synopsis' => '%1$d tests carried out in %2$f seconds. + %3$d failures.', + 'file' => 'File', 'line' => 'Line', 'class' => 'Class', diff --git a/PHPCI/Plugin/Codeception.php b/PHPCI/Plugin/Codeception.php index 9a651bec..fe12db76 100644 --- a/PHPCI/Plugin/Codeception.php +++ b/PHPCI/Plugin/Codeception.php @@ -9,50 +9,76 @@ namespace PHPCI\Plugin; -use PHPCI; use PHPCI\Builder; use PHPCI\Helper\Lang; use PHPCI\Model\Build; -use PHPCI\Plugin\Util\TapParser; +use PHPCI\Plugin\Util\TestResultParsers\Codeception as Parser; +use Psr\Log\LogLevel; /** - * Codeception Plugin - Enables full acceptance, unit, and functional testing - * + * Codeception Plugin - Enables full acceptance, unit, and functional testing. * @author Don Gilbert * @author Igor Timoshenko + * @author Adam Cooper * @package PHPCI * @subpackage Plugins */ -class Codeception implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin +class Codeception implements \PHPCI\Plugin, \PHPCI\ZeroConfigPlugin { - /** - * @var string - */ + /** @var string */ protected $args = ''; - /** - * @var Build - */ + /** @var Builder */ + protected $phpci; + + /** @var Build */ protected $build; /** - * @var Builder + * @var string $ymlConfigFile The path of a yml config for Codeception */ - protected $phpci; + protected $ymlConfigFile; /** - * @var string|string[] The path (or array of paths) of an yml config for Codeception + * @var string $path The path to the codeception tests folder. */ - protected $configFile; + protected $path; /** - * @var string The path where the reports and logs are stored + * @param $stage + * @param Builder $builder + * @param Build $build + * @return bool */ - protected $logPath = 'tests/_output'; + public static function canExecute($stage, Builder $builder, Build $build) + { + if ($stage == 'test' && !is_null(self::findConfigFile($builder->buildPath))) { + return true; + } + + return false; + } + + /** + * Try and find the codeception YML config file. + * @param $buildPath + * @return null|string + */ + public static function findConfigFile($buildPath) + { + if (file_exists($buildPath . 'codeception.yml')) { + return 'codeception.yml'; + } + + if (file_exists($buildPath . 'codeception.dist.yml')) { + return 'codeception.dist.yml'; + } + + return null; + } /** * Set up the plugin, configure options, etc. - * * @param Builder $phpci * @param Build $build * @param array $options @@ -61,104 +87,96 @@ class Codeception implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin { $this->phpci = $phpci; $this->build = $build; + $this->path = 'tests/'; - if (isset($options['config'])) { - $this->configFile = $options['config']; + if (empty($options['config'])) { + $this->ymlConfigFile = self::findConfigFile($this->phpci->buildPath); + } + if (isset($options['config'])) { + $this->ymlConfigFile = $options['config']; } - if (isset($options['args'])) { $this->args = (string) $options['args']; } - - if (isset($options['log_path'])) { - $this->logPath = $options['log_path']; + if (isset($options['path'])) { + $this->path = $options['path']; } } /** - * {@inheritDoc} + * Runs Codeception tests, optionally using specified config file(s). */ public function execute() { + if (empty($this->ymlConfigFile)) { + $this->phpci->logFailure('No configuration file found'); + return false; + } + $success = true; - $this->phpci->logExecOutput(false); - - // Run any config files first. This can be either a single value or an array - if ($this->configFile !== null) { - $success &= $this->runConfigFile($this->configFile); - } - - $tapString = file_get_contents( - $this->phpci->buildPath . $this->logPath . DIRECTORY_SEPARATOR . 'report.tap.log' - ); - - try { - $tapParser = new TapParser($tapString); - $output = $tapParser->parse(); - } catch (\Exception $ex) { - $this->phpci->logFailure($tapString); - - throw $ex; - } - - $failures = $tapParser->getTotalFailures(); - - $this->build->storeMeta('codeception-errors', $failures); - $this->build->storeMeta('codeception-data', $output); - - $this->phpci->logExecOutput(true); + // Run any config files first. This can be either a single value or an array. + $success &= $this->runConfigFile($this->ymlConfigFile); return $success; } /** - * {@inheritDoc} - */ - public static function canExecute($stage, Builder $builder, Build $build) - { - return $stage === 'test'; - } - - /** - * Run tests from a Codeception config file - * - * @param string $configPath + * Run tests from a Codeception config file. + * @param $configPath * @return bool|mixed + * @throws \Exception */ protected function runConfigFile($configPath) { if (is_array($configPath)) { return $this->recurseArg($configPath, array($this, 'runConfigFile')); } else { + $this->phpci->logExecOutput(false); + $codecept = $this->phpci->findBinary('codecept'); - $cmd = 'cd "%s" && ' . $codecept . ' run -c "%s" --tap ' . $this->args; + if (!$codecept) { + $this->phpci->logFailure(Lang::get('could_not_find', 'codecept')); + return false; + } + + $cmd = 'cd "%s" && ' . $codecept . ' run -c "%s" --xml ' . $this->args; if (IS_WIN) { - $cmd = 'cd /d "%s" && ' . $codecept . ' run -c "%s" --tap ' . $this->args; + $cmd = 'cd /d "%s" && ' . $codecept . ' run -c "%s" --xml ' . $this->args; } $configPath = $this->phpci->buildPath . $configPath; $success = $this->phpci->executeCommand($cmd, $this->phpci->buildPath, $configPath); + + $this->phpci->log( + 'Codeception XML path: '. $this->phpci->buildPath . $this->path . '_output/report.xml', + Loglevel::DEBUG + ); + $xml = file_get_contents($this->phpci->buildPath . $this->path . '_output/report.xml', false); + + try { + $parser = new Parser($this->phpci, $xml); + $output = $parser->parse(); + } catch (\Exception $ex) { + throw $ex; + } + + $meta = array( + 'tests' => $parser->getTotalTests(), + 'timetaken' => $parser->getTotalTimeTaken(), + 'failures' => $parser->getTotalFailures() + ); + + $this->build->storeMeta('codeception-meta', $meta); + $this->build->storeMeta('codeception-data', $output); + $this->build->storeMeta('codeception-errors', $parser->getTotalFailures()); + + $this->phpci->logExecOutput(true); + return $success; } } - - /** - * @param array $array - * @param \Callback $callable - * @return bool|mixed - */ - protected function recurseArg(array $array, $callable) - { - $success = true; - - foreach ($array as $subItem) { - $success &= call_user_func($callable, $subItem); - } - - return $success; - } } diff --git a/PHPCI/Plugin/Util/TestResultParsers/Codeception.php b/PHPCI/Plugin/Util/TestResultParsers/Codeception.php new file mode 100644 index 00000000..39f1ce6f --- /dev/null +++ b/PHPCI/Plugin/Util/TestResultParsers/Codeception.php @@ -0,0 +1,108 @@ + + * @package PHPCI\Plugin\Util\TestResultParsers + */ +class Codeception implements ParserInterface +{ + protected $phpci; + protected $resultsXml; + + protected $results; + + protected $totalTests; + protected $totalTimeTaken; + protected $totalFailures; + + /** + * @param Builder $phpci + * @param $resultsXml + */ + public function __construct(Builder $phpci, $resultsXml) + { + $this->phpci = $phpci; + $this->resultsXml = $resultsXml; + + $this->totalTests = 0; + } + + /** + * @return array An array of key/value pairs for storage in the plugins result metadata + */ + public function parse() + { + $rtn = array(); + + $this->results = new \SimpleXMLElement($this->resultsXml); + + // calculate total results + foreach ($this->results->testsuite as $testsuite) { + $this->totalTests += (int) $testsuite['tests']; + $this->totalTimeTaken += (float) $testsuite['time']; + $this->totalFailures += (int) $testsuite['failures']; + + foreach ($testsuite->testcase as $testcase) { + $testresult = array( + 'suite' => (string) $testsuite['name'], + 'file' => str_replace($this->phpci->buildPath, '/', (string) $testcase['file']), + 'name' => (string) $testcase['name'], + 'feature' => (string) $testcase['feature'], + 'assertions' => (int) $testcase['assertions'], + 'time' => (float) $testcase['time'] + ); + + if (isset($testcase['class'])) { + $testresult['class'] = (string) $testcase['class']; + } + + if (isset($testcase->failure)) { + $testresult['pass'] = false; + $testresult['message'] = (string) $testcase->failure; + } else { + $testresult['pass'] = true; + } + + $rtn[] = $testresult; + } + } + + return $rtn; + } + + /** + * Get the total number of tests performed. + * + * @return int + */ + public function getTotalTests() + { + return $this->totalTests; + } + + /** + * The time take to complete all tests + * + * @return mixed + */ + public function getTotalTimeTaken() + { + return $this->totalTimeTaken; + } + + /** + * A count of the test failures + * + * @return mixed + */ + public function getTotalFailures() + { + return $this->totalFailures; + } +} diff --git a/PHPCI/Plugin/Util/TestResultParsers/ParserInterface.php b/PHPCI/Plugin/Util/TestResultParsers/ParserInterface.php new file mode 100644 index 00000000..d0c77cb7 --- /dev/null +++ b/PHPCI/Plugin/Util/TestResultParsers/ParserInterface.php @@ -0,0 +1,16 @@ +li>a:hover,.dropdown-menu>li>a:focus{background-image:-webkit-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%);background-image:linear-gradient(to bottom, #f5f5f5 0, #e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-color:#e8e8e8}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-image:-webkit-linear-gradient(top, #428bca 0, #357ebd 100%);background-image:linear-gradient(to bottom, #428bca 0, #357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-color:#357ebd}.navbar-default{background-image:-webkit-linear-gradient(top, #fff 0, #f8f8f8 100%);background-image:linear-gradient(to bottom, #fff 0, #f8f8f8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top, #ebebeb 0, #f3f3f3 100%);background-image:linear-gradient(to bottom, #ebebeb 0, #f3f3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.075);box-shadow:inset 0 3px 9px rgba(0,0,0,0.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top, #3c3c3c 0, #222 100%);background-image:linear-gradient(to bottom, #3c3c3c 0, #222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top, #222 0, #282828 100%);background-image:linear-gradient(to bottom, #222 0, #282828 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.25);box-shadow:inset 0 3px 9px rgba(0,0,0,0.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-linear-gradient(top, #dff0d8 0, #c8e5bc 100%);background-image:linear-gradient(to bottom, #dff0d8 0, #c8e5bc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top, #d9edf7 0, #b9def0 100%);background-image:linear-gradient(to bottom, #d9edf7 0, #b9def0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top, #fcf8e3 0, #f8efc0 100%);background-image:linear-gradient(to bottom, #fcf8e3 0, #f8efc0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top, #f2dede 0, #e7c3c3 100%);background-image:linear-gradient(to bottom, #f2dede 0, #e7c3c3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top, #ebebeb 0, #f5f5f5 100%);background-image:linear-gradient(to bottom, #ebebeb 0, #f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0)}.progress-bar{background-image:-webkit-linear-gradient(top, #428bca 0, #3071a9 100%);background-image:linear-gradient(to bottom, #428bca 0, #3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0)}.progress-bar-success{background-image:-webkit-linear-gradient(top, #5cb85c 0, #449d44 100%);background-image:linear-gradient(to bottom, #5cb85c 0, #449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0)}.progress-bar-info{background-image:-webkit-linear-gradient(top, #5bc0de 0, #31b0d5 100%);background-image:linear-gradient(to bottom, #5bc0de 0, #31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0)}.progress-bar-warning{background-image:-webkit-linear-gradient(top, #f0ad4e 0, #ec971f 100%);background-image:linear-gradient(to bottom, #f0ad4e 0, #ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0)}.progress-bar-danger{background-image:-webkit-linear-gradient(top, #d9534f 0, #c9302c 100%);background-image:linear-gradient(to bottom, #d9534f 0, #c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top, #428bca 0, #3278b3 100%);background-image:linear-gradient(to bottom, #428bca 0, #3278b3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);border-color:#3278b3}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.box-header{background-image:-webkit-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%);background-image:linear-gradient(to bottom, #f5f5f5 0, #e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0)}.panel-primary>.box-header{background-image:-webkit-linear-gradient(top, #428bca 0, #357ebd 100%);background-image:linear-gradient(to bottom, #428bca 0, #357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0)}.panel-success>.box-header{background-image:-webkit-linear-gradient(top, #dff0d8 0, #d0e9c6 100%);background-image:linear-gradient(to bottom, #dff0d8 0, #d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0)}.panel-info>.box-header{background-image:-webkit-linear-gradient(top, #d9edf7 0, #c4e3f3 100%);background-image:linear-gradient(to bottom, #d9edf7 0, #c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0)}.panel-warning>.box-header{background-image:-webkit-linear-gradient(top, #fcf8e3 0, #faf2cc 100%);background-image:linear-gradient(to bottom, #fcf8e3 0, #faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0)}.panel-danger>.box-header{background-image:-webkit-linear-gradient(top, #f2dede 0, #ebcccc 100%);background-image:linear-gradient(to bottom, #f2dede 0, #ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0)}.well{background-image:-webkit-linear-gradient(top, #e8e8e8 0, #f5f5f5 100%);background-image:linear-gradient(to bottom, #e8e8e8 0, #f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)} \ No newline at end of file +.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:active,.btn.active{background-image:none}.btn-default{background-image:-webkit-linear-gradient(top, #fff 0, #e0e0e0 100%);background-image:linear-gradient(to bottom, #fff 0, #e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#dbdbdb;text-shadow:0 1px 0 #fff;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-primary{background-image:-webkit-linear-gradient(top, #428bca 0, #2d6ca2 100%);background-image:linear-gradient(to bottom, #428bca 0, #2d6ca2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#2b669a}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-success{background-image:-webkit-linear-gradient(top, #5cb85c 0, #419641 100%);background-image:linear-gradient(to bottom, #5cb85c 0, #419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-info{background-image:-webkit-linear-gradient(top, #5bc0de 0, #2aabd2 100%);background-image:linear-gradient(to bottom, #5bc0de 0, #2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-warning{background-image:-webkit-linear-gradient(top, #f0ad4e 0, #eb9316 100%);background-image:linear-gradient(to bottom, #f0ad4e 0, #eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-danger{background-image:-webkit-linear-gradient(top, #d9534f 0, #c12e2a 100%);background-image:linear-gradient(to bottom, #d9534f 0, #c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-image:-webkit-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%);background-image:linear-gradient(to bottom, #f5f5f5 0, #e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-color:#e8e8e8}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-image:-webkit-linear-gradient(top, #428bca 0, #357ebd 100%);background-image:linear-gradient(to bottom, #428bca 0, #357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-color:#357ebd}.navbar-default{background-image:-webkit-linear-gradient(top, #fff 0, #f8f8f8 100%);background-image:linear-gradient(to bottom, #fff 0, #f8f8f8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top, #ebebeb 0, #f3f3f3 100%);background-image:linear-gradient(to bottom, #ebebeb 0, #f3f3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.075);box-shadow:inset 0 3px 9px rgba(0,0,0,0.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top, #3c3c3c 0, #222 100%);background-image:linear-gradient(to bottom, #3c3c3c 0, #222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top, #222 0, #282828 100%);background-image:linear-gradient(to bottom, #222 0, #282828 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.25);box-shadow:inset 0 3px 9px rgba(0,0,0,0.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-linear-gradient(top, #dff0d8 0, #c8e5bc 100%);background-image:linear-gradient(to bottom, #dff0d8 0, #c8e5bc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top, #d9edf7 0, #b9def0 100%);background-image:linear-gradient(to bottom, #d9edf7 0, #b9def0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top, #fcf8e3 0, #f8efc0 100%);background-image:linear-gradient(to bottom, #fcf8e3 0, #f8efc0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top, #f2dede 0, #e7c3c3 100%);background-image:linear-gradient(to bottom, #f2dede 0, #e7c3c3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top, #ebebeb 0, #f5f5f5 100%);background-image:linear-gradient(to bottom, #ebebeb 0, #f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0)}.progress-bar{background-image:-webkit-linear-gradient(top, #428bca 0, #3071a9 100%);background-image:linear-gradient(to bottom, #428bca 0, #3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0)}.progress-bar-success{background-image:-webkit-linear-gradient(top, #5cb85c 0, #449d44 100%);background-image:linear-gradient(to bottom, #5cb85c 0, #449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0)}.progress-bar-info{background-image:-webkit-linear-gradient(top, #5bc0de 0, #31b0d5 100%);background-image:linear-gradient(to bottom, #5bc0de 0, #31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0)}.progress-bar-warning{background-image:-webkit-linear-gradient(top, #f0ad4e 0, #ec971f 100%);background-image:linear-gradient(to bottom, #f0ad4e 0, #ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0)}.progress-bar-danger{background-image:-webkit-linear-gradient(top, #d9534f 0, #c9302c 100%);background-image:linear-gradient(to bottom, #d9534f 0, #c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top, #428bca 0, #3278b3 100%);background-image:linear-gradient(to bottom, #428bca 0, #3278b3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);border-color:#3278b3}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.box-header{background-image:-webkit-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%);background-image:linear-gradient(to bottom, #f5f5f5 0, #e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0)}.panel-primary>.box-header{background-image:-webkit-linear-gradient(top, #428bca 0, #357ebd 100%);background-image:linear-gradient(to bottom, #428bca 0, #357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0)}.panel-success>.box-header{background-image:-webkit-linear-gradient(top, #dff0d8 0, #d0e9c6 100%);background-image:linear-gradient(to bottom, #dff0d8 0, #d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0)}.panel-info>.box-header{background-image:-webkit-linear-gradient(top, #d9edf7 0, #c4e3f3 100%);background-image:linear-gradient(to bottom, #d9edf7 0, #c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0)}.panel-warning>.box-header{background-image:-webkit-linear-gradient(top, #fcf8e3 0, #faf2cc 100%);background-image:linear-gradient(to bottom, #fcf8e3 0, #faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0)}.panel-danger>.box-header{background-image:-webkit-linear-gradient(top, #f2dede 0, #ebcccc 100%);background-image:linear-gradient(to bottom, #f2dede 0, #ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0)}.well{background-image:-webkit-linear-gradient(top, #e8e8e8 0, #f5f5f5 100%);background-image:linear-gradient(to bottom, #e8e8e8 0, #f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)} diff --git a/public/assets/css/bootstrap.min.css b/public/assets/css/bootstrap.min.css index 7af7ea7c..7e029af9 100755 --- a/public/assets/css/bootstrap.min.css +++ b/public/assets/css/bootstrap.min.css @@ -1,7 +1,7 @@ /*! - * Bootstrap v3.1.1 (http://getbootstrap.com) + * Bootstrap v3.2.0 (http://getbootstrap.com) * Copyright 2011-2014 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ -/*! normalize.css v3.0.0 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@media print{*{text-shadow:none !important;color:#000 !important;background:transparent !important;box-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff !important}.navbar{display:none}.table td,.table th{background-color:#fff !important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#999}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-muted{color:#999}.text-primary{color:#428bca}a.text-primary:hover{color:#3071a9}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#428bca}a.bg-primary:hover{background-color:#3071a9}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:bold}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#999}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;white-space:nowrap;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*="col-"]{position:static;float:none;display:table-column}table td[class*="col-"],table th[class*="col-"]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}@media (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;overflow-x:scroll;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type="search"]{-webkit-appearance:none}input[type="date"]{line-height:34px}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;margin-top:10px;margin-bottom:10px;padding-left:20px}.radio label,.checkbox label{display:inline;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:normal;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.has-feedback .form-control-feedback{position:absolute;top:25px;right:0;display:block;width:34px;height:34px;line-height:34px;text-align:center}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:none;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-control-static{padding-top:7px}@media (min-width:768px){.form-horizontal .control-label{text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#428bca;font-weight:normal;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;outline:0;background-color:#428bca}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#999}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:none}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-left-radius:4px;border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle="buttons"]>.btn>input[type="radio"],[data-toggle="buttons"]>.btn>input[type="checkbox"]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px 15px;font-size:18px;line-height:20px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:none}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media (min-width:768px){.navbar-left{float:left !important}.navbar-right{float:right !important}}.navbar-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{float:none;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#e7e7e7;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#080808;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#428bca;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;color:#fff;line-height:1;vertical-align:baseline;white-space:nowrap;text-align:center;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.box-body{padding:15px}.box-header{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.box-header>.dropdown .dropdown-toggle{color:inherit}.box-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.box-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.box-header+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.box-body+.table,.panel>.box-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px;overflow:hidden}.panel-group .panel+.panel{margin-top:5px}.panel-group .box-header{border-bottom:0}.panel-group .box-header+.panel-collapse .box-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .box-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.box-header{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.box-header+.panel-collapse .box-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .box-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.box-header{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.box-header+.panel-collapse .box-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .box-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.box-header{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.box-header+.panel-collapse .box-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .box-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.box-header{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.box-header+.panel-collapse .box-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .box-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.box-header{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.box-header+.panel-collapse .box-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .box-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.box-header{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.box-header+.panel-collapse .box-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .box-body{border-bottom-color:#ebccd1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:auto;overflow-y:scroll;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0, -25%);-ms-transform:translate(0, -25%);transform:translate(0, -25%);-webkit-transition:-webkit-transform 0.3s ease-out;-moz-transition:-moz-transform 0.3s ease-out;-o-transition:-o-transform 0.3s ease-out;transition:transform 0.3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);transform:translate(0, 0)}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);background-clip:padding-box;outline:none}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.42857143px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:20px}.modal-footer{margin-top:15px;padding:19px 20px 20px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.clearfix:before,.clearfix:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.box-body:before,.box-body:after,.modal-footer:before,.modal-footer:after{content:" ";display:table}.clearfix:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.box-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important;visibility:hidden !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block !important}table.visible-md{display:table}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:1200px){.visible-lg{display:block !important}table.visible-lg{display:table}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none !important}}@media (min-width:1200px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}@media print{.hidden-print{display:none !important}} \ No newline at end of file +/*! normalize.css v3.0.0 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@media print{*{text-shadow:none !important;color:#000 !important;background:transparent !important;box-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff !important}.navbar{display:none}.table td,.table th{background-color:#fff !important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#999}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-muted{color:#999}.text-primary{color:#428bca}a.text-primary:hover{color:#3071a9}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#428bca}a.bg-primary:hover{background-color:#3071a9}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:bold}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#999}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;white-space:nowrap;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*="col-"]{position:static;float:none;display:table-column}table td[class*="col-"],table th[class*="col-"]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}@media (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;overflow-x:scroll;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type="search"]{-webkit-appearance:none}input[type="date"]{line-height:34px}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;margin-top:10px;margin-bottom:10px;padding-left:20px}.radio label,.checkbox label{display:inline;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:normal;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.has-feedback .form-control-feedback{position:absolute;top:25px;right:0;display:block;width:34px;height:34px;line-height:34px;text-align:center}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:none;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-control-static{padding-top:7px}@media (min-width:768px){.form-horizontal .control-label{text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#428bca;font-weight:normal;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;outline:0;background-color:#428bca}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#999}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:none}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-left-radius:4px;border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle="buttons"]>.btn>input[type="radio"],[data-toggle="buttons"]>.btn>input[type="checkbox"]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px 15px;font-size:18px;line-height:20px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:none}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media (min-width:768px){.navbar-left{float:left !important}.navbar-right{float:right !important}}.navbar-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{float:none;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#e7e7e7;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#080808;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#428bca;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;color:#fff;line-height:1;vertical-align:baseline;white-space:nowrap;text-align:center;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.box-body{padding:15px}.box-header{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.box-header>.dropdown .dropdown-toggle{color:inherit}.box-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.box-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.box-header+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.box-body+.table,.panel>.box-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px;overflow:hidden}.panel-group .panel+.panel{margin-top:5px}.panel-group .box-header{border-bottom:0}.panel-group .box-header+.panel-collapse .box-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .box-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.box-header{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.box-header+.panel-collapse .box-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .box-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.box-header{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.box-header+.panel-collapse .box-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .box-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.box-header{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.box-header+.panel-collapse .box-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .box-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.box-header{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.box-header+.panel-collapse .box-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .box-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.box-header{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.box-header+.panel-collapse .box-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .box-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.box-header{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.box-header+.panel-collapse .box-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .box-body{border-bottom-color:#ebccd1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:auto;overflow-y:scroll;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0, -25%);-ms-transform:translate(0, -25%);transform:translate(0, -25%);-webkit-transition:-webkit-transform 0.3s ease-out;-moz-transition:-moz-transform 0.3s ease-out;-o-transition:-o-transform 0.3s ease-out;transition:transform 0.3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);transform:translate(0, 0)}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);background-clip:padding-box;outline:none}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.42857143px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:20px}.modal-footer{margin-top:15px;padding:19px 20px 20px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.clearfix:before,.clearfix:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.box-body:before,.box-body:after,.modal-footer:before,.modal-footer:after{content:" ";display:table}.clearfix:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.box-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important;visibility:hidden !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block !important}table.visible-md{display:table}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:1200px){.visible-lg{display:block !important}table.visible-lg{display:table}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none !important}}@media (min-width:1200px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}@media print{.hidden-print{display:none !important}} diff --git a/public/assets/js/bootstrap.min.js b/public/assets/js/bootstrap.min.js index 856dc42f..9e2add70 100755 --- a/public/assets/js/bootstrap.min.js +++ b/public/assets/js/bootstrap.min.js @@ -1,7 +1,11 @@ /*! - * Bootstrap v3.1.1 (http://getbootstrap.com) + * Bootstrap v3.2.0 (http://getbootstrap.com) * Copyright 2011-2014 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ -+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function f(){e.trigger("closed.bs.alert").remove()}var c=a(this),d=c.attr("data-target");d||(d=c.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,""));var e=a(d);b&&b.preventDefault(),e.length||(e=c.hasClass("alert")?c:c.parent()),e.trigger(b=a.Event("close.bs.alert"));if(b.isDefaultPrevented())return;e.removeClass("in"),a.support.transition&&e.hasClass("fade")?e.one(a.support.transition.end,f).emulateTransitionEnd(150):f()};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.isLoading=!1};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",f.resetText||d.data("resetText",d[e]()),d[e](f[b]||this.options[b]),setTimeout(a.proxy(function(){b=="loadingText"?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},b.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");c.prop("type")=="radio"&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f=typeof c=="object"&&c;e||d.data("bs.button",e=new b(this,f)),c=="toggle"?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,this.options.pause=="hover"&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();if(b>this.$items.length-1||b<0)return;return this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){if(this.sliding)return;return this.slide("next")},b.prototype.prev=function(){if(this.sliding)return;return this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g=b=="next"?"left":"right",h=b=="next"?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});this.$element.trigger(j);if(j.isDefaultPrevented())return;return this.sliding=!0,f&&this.pause(),this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(d.css("transition-duration").slice(0,-1)*1e3)):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")),f&&this.cycle(),this};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),typeof c=="object"&&c),g=typeof c=="string"?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),typeof c=="number"?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c=a(this),d,e=a(c.attr("data-target")||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),c.data()),g=c.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=c.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){function e(d){a(b).remove(),a(c).each(function(){var b=f(a(this)),c={relatedTarget:this};if(!b.hasClass("open"))return;b.trigger(d=a.Event("hide.bs.dropdown",c));if(d.isDefaultPrevented())return;b.removeClass("open").trigger("hidden.bs.dropdown",c)})}function f(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}"use strict";var b=".dropdown-backdrop",c="[data-toggle=dropdown]",d=function(b){a(b).on("click.bs.dropdown",this.toggle)};d.prototype.toggle=function(b){var c=a(this);if(c.is(".disabled, :disabled"))return;var d=f(c),g=d.hasClass("open");e();if(!g){"ontouchstart"in document.documentElement&&!d.closest(".navbar-nav").length&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?typeof c=="string"?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||(typeof b.content=="function"?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f=typeof c=="object"&&c;if(!e&&c=="destroy")return;e||d.data("bs.popover",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,""));if(b.parent("li").hasClass("active"))return;var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});b.trigger(f);if(f.isDefaultPrevented())return;var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})},b.prototype.activate=function(b,c,d){function g(){e.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),f?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var e=c.find("> .active"),f=d&&a.support.transition&&e.hasClass("fade");f?e.one(a.support.transition.end,g).emulateTransitionEnd(150):g(),e.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),typeof c=="string"&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(this.transitioning||this.$element.hasClass("in"))return;var b=a.Event("show.bs.collapse");this.$element.trigger(b);if(b.isDefaultPrevented())return;var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("collapse in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])},b.prototype.hide=function(){if(this.transitioning||!this.$element.hasClass("in"))return;var b=a.Event("hide.bs.collapse");this.$element.trigger(b);if(b.isDefaultPrevented())return;var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};if(!a.support.transition)return d.call(this);this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350)},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),typeof c=="object"&&c);!e&&f.toggle&&c=="show"&&(c=!c),e||d.data("bs.collapse",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c=a(this),d,e=c.attr("data-target")||b.preventDefault()||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":c.data(),i=c.attr("data-parent"),j=i&&a(i);if(!g||!g.transitioning)j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(c).addClass("collapsed"),c[f.hasClass("in")?"addClass":"removeClass"]("collapsed");f.collapse(h)})}(jQuery),+function(a){function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(a.style[c]!==undefined)return{end:b[c]};return!1}"use strict",a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery) \ No newline at end of file +/*! + * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=09c8c466aa06e64a7262) + * Config saved to config.json and https://gist.github.com/09c8c466aa06e64a7262 + */ +if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(t){"use strict";function e(e){return this.each(function(){var i=t(this),s=i.data("bs.alert");s||i.data("bs.alert",s=new o(this)),"string"==typeof e&&s[e].call(i)})}var i='[data-dismiss="alert"]',o=function(e){t(e).on("click",i,this.close)};o.VERSION="3.2.0",o.prototype.close=function(e){function i(){n.detach().trigger("closed.bs.alert").remove()}var o=t(this),s=o.attr("data-target");s||(s=o.attr("href"),s=s&&s.replace(/.*(?=#[^\s]*$)/,""));var n=t(s);e&&e.preventDefault(),n.length||(n=o.hasClass("alert")?o:o.parent()),n.trigger(e=t.Event("close.bs.alert")),e.isDefaultPrevented()||(n.removeClass("in"),t.support.transition&&n.hasClass("fade")?n.one("bsTransitionEnd",i).emulateTransitionEnd(150):i())};var s=t.fn.alert;t.fn.alert=e,t.fn.alert.Constructor=o,t.fn.alert.noConflict=function(){return t.fn.alert=s,this},t(document).on("click.bs.alert.data-api",i,o.prototype.close)}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var o=t(this),s=o.data("bs.button"),n="object"==typeof e&&e;s||o.data("bs.button",s=new i(this,n)),"toggle"==e?s.toggle():e&&s.setState(e)})}var i=function(e,o){this.$element=t(e),this.options=t.extend({},i.DEFAULTS,o),this.isLoading=!1};i.VERSION="3.2.0",i.DEFAULTS={loadingText:"loading..."},i.prototype.setState=function(e){var i="disabled",o=this.$element,s=o.is("input")?"val":"html",n=o.data();e+="Text",null==n.resetText&&o.data("resetText",o[s]()),o[s](null==n[e]?this.options[e]:n[e]),setTimeout(t.proxy(function(){"loadingText"==e?(this.isLoading=!0,o.addClass(i).attr(i,i)):this.isLoading&&(this.isLoading=!1,o.removeClass(i).removeAttr(i))},this),0)},i.prototype.toggle=function(){var t=!0,e=this.$element.closest('[data-toggle="buttons"]');if(e.length){var i=this.$element.find("input");"radio"==i.prop("type")&&(i.prop("checked")&&this.$element.hasClass("active")?t=!1:e.find(".active").removeClass("active")),t&&i.prop("checked",!this.$element.hasClass("active")).trigger("change")}t&&this.$element.toggleClass("active")};var o=t.fn.button;t.fn.button=e,t.fn.button.Constructor=i,t.fn.button.noConflict=function(){return t.fn.button=o,this},t(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(i){var o=t(i.target);o.hasClass("btn")||(o=o.closest(".btn")),e.call(o,"toggle"),i.preventDefault()})}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var o=t(this),s=o.data("bs.carousel"),n=t.extend({},i.DEFAULTS,o.data(),"object"==typeof e&&e),r="string"==typeof e?e:n.slide;s||o.data("bs.carousel",s=new i(this,n)),"number"==typeof e?s.to(e):r?s[r]():n.interval&&s.pause().cycle()})}var i=function(e,i){this.$element=t(e).on("keydown.bs.carousel",t.proxy(this.keydown,this)),this.$indicators=this.$element.find(".carousel-indicators"),this.options=i,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter.bs.carousel",t.proxy(this.pause,this)).on("mouseleave.bs.carousel",t.proxy(this.cycle,this))};i.VERSION="3.2.0",i.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},i.prototype.keydown=function(t){switch(t.which){case 37:this.prev();break;case 39:this.next();break;default:return}t.preventDefault()},i.prototype.cycle=function(e){return e||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(t.proxy(this.next,this),this.options.interval)),this},i.prototype.getItemIndex=function(t){return this.$items=t.parent().children(".item"),this.$items.index(t||this.$active)},i.prototype.to=function(e){var i=this,o=this.getItemIndex(this.$active=this.$element.find(".item.active"));return e>this.$items.length-1||0>e?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){i.to(e)}):o==e?this.pause().cycle():this.slide(e>o?"next":"prev",t(this.$items[e]))},i.prototype.pause=function(e){return e||(this.paused=!0),this.$element.find(".next, .prev").length&&t.support.transition&&(this.$element.trigger(t.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},i.prototype.next=function(){return this.sliding?void 0:this.slide("next")},i.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},i.prototype.slide=function(e,i){var o=this.$element.find(".item.active"),s=i||o[e](),n=this.interval,r="next"==e?"left":"right",a="next"==e?"first":"last",l=this;if(!s.length){if(!this.options.wrap)return;s=this.$element.find(".item")[a]()}if(s.hasClass("active"))return this.sliding=!1;var h=s[0],p=t.Event("slide.bs.carousel",{relatedTarget:h,direction:r});if(this.$element.trigger(p),!p.isDefaultPrevented()){if(this.sliding=!0,n&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var c=t(this.$indicators.children()[this.getItemIndex(s)]);c&&c.addClass("active")}var d=t.Event("slid.bs.carousel",{relatedTarget:h,direction:r});return t.support.transition&&this.$element.hasClass("slide")?(s.addClass(e),s[0].offsetWidth,o.addClass(r),s.addClass(r),o.one("bsTransitionEnd",function(){s.removeClass([e,r].join(" ")).addClass("active"),o.removeClass(["active",r].join(" ")),l.sliding=!1,setTimeout(function(){l.$element.trigger(d)},0)}).emulateTransitionEnd(1e3*o.css("transition-duration").slice(0,-1))):(o.removeClass("active"),s.addClass("active"),this.sliding=!1,this.$element.trigger(d)),n&&this.cycle(),this}};var o=t.fn.carousel;t.fn.carousel=e,t.fn.carousel.Constructor=i,t.fn.carousel.noConflict=function(){return t.fn.carousel=o,this},t(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(i){var o,s=t(this),n=t(s.attr("data-target")||(o=s.attr("href"))&&o.replace(/.*(?=#[^\s]+$)/,""));if(n.hasClass("carousel")){var r=t.extend({},n.data(),s.data()),a=s.attr("data-slide-to");a&&(r.interval=!1),e.call(n,r),a&&n.data("bs.carousel").to(a),i.preventDefault()}}),t(window).on("load",function(){t('[data-ride="carousel"]').each(function(){var i=t(this);e.call(i,i.data())})})}(jQuery),+function(t){"use strict";function e(e){e&&3===e.which||(t(s).remove(),t(n).each(function(){var o=i(t(this)),s={relatedTarget:this};o.hasClass("open")&&(o.trigger(e=t.Event("hide.bs.dropdown",s)),e.isDefaultPrevented()||o.removeClass("open").trigger("hidden.bs.dropdown",s))}))}function i(e){var i=e.attr("data-target");i||(i=e.attr("href"),i=i&&/#[A-Za-z]/.test(i)&&i.replace(/.*(?=#[^\s]*$)/,""));var o=i&&t(i);return o&&o.length?o:e.parent()}function o(e){return this.each(function(){var i=t(this),o=i.data("bs.dropdown");o||i.data("bs.dropdown",o=new r(this)),"string"==typeof e&&o[e].call(i)})}var s=".dropdown-backdrop",n='[data-toggle="dropdown"]',r=function(e){t(e).on("click.bs.dropdown",this.toggle)};r.VERSION="3.2.0",r.prototype.toggle=function(o){var s=t(this);if(!s.is(".disabled, :disabled")){var n=i(s),r=n.hasClass("open");if(e(),!r){"ontouchstart"in document.documentElement&&!n.closest(".navbar-nav").length&&t('