From 2e89dc4010ee260c5df9337126c4900634f8a951 Mon Sep 17 00:00:00 2001 From: Christopher De Cairos Date: Wed, 1 May 2013 17:05:01 -0400 Subject: [PATCH 01/34] Fixes Bug 866832 - Tag Filtering for Make Creation and Updating --- README.md | 43 +++++++++++++++++---------- env.sample | 19 +++++++----- lib/maker.js | 24 +++++++++++++++ lib/middleware.js | 46 ++++++++++++++++++++++++++--- lib/models/make.js | 1 + lib/tags.js | 67 ++++++++++++++++++++++++++++++++++++++++++ package.json | 4 +-- public/js/README.md | 35 ++++++++++++++++++++++ public/js/search.js | 71 +++++++++++++++++++++++++++++---------------- public/search.html | 17 +++++++++-- routes/make.js | 8 ++--- server.js | 5 ++-- test/post.js | 7 +++-- 13 files changed, 283 insertions(+), 64 deletions(-) create mode 100644 lib/maker.js create mode 100644 lib/tags.js create mode 100644 public/js/README.md diff --git a/README.md b/README.md index fb8edf4..20ec8f6 100755 --- a/README.md +++ b/README.md @@ -73,21 +73,30 @@ Right now there is a small node app in `test/test-make-client.js` that will requ POST /api/make Create Make - If Post Data is a valid Make, it creates one and returns it with the _id and __v populated. + + If post data contains a valid Make, it creates one and returns it with the _id. + Post Data should be a JSON object specifying the id of the authenticated webmaker creating the Make
+ { "maker": "username", make: { ... } } + Yes PUT /api/make/:id Update a Make - The Make must already exist and the __v must be the same as the current version on the server. This is an implementation of optimistic locking. + The Make must already exist. This is an implementation of optimistic locking. + Post Data should be a JSON object specifying the id of the authenticated webmaker updating the Make and a flag indicating if the user has admin priveliges.
+ { "maker": "username", make: { ... } } + Yes DELETE /api/make/:id Deletes a Make - The effect is that of a delete operation, though the Make is actually only marked as deleted using the deletedAt timestamp. + The effect is that of a delete operation, though the Make is actually only marked as deleted using the deletedAt timestamp. + Post Data should be a JSON object specifying the id of the authenticated webmaker deleting the Make and a flag indicating if the user has admin priveliges.
+ { "maker": "username" } Yes @@ -100,27 +109,27 @@ Right now there is a small node app in `test/test-make-client.js` that will requ -### Example Usage +### Consuming the API ``` jQuery.ajax({ type: "POST", url: "/api/make", data: { - "url": "http://thimble.webmadecontent.org/abcd.html", - "contentType": "text/html", - "title": "Animal something-or-other", - "locale": "en_us", - "tags": ["awesome"], - "privateTags": ["webmaker.org:project", "skill:css"], - "description": "This handy HTML template makes it easy to quickly create your own text and image mashup, then publish it for sharing via Facebook, Tumblr or any web page. Your 15 seconds of internet fame await!", - "author": "swex@mozilla.com", - "contentAuthor": "swex@mozilla.com", - "remixedFrom": null, - "published": true + "user": "webmaker@host.com", + "make": { + "url": "http://thimble.webmadecontent.org/abcd.html", + "contentType": "application/x-thimble", + "title": "Animal something-or-other", + "locale": "en_us", + "tags": [ "awesome", "#css", "thimble.webmaker.org:project" ], + "description": "This handy HTML template makes it easy to quickly create your own text and image mashup, then publish it for sharing via Facebook, Tumblr or any web page. Your 15 seconds of internet fame await!", + "author": "swex@mozilla.com", + "remixedFrom": null + } }, success: function(data, textStatus, jqXHR){ - console.log("Post resposne:"); + console.log("Post response:"); console.dir(data); console.log(textStatus); console.dir(jqXHR); @@ -130,6 +139,8 @@ Right now there is a small node app in `test/test-make-client.js` that will requ } }); ``` +A client library has been written to aid in the consumption of this API. +Documentation can be found [here](public/js/README.md) ### Searching Test Ground diff --git a/env.sample b/env.sample index 97f63a3..abe1c28 100644 --- a/env.sample +++ b/env.sample @@ -1,25 +1,25 @@ # A Secret used to sign Session cookies. -SESSION_SECRET=I wish the people who clean my office at night were invited to our company Christmas party. +export SESSION_SECRET='I wish the people who clean my office at night were invited to our company Christmas party.' # Port to listen on -PORT=5000 +export PORT=5000 # development or production -export NODE_ENV="development" +export NODE_ENV='development' # URL of the Mongodb instance -MONGO_URL=mongodb://localhost/makeapi +export MONGO_URL='mongodb://localhost/makeapi' # Where the server is running. Used for test data generating script -HOST='localhost' +export HOST='localhost' # Host and port for your Elastic search cluster -ELASTIC_SEARCH_URL='http://localhost:9200' +export ELASTIC_SEARCH_URL='http://localhost:9200' # A List of allowed users for write operations to the database # List in this format: "user:pass,user2:pass" # You shouldn't use this username/password combo yourself -ALLOWED_USERS="testuser:password" +export ALLOWED_USERS='testuser:password' # statsd metrics collection. If the following are left empty, no stats # will be collected or sent to a server. Only STATSD_HOST and STATSD_PORT @@ -28,3 +28,8 @@ ALLOWED_USERS="testuser:password" export STATSD_HOST= export STATSD_PORT= export STATSD_PREFIX= + +# These are used to check if a user is an administrator +# Don't use this username/password combo +export LOGIN_SERVER_URL='http://localhost:3000/isAdmin' +export LOGIN_SERVER_AUTH='{"user":"testuser","pass":"password"}' diff --git a/lib/maker.js b/lib/maker.js new file mode 100644 index 0000000..896b213 --- /dev/null +++ b/lib/maker.js @@ -0,0 +1,24 @@ +module.exports = function() { + var env = new require( "habitat" )(), + loginServerURL = env.get( "LOGIN_SERVER_URL" ), + loginServerAuth = env.get( "LOGIN_SERVER_AUTH" ), + request = require( "request" ); + + return { + isAdmin: function( user, callback ){ + request.get( loginServerURL + "/?id=" + user, { + json: true, + auth: { + user: loginServerAuth.user, + pass: loginServerAuth.pass, + sendImmediately: true + } + }, function( error, resp, body ) { + if ( error ) { + return callback( error ); + } + callback( null, body.isAdmin ); + }); + } + }; +}; diff --git a/lib/middleware.js b/lib/middleware.js index 619fb78..bd70586 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -4,12 +4,13 @@ module.exports = function( env ) { - var userList = env.get( "ALLOWED_USERS" ), - qs = require( "querystring" ); - - userList = qs.parse( userList, ",", ":" ); + var qs = require( "querystring" ), + userList = qs.parse( env.get( "ALLOWED_USERS" ), ",", ":" ), + tags = require( "./tags" )(), + maker = require( "./maker" )(); return { + // Use with express.basicAuth middleware authenticateUser: function( user, pass ) { for ( var username in userList ) { if ( userList.hasOwnProperty( username ) ) { @@ -19,6 +20,43 @@ module.exports = function( env ) { } } return false; + }, + prefixAuth: function( req, res, next ) { + + if ( typeof req.body.make === "string" ) { + req.body.make = qs.parse( req.body.make ); + } + + var makerID = req.body.maker, + makeTags = req.body.make.tags, + appTags = req.body.make.appTags; + + makeTags = typeof makeTags === "string" ? [makeTags] : makeTags; + appTags = typeof appTags === "string" ? [appTags] : appTags; + + maker.isAdmin( makerID, function( err, isAdmin ) { + if ( err ) { + return res.json( 500, { error: err } ); + } + + var options = { + maker: makerID, + isAdmin: isAdmin + }, + validTags = []; + + if ( makeTags ) { + validTags = tags.validateTags( makeTags, options ); + } + + if ( appTags ) { + validTags = validTags.concat( tags.validateApplicationTags( appTags, req.user ) ); + } + + req.body.make.tags = validTags; + + next(); + }); } }; }; diff --git a/lib/models/make.js b/lib/models/make.js index ba63ef5..6721783 100644 --- a/lib/models/make.js +++ b/lib/models/make.js @@ -17,6 +17,7 @@ module.exports = function( environment, mongoInstance ) { var Timestamp = { type: Number, + "default": Date.now(), es_type: "long", es_indexed: true, es_index: "not_analyzed" diff --git a/lib/tags.js b/lib/tags.js new file mode 100644 index 0000000..4271b8f --- /dev/null +++ b/lib/tags.js @@ -0,0 +1,67 @@ +module.exports = function() { + + // Application Tags are "webmaker.org:foo", which means two + // strings, joined with a ':', and the first string does not + // contain an '@' + var appTagRegex = /(^[^@]+)\:[^:]+/, + + // User Tags are "some@something.com:foo", which means two + // strings, joined with a ':', and the first string contains + // an email address (i.e., an '@'). + userTagRegex = /^([^@]+@[^@]+)\:[^:]+/, + + // Raw Tags are "foo" or "#fooBar", which means one string + // which does not include a colon. + rawTagRegex = /^[^:]+$/, + + // Trim any whitespace around tags + trimWhitespace = function( tags ) { + return tags.map(function( val ) { + return val.trim(); + }); + }; + + return { + validateTags: function( tags, options ) { + + var user; + + tags = trimWhitespace( tags ); + + return tags.filter(function( val ){ + + // allow if user is an admin, or val is a raw tag + if ( options.isAdmin || rawTagRegex.test( val ) ) { + return true; + } + + user = userTagRegex.exec( val ); + + // Allow if val is a user tag, and user is logged in + if ( user && user[ 1 ] === options.maker ) { + return true; + } + + return false; + }); + }, + validateApplicationTags: function( tags, application ) { + + var appTag; + + tags = trimWhitespace( tags ); + + return tags.filter(function( val ) { + appTag = appTagRegex.exec( val ); + + // Allow if is application tag, and the application tag matches the + // username of the app making the request + if ( appTag && appTag === application ) { + return true; + } + + return false; + }); + } + }; +}; diff --git a/package.json b/package.json index 12a7fd0..b650b6b 100755 --- a/package.json +++ b/package.json @@ -36,10 +36,10 @@ "node-statsd": "0.0.7", "nunjucks": "~0.1.8a", "request": "2.20.0", - "querystring": "0.2.0", - "Faker": "0.5.8" + "querystring": "0.2.0" }, "devDependencies": { + "Faker": "0.5.8", "grunt": "~0.4.1", "grunt-contrib-csslint": "~0.1.2", "grunt-contrib-jshint": "~0.4.3" diff --git a/public/js/README.md b/public/js/README.md new file mode 100644 index 0000000..7b9d485 --- /dev/null +++ b/public/js/README.md @@ -0,0 +1,35 @@ +#Make API Client Library# + + +##API Reference## +TO-DO + +##Tagging## + +There are 3 types of tags that can be applied to makes. Each tag requires specific levels of authentication, which will be described below. + +**Raw Tags** + +Raw tags can be applied by any user, and can be prefixed by a '#'. + +I.E. +\#Awesome +WebLiteracy +very-cool + +**User Tags** + +User tags are the only prefix that a regular webmaker user can apply to a tag. The separator for a prefixed tag is a colon ':' + +I.E. +webmaker@domain.com:favourite +webmaker2@domain.com:myawesomelist + +**Application tags** + +These tags are reserved for trusted apps (with the exception that an admin webmaker can add them) + +I.E. +webmaker.org:featured +popcorn.webmaker.org:project +thimble.webmaker.org:tutorial diff --git a/public/js/search.js b/public/js/search.js index 11a0685..b51e098 100644 --- a/public/js/search.js +++ b/public/js/search.js @@ -3,12 +3,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ document.addEventListener( "DOMContentLoaded", function() { - var make = Make({ - apiURL: "", - auth: "testuser:password" - }); - var makeTitle = document.getElementById( "make-title" ), + var makeURL = document.getElementById( "makeURL" ), + username = document.getElementById( "username" ), + password = document.getElementById( "password" ), + makeTitle = document.getElementById( "make-title" ), makeDescription = document.getElementById( "make-description" ), makeAuthor = document.getElementById( "make-author" ), makeEmail = document.getElementById( "make-email" ), @@ -23,12 +22,23 @@ page = document.getElementById( "page" ), makeTagPrefix = document.getElementById( "tag-prefix" ), makeId = document.getElementById( "make-id" ), + idSearch = document.getElementById( "search-make-id" ), sortBy = document.getElementById( "sort-field" ), + webmakerID = document.getElementById( "webmaker-id" ), makeResult = document.getElementById( "make-result" ), - searchResult = document.getElementById( "search-result" ); + searchResult = document.getElementById( "search-result" ), + appTags = document.getElementById( "app-tags" ); + + function make() { + var url = makeURL.value || ""; + return Make({ + apiURL: url, + auth: username.value + ":" + password.value + }); + } window.grabTags = function() { - make + make() .tags({ tags: searchTags.value.split( "," ), execution: document.querySelector( "input[name='execution']:checked" ).value @@ -46,7 +56,7 @@ }; window.myProjects = function() { - make + make() .tags({ tags: searchTags.value.split( "," ), execution: document.querySelector( "input[name='execution']:checked" ).value @@ -65,8 +75,8 @@ }; window.findProject = function() { - make - .find( { id: document.getElementById( "search-make-id" ).value } ) + make() + .find( { id: idSearch.value } ) .then(function( error, data ) { if ( error ) { searchResult.value = JSON.stringify( error, null, 2 ); @@ -77,7 +87,7 @@ }; window.prefixSearch = function() { - make + make() .tagPrefix( makeTagPrefix.value ) .limit( size.value ) .page( page.value || 1 ) @@ -93,15 +103,19 @@ function getData() { return { - title: makeTitle.value, - description: makeDescription.value, - author: makeAuthor.value, - email: makeEmail.value, - contentType: makeContentType.value, - locale: makeLocale.value, - url: makeUrl.value, - thumbnail: makeThumbnail.value, - tags: makeTags.value.split( "," ) + maker: webmakerID.value, + make: { + title: makeTitle.value, + description: makeDescription.value, + author: makeAuthor.value, + email: makeEmail.value, + contentType: makeContentType.value, + locale: makeLocale.value, + url: makeUrl.value, + thumbnail: makeThumbnail.value, + tags: makeTags.value.split( "," ), + appTags: appTags.value.split( "," ) + } }; } @@ -110,19 +124,26 @@ makeResult.value = JSON.stringify( error, null, 2 ); return; } - makeId.value = resp._id; - makeResult.value = JSON.stringify( resp, null, 2 ); + try { + makeId.value = resp._id; + makeResult.value = JSON.stringify( resp, null, 2 ); + } catch( e ) { + makeResult.value = e.toString(); + } } window.createMake = function() { - make.create( getData(), handleResponse ); + make() + .create( getData(), handleResponse ); }; window.updateMake = function() { - make.update( makeId.value, getData(), handleResponse ); + make() + .update( makeId.value, getData(), handleResponse ); }; window.deleteMake = function() { - make.remove( makeId.value, handleResponse ); + make() + .remove( makeId.value, handleResponse ); }; }, false ); diff --git a/public/search.html b/public/search.html index dea01c3..69be38a 100644 --- a/public/search.html +++ b/public/search.html @@ -2,10 +2,8 @@ - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - - @@ -19,6 +17,15 @@ </head> <body> <h1>Make API Client Library Test Page</h1> + <div class="input-wrapper"> + MakeAPI URL: <input type="text" id="makeURL" value="http://makeapi.mofostaging.net" /> + </div> + <div class="input-wrapper"> + Username: <input type="text" id="username" /> + </div> + <div class="input-wrapper"> + Password: <input type="password" id="password" /> + </div> <h2>Search</h2> <div class="input-wrapper"> Tags(CSV)<input type="text" id="tags"/> @@ -96,10 +103,16 @@ <h2>Create / Update / Delete</h2> <div class="input-wrapper"> Tags(CSV) <input type="text" id="make-tags" value="" placeholder="tags"> </div> + <div class="input-wrapper"> + AppTags(CSV) <input type="text" id="app-tags" placeholder="App Tags"/> + </div> <br /> <div class="input-wrapper"> ID(update and delete) <input type="text" id="make-id" value="" placeholder="ID HERE"> </div> + <div class="input-wrapper"> + webmaker ID: <input type="text" id="webmaker-id" value="" placeholder="webmakerID"> + </div> <br /> <input type="button" onclick="createMake();" value="Create a Make" /><br /> <input type="button" onclick="updateMake();" value="Update a Make" /><br /> diff --git a/routes/make.js b/routes/make.js index f503432..37e47fe 100644 --- a/routes/make.js +++ b/routes/make.js @@ -30,9 +30,9 @@ module.exports = function( makeCtor, env ) { create: function( req, res ) { var make = new Make(); - for ( var i in Make.publicFields ){ + for ( var i in Make.publicFields ) { var field = Make.publicFields[ i ]; - make[ field ] = req.body[ field ]; + make[ field ] = req.body.make[ field ]; } make.createdAt = Date.now(); @@ -45,8 +45,8 @@ module.exports = function( makeCtor, env ) { Make.findById( req.params.id ).where( "deletedAt", null ).exec(function( err, make ) { for ( var i in Make.publicFields ) { var field = Make.publicFields[ i ]; - if ( req.body[ field ] ) { - make[ field ] = req.body[ field ]; + if ( req.body.make[ field ] ) { + make[ field ] = req.body.make[ field ]; } } make.updatedAt = ( new Date() ).getTime(); diff --git a/server.js b/server.js index c0f0128..dc4a31a 100755 --- a/server.js +++ b/server.js @@ -36,6 +36,7 @@ app.use( express.static( path.join( __dirname + "/public" ) ) ); app.use( express.bodyParser() ); app.use( express.cookieParser() ); app.use( express.cookieSession({ + key: "express.sid", secret: env.get( "SESSION_SECRET" ), cookie: { maxAge: 2678400000 // 31 days. Persona saves session data for 1 month @@ -44,8 +45,8 @@ app.use( express.cookieSession({ })); app.get( "/", routes.index ); -app.post( "/api/make", express.basicAuth( middleware.authenticateUser ), Mongo.isDbOnline, routes.create ); -app.put( "/api/make/:id", express.basicAuth( middleware.authenticateUser ), Mongo.isDbOnline, routes.update ); +app.post( "/api/make", express.basicAuth( middleware.authenticateUser ), Mongo.isDbOnline, middleware.prefixAuth, routes.create ); +app.put( "/api/make/:id", express.basicAuth( middleware.authenticateUser ), Mongo.isDbOnline, middleware.prefixAuth, routes.update ); app.del( "/api/make/:id", express.basicAuth( middleware.authenticateUser ), Mongo.isDbOnline, routes.remove ); app.get( "/api/makes/search", Mongo.isDbOnline, function crossOrigin( req, res, next ) { res.header( "Access-Control-Allow-Origin", "*" ); diff --git a/test/post.js b/test/post.js index 27a89b6..f80c7e3 100644 --- a/test/post.js +++ b/test/post.js @@ -24,7 +24,10 @@ if ( !auth ) { for ( var i = 0; i < numberOfRecords; i++ ) { - var postData = querystring.stringify( fakeIt() ); + var postData = querystring.stringify({ + make: querystring.stringify( fakeIt() ), + maker: "cade" + }); postOptions = { auth: auth, @@ -47,4 +50,4 @@ for ( var i = 0; i < numberOfRecords; i++ ) { postReq.write( postData ); postReq.end(); -} \ No newline at end of file +} From fff74cd73a58ab903e0fc5f37d9954b37d06d6d5 Mon Sep 17 00:00:00 2001 From: "David Humphrey (:humph) david.humphrey@senecacollege.ca" <david.humphrey@senecacollege.ca> Date: Thu, 9 May 2013 16:05:52 -0400 Subject: [PATCH 02/34] Fix bug 870397 - Add some helper APIs around make results, return data.hits directly for search callback r=cade --- Gruntfile.js | 7 +- env.sample | 7 +- lib/api.js | 4 + lib/maker.js | 22 +---- lib/middleware.js | 13 ++- package.json | 7 +- public/js/make-api.js | 137 ++++++++++++++++++++++++++++- public/js/search.js | 35 +------- public/search.html | 58 +------------ routes/make.js | 38 +++----- test/fake.js | 60 ------------- test/fake/FakeAPI.js | 127 +++++++++++++++++++++++++++ test/fake/counterfeit.js | 75 ++++++++++++++++ test/fake/example.js | 47 ++++++++++ test/fake/fake.js | 182 +++++++++++++++++++++++++++++++++++++++ test/post.js | 53 ------------ test/test-make-client.js | 2 +- 17 files changed, 617 insertions(+), 257 deletions(-) create mode 100644 lib/api.js delete mode 100644 test/fake.js create mode 100755 test/fake/FakeAPI.js create mode 100644 test/fake/counterfeit.js create mode 100644 test/fake/example.js create mode 100644 test/fake/fake.js delete mode 100644 test/post.js diff --git a/Gruntfile.js b/Gruntfile.js index 1c6ac56..19bc465 100755 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -8,12 +8,17 @@ module.exports = function( grunt ) { ] }, jshint: { + options: { + es5: true, + newcap: false + }, files: [ "Gruntfile.js", "server.js", "lib/**/*.js", "public/**/*.js", - "routes/**/*.js" + "routes/**/*.js", + "test/**/*.js" ] } }); diff --git a/env.sample b/env.sample index abe1c28..10f7c9f 100644 --- a/env.sample +++ b/env.sample @@ -29,7 +29,6 @@ export STATSD_HOST= export STATSD_PORT= export STATSD_PREFIX= -# These are used to check if a user is an administrator -# Don't use this username/password combo -export LOGIN_SERVER_URL='http://localhost:3000/isAdmin' -export LOGIN_SERVER_AUTH='{"user":"testuser","pass":"password"}' +# This is used to check if a user is an administrator (include user/password) +# in URL. Don't use this username/password combo +export LOGIN_SERVER_URL_WITH_AUTH='http://loginuser:loginpassword@localhost:3000' diff --git a/lib/api.js b/lib/api.js new file mode 100644 index 0000000..2f1a6e8 --- /dev/null +++ b/lib/api.js @@ -0,0 +1,4 @@ +module.exports = { + makeAPI: require( "../public/js/make-api.js" ), + fakeAPI: require( "../test/fake/FakeAPI.js" ) +}; diff --git a/lib/maker.js b/lib/maker.js index 896b213..135c600 100644 --- a/lib/maker.js +++ b/lib/maker.js @@ -1,24 +1,10 @@ +var loginAPI = require( "webmaker-loginapi"); + module.exports = function() { var env = new require( "habitat" )(), - loginServerURL = env.get( "LOGIN_SERVER_URL" ), - loginServerAuth = env.get( "LOGIN_SERVER_AUTH" ), - request = require( "request" ); + isAdmin = loginAPI( env.get( "LOGIN_SERVER_URL_WITH_AUTH" ) ).isAdmin; return { - isAdmin: function( user, callback ){ - request.get( loginServerURL + "/?id=" + user, { - json: true, - auth: { - user: loginServerAuth.user, - pass: loginServerAuth.pass, - sendImmediately: true - } - }, function( error, resp, body ) { - if ( error ) { - return callback( error ); - } - callback( null, body.isAdmin ); - }); - } + isAdmin: isAdmin }; }; diff --git a/lib/middleware.js b/lib/middleware.js index bd70586..6bbbf53 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -12,14 +12,13 @@ module.exports = function( env ) { return { // Use with express.basicAuth middleware authenticateUser: function( user, pass ) { - for ( var username in userList ) { - if ( userList.hasOwnProperty( username ) ) { - if ( user === username && pass === userList[ username ] ) { - return true; - } + var found = false; + Object.keys( userList ).forEach( function( username ) { + if ( user === username && pass === userList[ username ] ) { + found = true; } - } - return false; + }); + return found; }, prefixAuth: function( req, res, next ) { diff --git a/package.json b/package.json index b650b6b..7ac9eec 100755 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "makeapi", - "version": "0.0.11", + "version": "0.1.0", "description": "MakeAPI for Webmaker", - "main": "public/js/make-api.js", + "main": "./lib/api.js", "scripts": { "test": "grunt" }, @@ -36,7 +36,8 @@ "node-statsd": "0.0.7", "nunjucks": "~0.1.8a", "request": "2.20.0", - "querystring": "0.2.0" + "querystring": "0.2.0", + "webmaker-loginapi": "0.1.0" }, "devDependencies": { "Faker": "0.5.8", diff --git a/public/js/make-api.js b/public/js/make-api.js index 6483406..619ca01 100644 --- a/public/js/make-api.js +++ b/public/js/make-api.js @@ -90,6 +90,125 @@ var module = module || undefined; xhrStrategy( type, path, data, callback ); } + // Extend a make with some API sugar. + function wrap( make, options ) { + + function getMakeInstance() { + if ( !getMakeInstance.instance ) { + getMakeInstance.instance = Make( options ); + } + return getMakeInstance.instance; + } + + // Lazily extract various tags types as needed, and memoize. + function lazyInitTags( o, name, regexp ) { + delete o[ name ]; + var tags = []; + make.tags.forEach( function( tag ) { + if( regexp.test( tag ) ) { + tags.push( tag ); + } + }); + o[ name ] = tags; + return tags; + } + + var wrapped = { + // Application Tags are "webmaker.org:foo", which means two + // strings, joined with a ':', and the first string does not + // contain an '@' + get appTags() { + return lazyInitTags( this, 'appTags', /^[^@]+\:[^:]+/ ); + }, + + // User Tags are "some@something.com:foo", which means two + // strings, joined with a ':', and the first string contains + // an email address (i.e., an '@'). + get userTags() { + return lazyInitTags( this, 'userTags', /^[^@]+@[^@]+\:[^:]+/ ); + }, + + // Raw Tags are "foo" or "#fooBar", which means one string + // which does not include a colon. + get rawTags() { + return lazyInitTags( this, 'rawTags', /^[^:]+$/ ); + }, + + // Determine whether this make is tagged with any of the tags + // passed into `tags`. This can be a String or [ String ], + // and the logic is OR vs. AND for multiple. + taggedWithAny: function( tags ) { + var any = false, + all = make.tags; + tags = Array.isArray( tags ) ? tags : [ tags ]; + for( var i = 0; i < tags.length; i++ ) { + if ( all.indexOf( tags[ i ] ) > -1 ) { + return true; + } + } + return false; + }, + + // Get a list of other makes that were remixed from this make. + // The current make's URL is used as a key. + remixes: function( callback ) { + callback = callback || function(){}; + getMakeInstance() + .find({ remixedFrom: wrapped._id }) + .then( callback ); + }, + + // Similar to remixes(), but filter out only those remixes that + // have a different locale (i.e., are localized versions of this + // make). + locales: function( callback ) { + callback = callback || function(){}; + this.remixes( function( err, results ) { + if( err ) { + callback( err ); + return; + } + var locales = []; + results.forEach( function( one ) { + if ( one.locale !== wrapped.locale ) { + locales.push( one ); + } + }); + callback( null, locales ); + }); + }, + + // Get the original make used to create this remix. Null is sent + // back in the callback if there was no original (not a remix) + original: function( callback ) { + callback = callback || function(){}; + if ( !wrapped.remixedFrom ) { + callback( null, null ); + return; + } + getMakeInstance() + .find({ _id: wrapped._id }) + .then( callback ); + }, + + update: function( callback ) { + callback = callback || function(){}; + getMakeInstance() + .update( wrapped._id, { maker: wrapped.email, make: wrapped }, callback ); + } + + }; + + // Extend wrapped with contents of make + [ "url", "contentType", "locale", "title", "description", + "author", "published", "tags", "thumbnail", "email", + "remixedFrom", "_id" ].forEach( function( prop ) { + wrapped[ prop ] = make[ prop ]; + }); + + return wrapped; + } + // Shorthand for creating a Make Object Make = function Make( options ) { apiURL = options.apiURL; @@ -249,9 +368,25 @@ var module = module || undefined; this.pageNum = 1; this.searchFilters = []; this.sortBy = []; - doXHR( "GET", "/api/makes/search", escape( JSON.stringify( searchQuery ) ), callback ); + + doXHR( "GET", "/api/makes/search", + escape( JSON.stringify( searchQuery ) ), + function( err, data ) { + if ( err ) { + callback( err ); + } else { + // Wrap resulting makes with some extra API. + var hits = data.hits || []; + for( var i = 0; i < hits.length; i++ ) { + hits[ i ] = wrap( hits[ i ], options ); + } + callback( null, hits ); + } + } + ); }, + // Options should be of the form: { maker: "email@address", make: {...} } create: function create( options, callback ) { doXHR( "POST", "/api/make", options, callback ); return this; diff --git a/public/js/search.js b/public/js/search.js index b51e098..1c54847 100644 --- a/public/js/search.js +++ b/public/js/search.js @@ -7,27 +7,14 @@ var makeURL = document.getElementById( "makeURL" ), username = document.getElementById( "username" ), password = document.getElementById( "password" ), - makeTitle = document.getElementById( "make-title" ), - makeDescription = document.getElementById( "make-description" ), - makeAuthor = document.getElementById( "make-author" ), - makeEmail = document.getElementById( "make-email" ), - makeContentType = document.getElementById( "make-content-type" ), - makeLocale = document.getElementById( "make-locale" ), - makeUrl = document.getElementById( "make-url" ), - makeThumbnail = document.getElementById( "make-thumbnail" ), - makeTags = document.getElementById( "make-tags" ), searchTags = document.getElementById( "tags" ), searchAuthor = document.getElementById( "author" ), size = document.getElementById( "size" ), page = document.getElementById( "page" ), makeTagPrefix = document.getElementById( "tag-prefix" ), - makeId = document.getElementById( "make-id" ), idSearch = document.getElementById( "search-make-id" ), sortBy = document.getElementById( "sort-field" ), - webmakerID = document.getElementById( "webmaker-id" ), - makeResult = document.getElementById( "make-result" ), - searchResult = document.getElementById( "search-result" ), - appTags = document.getElementById( "app-tags" ); + searchResult = document.getElementById( "search-result" ); function make() { var url = makeURL.value || ""; @@ -51,7 +38,7 @@ searchResult.value = JSON.stringify( error, null, 2 ); return; } - searchResult.value = JSON.stringify( data.hits, null, 2 ); + searchResult.value = JSON.stringify( data, null, 2 ); }); }; @@ -70,7 +57,7 @@ searchResult.value = JSON.stringify( error, null, 2 ); return; } - searchResult.value = JSON.stringify( data.hits, null, 2 ); + searchResult.value = JSON.stringify( data, null, 2 ); }); }; @@ -97,7 +84,7 @@ searchResult.value = JSON.stringify( error, null, 2 ); return; } - searchResult.value = JSON.stringify( data.hits, null, 2 ); + searchResult.value = JSON.stringify( data, null, 2 ); }); }; @@ -132,18 +119,4 @@ } } - window.createMake = function() { - make() - .create( getData(), handleResponse ); - }; - - window.updateMake = function() { - make() - .update( makeId.value, getData(), handleResponse ); - }; - - window.deleteMake = function() { - make() - .remove( makeId.value, handleResponse ); - }; }, false ); diff --git a/public/search.html b/public/search.html index 69be38a..bbff91e 100644 --- a/public/search.html +++ b/public/search.html @@ -6,14 +6,13 @@ <html lang="en"> <head> <meta charset="utf-8"> - <title> - + Demo Search Page - - - + + +

Make API Client Library Test Page

@@ -70,54 +69,5 @@

Search



- -

Create / Update / Delete

-
- Title -
-
- Description -
-
- Author -
-
- Email -
-
- ContentType - -
-
- Locale -
-
- Url -
-
- Thumbnail -
-
- Tags(CSV) -
-
- AppTags(CSV) -
-
-
- ID(update and delete) -
-
- webmaker ID: -
-
-
-
- -

Response Text

- diff --git a/routes/make.js b/routes/make.js index 37e47fe..d8b0f2b 100644 --- a/routes/make.js +++ b/routes/make.js @@ -14,7 +14,7 @@ module.exports = function( makeCtor, env ) { function handleError( res, err, code, type ){ metrics.increment( "make." + type + ".error" ); - return res.json( { error: err }, code ); + res.json( code, { error: err } ); } function handleSave( resp, err, make, type ){ @@ -26,34 +26,24 @@ module.exports = function( makeCtor, env ) { } } - return { - create: function( req, res ) { - var make = new Make(); - - for ( var i in Make.publicFields ) { - var field = Make.publicFields[ i ]; - make[ field ] = req.body.make[ field ]; - } + function updateFields( res, make, body, type ) { + Make.publicFields.forEach( function( field ) { + make[ field ] = body[ field ] || null; + }); - make.createdAt = Date.now(); + make.createdAt = Date.now(); + make.save(function( err, make ){ + return handleSave( res, err, make, type ); + }); + } - make.save(function( err, make ){ - return handleSave( res, err, make, "create" ); - }); + return { + create: function( req, res ) { + updateFields( res, new Make(), req.body.make, "create" ); }, update: function( req, res ) { Make.findById( req.params.id ).where( "deletedAt", null ).exec(function( err, make ) { - for ( var i in Make.publicFields ) { - var field = Make.publicFields[ i ]; - if ( req.body.make[ field ] ) { - make[ field ] = req.body.make[ field ]; - } - } - make.updatedAt = ( new Date() ).getTime(); - - make.save(function( err, make ){ - handleSave( res, err, make, "update" ); - }); + updateFields( res, make, req.body.make, "update" ); }); }, remove: function( req, res ) { diff --git a/test/fake.js b/test/fake.js deleted file mode 100644 index 7dd980b..0000000 --- a/test/fake.js +++ /dev/null @@ -1,60 +0,0 @@ -var Faker = require( "Faker" ), - IMG_CATEGORIES = [ - "abstract", - "animals", - "business", - "cats", - "city", - "foodnight", - "life", - "fashion", - "people", - "nature", - "sports", - "technics", - "transport" - ]; - -function randomDate(start, end) { - return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())) -} - -function maybe( chance, fn ) { - var n = ( Faker.random.number( chance ) ); - if ( n === 0 ) { fn(); } else { - return false; - } -} - - -function makeFake() { - var fakeData = {}; - - fakeData.title = Faker.random.catch_phrase_adjective() + " " + Faker.random.bs_adjective() + " " + Faker.random.bs_noun(); - fakeData.author = Faker.Name.findName(); - fakeData.description = Faker.Lorem.paragraph(); - fakeData.thumbnail = "http://www.lorempixel.com/640/350/" + Faker.Helpers.randomize( IMG_CATEGORIES ) + "/" + Faker.random.number( 10 ); - fakeData.contentType = Faker.Helpers.randomize( [ "application/x-butter", "application/x-thimble", "text/html" ] ); - fakeData.locale = Faker.Helpers.randomize(["en_us","en_ca","en_gb"]); - fakeData.url = "http://www.webmaker.org/" + Faker.random.number( 99999999999 ); - fakeData.tags = []; - fakeData.email = Faker.Helpers.randomize([ - "matts@mozillafoundation.org", "kate@mozillafoundation.org", "jbuck@mozillafoundation.org", - "scott@mozillafoundation.org", "surman@mozillafoundation.org", "pomax@mozillafoundation.org", - Faker.Internet.email() - ]); - - // Type - fakeData.tags.push( "makeType:" + Faker.Helpers.randomize( [ "thimble", "popcorn", "challenge", "event", "kit", "demo" ] ) ); - // Featured? - maybe( 5, function() { - fakeData.tags.push( "featured" ); - }); - maybe( 10, function() { - fakeData.tags.push( "tutorial" ); - }); - return fakeData; -} - -module.exports = makeFake; - diff --git a/test/fake/FakeAPI.js b/test/fake/FakeAPI.js new file mode 100755 index 0000000..5ad2fe3 --- /dev/null +++ b/test/fake/FakeAPI.js @@ -0,0 +1,127 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * A Fake MakeAPI server (i.e., FakeAPI). The idea is to simulate MongoDB + * and Elastic Search, while still retaining the routes and internal logic. + * The FakeAPI server is not as full-featured as the MakeAPI, espcially + * when it comes to complex searches in Elastic Search. Currently, only + * searching by ID (i.e., _id) and Tags is supported. + * + * This module exposes two functions, `start` and `stop`. These are used + * to control an instance of the FakeAPI. The `start` function can optionally + * be passed a `port` (5123 is the default), and a number of `fakes`, which + * will cause fake documents to be generated and inserted (see counterfeit.js). + * + * You can see an example of how to interact with the server and use + * make-api.js in `example.js`. + */ +var express = require( "express" ), + habitat = require( "habitat" ), + server, + Fogin, + Fake = require( "./fake.js" ), + Make = require( "../../public/js/make-api.js" ), + counterfeit = require( "./counterfeit.js" ); + +module.exports = { + start: function( options, callback ) { + options = options || {}; + // Default basic auth creds + options.username = options.username || "user"; + options.password = options.password || "password"; + callback = callback || function(){}; + + var port = options.port || 5123, + foginPort = port + 1, + foginUrl = "http://" + options.username + ":" + options.password + "@localhost:" + foginPort, + fakes = options.fakes; + + Fogin = require( "webmaker-loginapi" )( foginUrl ).Fogin, + + // Export faked env vars expected for basic auth integration (see lib/maker.js) + process.env.ALLOWED_USERS = options.username + ":" + options.password; + process.env.LOGIN_SERVER_URL_WITH_AUTH = "http://" + options.username + ":" + options.password + + "@localhost:" + foginPort; + + function startFakeAPI() { + habitat.load(); + + var app = express(), + env = new habitat(), + routes = require( "../../routes/make.js" )( Fake, env ), + middleware = require( "../../lib/middleware.js" )( env ); + + app.use( express.logger( "dev" ) ); + app.use( express.bodyParser() ); + + app.post( "/api/make", express.basicAuth( middleware.authenticateUser ), + middleware.prefixAuth, + routes.create ); + app.put( "/api/make/:id", express.basicAuth( middleware.authenticateUser ), + middleware.prefixAuth, routes.update ); + app.del( "/api/make/:id", express.basicAuth( middleware.authenticateUser ), + middleware.prefixAuth, routes.remove ); + app.get( "/api/makes/search", function crossOrigin( req, res, next ) { + res.header( "Access-Control-Allow-Origin", "*" ); + next(); + }, routes.search ); + app.options( "/api/makes/search", function( req, res ) { + res.header( "Access-Control-Allow-Origin", "*" ); + res.header( "Access-Control-Allow-Headers", "Content-Type" ); + res.send( 200 ); + }); + + server = app.listen( port, function( req, res ) { + if ( !fakes ) { + callback( req, res ); + return; + } + var make = Make({ + apiURL: "http://localhost:" + port, + auth: options.username + ":" + options.password + }); + + function createFake() { + var fake = counterfeit.createFake(); + make.create({ + maker: fake.email, + make: fake + }, function( err, make ) { + if( !--fakes ) { + callback( req, res ); + return; + } + createFake(); + }); + } + createFake(); + }); + } + + // Spin up a fake login server too + Fogin.start({ + port: port + 1, + username: options.username, + password: options.password, + logins: counterfeit.users.map( function( email ) { + return { + email: email, + fullName: counterfeit.createName(), + // TODO: randomize this... + isAdmin: true + }; + }) + }, startFakeAPI ); + }, + + stop: function() { + if ( !server ) { + return; + } + server.close(); + Fogin.stop(); + server = null; + } +}; diff --git a/test/fake/counterfeit.js b/test/fake/counterfeit.js new file mode 100644 index 0000000..68521ab --- /dev/null +++ b/test/fake/counterfeit.js @@ -0,0 +1,75 @@ +var Faker = require( "Faker" ), + IMG_CATEGORIES = [ + "abstract", + "animals", + "business", + "cats", + "city", + "foodnight", + "life", + "fashion", + "people", + "nature", + "sports", + "technics", + "transport" + ]; + +function randomDate(start, end) { + return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())); +} + +function maybe( chance, fn ) { + var n = ( Faker.random.number( chance ) ); + if ( n === 0 ) { + fn(); + } else { + return false; + } +} + +// Stable list of user emails +var users = [ + "matts@mozillafoundation.org", "kate@mozillafoundation.org", "jbuck@mozillafoundation.org", + "scott@mozillafoundation.org", "surman@mozillafoundation.org", "pomax@mozillafoundation.org" + ]; + +function createEmail() { + return Faker.Helpers.randomize( users ); +} + +function createName() { + return Faker.Name.findName(); +} + +function createFake() { + var fakeData = {}; + + fakeData.title = Faker.random.catch_phrase_adjective() + " " + Faker.random.bs_adjective() + " " + Faker.random.bs_noun(); + fakeData.author = createName(); + fakeData.description = Faker.Lorem.paragraph(); + fakeData.thumbnail = "http://www.lorempixel.com/640/350/" + Faker.Helpers.randomize( IMG_CATEGORIES ) + "/" + Faker.random.number( 10 ); + fakeData.contentType = Faker.Helpers.randomize( [ "application/x-butter", "application/x-thimble", "text/html" ] ); + fakeData.locale = Faker.Helpers.randomize(["en_us","en_ca","en_gb"]); + fakeData.url = "http://www.webmaker.org/" + Faker.random.number( 99999999999 ); + fakeData.tags = []; + fakeData.email = createEmail(); + + // Type + fakeData.tags.push( "makeType:" + Faker.Helpers.randomize( [ "thimble", "popcorn", "challenge", "event", "kit", "demo" ] ) ); + // Featured? + maybe( 5, function() { + fakeData.tags.push( "featured" ); + }); + maybe( 10, function() { + fakeData.tags.push( "tutorial" ); + }); + return fakeData; +} + +module.exports = { + createFake: createFake, + createEmail: createEmail, + createName: createName, + users: users +}; diff --git a/test/fake/example.js b/test/fake/example.js new file mode 100644 index 0000000..5e48cd2 --- /dev/null +++ b/test/fake/example.js @@ -0,0 +1,47 @@ +var FakeAPI = require("./FakeAPI.js"), + Make = require("../../public/js/make-api.js"), + port = 5124, + username = "username", + password = "secret"; + +/** + * Simple demo of using the FakeAPI. Start a FakeAPI server + * on port 5124 and create 50 fake makes. + */ +FakeAPI.start({ + port: port, + username: username, + password: password, + fakes: 50 +}, function(){ + + function make() { + return Make({ + apiURL: "http://localhost:" + port, + auth: username + ":" + password + }); + } + + // Find all the makes tagged "tutorial" + make().tags("tutorial").then( function( err, results ) { + // Display the results we get back: + console.log( results ); + + // Get the first make in the results + var result0 = results[ 0 ]; + + // Try getting the same make by _id + make().id( result0._id ).then( function( err, _idResults ) { + console.log("_id results", _idResults); + }); + + // Try getting the same make by URL + make().url( result0.url ).then( function( err, urlResults ) { + console.log("url results", urlResults); + + var doc = urlResults[ 0 ]; + doc.url = "new url"; + doc.update(); + }); + }); +}); diff --git a/test/fake/fake.js b/test/fake/fake.js new file mode 100644 index 0000000..00f0422 --- /dev/null +++ b/test/fake/fake.js @@ -0,0 +1,182 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var mongo = {}, + seed = Date.now(); + +/** + * Format search results like ES does + */ +function formatResults( results ) { + results = results || []; + if ( !Array.isArray( results ) ) { + results = [ results ]; + } + return { hits: results }; +} + +function finder( _id ) { + var whereKey, whereVal; + + function where( key, val ) { + whereKey = key; + whereVal = val; + return { + exec: exec + }; + } + + function exec( callback ) { + var doc = mongo[ _id ]; + // == so we deal with undefined vs. null + if ( !whereKey || doc [ whereKey ] == whereVal ) { + callback( null, doc ); + return; + } + callback( null, null ); + } + + return { + where: where, + exec: exec + }; +} + +function Make(){} + +Make.publicFields = [ "url", "contentType", "locale", "locales", "title", + "description", "author", "published", "tags", + "thumbnail", "email", "remixedFrom" ]; +/** + * We support only a tiny subset of the ES DSL, specifically searching + * by ID and searching for tags. + * + * A search for ID=1234 looks like this: + * { + * query: { + * filtered: { + * query: { + * match_all: {} + * }, + * filter: { + * and: [{ + * query: { + * field: { + * _id: 1234 + * } + * } + * } + * ] + * } + * } + * }, + * size: 10, + * from: 0 + * } + * + * A search for the `foo` and `bar` tags looks like this: + * s = { + * query: { + * filtered: { + * query: { + * match_all: {} + * }, + * filter: { + * and: [{ + * terms: { + * tags: [foo, bar], + * execution: and + * } + * } + * ] + * } + * } + * }, + * size: 10, + * from: 0 + * } + */ +function findByTags( tags ) { + var results = []; + + function matchTags( allTags, searchTags ) { + allTags = allTags || []; + return searchTags.every( function( tag ) { + return allTags.indexOf( tag ) > -1; + }); + } + + Object.keys( mongo ).forEach( function( key ) { + var doc = mongo[ key ]; + if ( matchTags( doc.tags, tags ) ) { + results.push( doc ); + } + }); + + return results; +} + +function findByTerm( term ) { + var name = Object.keys( term )[ 0 ], + value = term[ name ], + results = []; + + Object.keys( mongo ).forEach( function( key ) { + var doc = mongo[ key ]; + if ( doc[ name ] === value ) { + results.push( doc ); + } + }); + return results; + +} + +function dslParse( searchData, callback ) { + // In both the ID and Tags cases, we only care about the first `and` term + var and0; + try { + and0 = searchData.query.filtered.filter.and[ 0 ]; + } catch( err ) { + callback( "Error: ES DSL syntax was not formatted correctly for FakeAPI." ); + return; + } + + // Find by ID + if ( and0.query && + and0.query.field && + and0.query.field._id ) { + Make.findById( and0.query.field._id ).exec( callback ); + } + // Find by Tags + else if ( and0.terms && + and0.terms.tags ) { + callback( null, formatResults( findByTags( and0.terms.tags ) ) ); + } + // Find by term (e.g., URL). + else if ( and0.term ) + callback( null, formatResults( findByTerm( and0.term ) ) ); + // Everything else is unsupported...until you add it ;) + else { + callback( "Error: this use of the ES DSL is unsupported in FakeAPI." ); + } +} + +Make.search = function( searchData, callback ){ + // We support a tiny subset of https://github.com/jamescarr/mongoosastic#advanced-queries + dslParse( searchData, callback ); +}; + +Make.findById = function( _id ) { + return finder( _id ); +}; + +Make.prototype.save = function( callback ) { + this._id = seed++; + this.createdAt = Date.now(); + this.updatedAt = Date.now(); + mongo[ this._id ] = this; + callback( null, this ); +}; + +module.exports = Make; diff --git a/test/post.js b/test/post.js deleted file mode 100644 index f80c7e3..0000000 --- a/test/post.js +++ /dev/null @@ -1,53 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -var querystring = require('querystring'), - http = require('http'), - fs = require('fs'), - fakeIt = require('./fake'), - habitat = require('habitat'), - - auth = process.env.AUTH, - - postData, postOptions, postReq, - - numberOfRecords = 1000; - -habitat.load(); - -if ( !auth ) { - console.error( "ERROR: You need credentials to write to the database. " + - "Specify the AUTH environment variable" ); - return; -} - -for ( var i = 0; i < numberOfRecords; i++ ) { - - var postData = querystring.stringify({ - make: querystring.stringify( fakeIt() ), - maker: "cade" - }); - - postOptions = { - auth: auth, - host: process.env.HOST, - port: process.env.PORT, - path: '/api/make', - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': postData.length - } - }; - - postReq = http.request( postOptions, function ( res ) { - res.setEncoding( 'utf8' ); - res.on( 'data', function ( chunk ) { - console.log( 'Response: ' + chunk + '\n' ); - }); - }); - - postReq.write( postData ); - postReq.end(); -} diff --git a/test/test-make-client.js b/test/test-make-client.js index 0b3c95a..0f8c30d 100644 --- a/test/test-make-client.js +++ b/test/test-make-client.js @@ -37,7 +37,7 @@ app.use( express.bodyParser() ); app.use( express.cookieParser() ); function randomDate(start, end) { - return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())) + return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())); } app.get( "/", function( req, res ) { From 6275fb4bff8834d0278d784f4cde428b9593ab24 Mon Sep 17 00:00:00 2001 From: "David Humphrey (:humph) david.humphrey@senecacollege.ca" Date: Thu, 16 May 2013 14:00:44 -0400 Subject: [PATCH 03/34] Remove things needed for FakeAPI from .npmignore --- .npmignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.npmignore b/.npmignore index a9168a5..ae555f7 100644 --- a/.npmignore +++ b/.npmignore @@ -3,10 +3,8 @@ .gitignore Gruntfile.js server.js -lib/* node_modules/* public/js/search.js public/stylesheets/* routes/* views/* -test/* From b2fa6f9fe82075bb9fbd981e680b3be04c54f691 Mon Sep 17 00:00:00 2001 From: "David Humphrey (:humph) david.humphrey@senecacollege.ca" Date: Thu, 16 May 2013 14:03:13 -0400 Subject: [PATCH 04/34] Bump version for npm changes --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7ac9eec..128fb0a 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "makeapi", - "version": "0.1.0", + "version": "0.1.1", "description": "MakeAPI for Webmaker", "main": "./lib/api.js", "scripts": { @@ -28,6 +28,7 @@ "license": "MPL-2.0", "dependencies": { "express": "~3.0.6", + "Faker": "0.5.8", "habitat": "0.4.1", "mongoose": "~3.5.2", "mongoose-validator": "~0.2.1", @@ -40,7 +41,6 @@ "webmaker-loginapi": "0.1.0" }, "devDependencies": { - "Faker": "0.5.8", "grunt": "~0.4.1", "grunt-contrib-csslint": "~0.1.2", "grunt-contrib-jshint": "~0.4.3" From 5e1cf1c82506ae2463627bddaf91967eeefbef90 Mon Sep 17 00:00:00 2001 From: "David Humphrey (:humph) david.humphrey@senecacollege.ca" Date: Thu, 16 May 2013 14:14:39 -0400 Subject: [PATCH 05/34] Remove routes/* from .npmignore needed by FakeAPI, bump version --- .npmignore | 1 - package.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.npmignore b/.npmignore index ae555f7..c0d0ad9 100644 --- a/.npmignore +++ b/.npmignore @@ -6,5 +6,4 @@ server.js node_modules/* public/js/search.js public/stylesheets/* -routes/* views/* diff --git a/package.json b/package.json index 128fb0a..ce7a8ef 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "makeapi", - "version": "0.1.1", + "version": "0.1.2", "description": "MakeAPI for Webmaker", "main": "./lib/api.js", "scripts": { From c59fb463bd188ba04b6a3b932c1b9d39ef887a77 Mon Sep 17 00:00:00 2001 From: Christopher De Cairos Date: Thu, 16 May 2013 14:52:50 -0400 Subject: [PATCH 06/34] Fixes Bug 873174 - fix bad comparison for application tags --- lib/tags.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tags.js b/lib/tags.js index 4271b8f..e45f510 100644 --- a/lib/tags.js +++ b/lib/tags.js @@ -56,7 +56,7 @@ module.exports = function() { // Allow if is application tag, and the application tag matches the // username of the app making the request - if ( appTag && appTag === application ) { + if ( appTag && appTag[ 1 ] === application ) { return true; } From f7440244d194265cbda079bc32d94c6f41278156 Mon Sep 17 00:00:00 2001 From: "David Humphrey (:humph) david.humphrey@senecacollege.ca" Date: Thu, 16 May 2013 16:27:43 -0400 Subject: [PATCH 07/34] Add callback arg to FakeAPI.stop() --- package.json | 4 ++-- test/fake/FakeAPI.js | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index ce7a8ef..7e78185 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "makeapi", - "version": "0.1.2", + "version": "0.1.3", "description": "MakeAPI for Webmaker", "main": "./lib/api.js", "scripts": { @@ -38,7 +38,7 @@ "nunjucks": "~0.1.8a", "request": "2.20.0", "querystring": "0.2.0", - "webmaker-loginapi": "0.1.0" + "webmaker-loginapi": "0.1.1" }, "devDependencies": { "grunt": "~0.4.1", diff --git a/test/fake/FakeAPI.js b/test/fake/FakeAPI.js index 5ad2fe3..0c1d62f 100755 --- a/test/fake/FakeAPI.js +++ b/test/fake/FakeAPI.js @@ -116,12 +116,14 @@ module.exports = { }, startFakeAPI ); }, - stop: function() { + stop: function( callback ) { + callback = callback || function(){}; if ( !server ) { return; } - server.close(); - Fogin.stop(); - server = null; + server.close( function() { + server = null; + Fogin.stop( callback ); + }); } }; From b6d323ab6828c385fb31e0f54a8ba022c204ff04 Mon Sep 17 00:00:00 2001 From: Jon Buckley Date: Fri, 17 May 2013 09:27:20 -0400 Subject: [PATCH 08/34] 0.1.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7e78185..d6bdf45 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "makeapi", - "version": "0.1.3", + "version": "0.1.4", "description": "MakeAPI for Webmaker", "main": "./lib/api.js", "scripts": { From fbf33d17ae71d7ba627f3b89084fd35e92bc7e14 Mon Sep 17 00:00:00 2001 From: Jon Buckley Date: Fri, 17 May 2013 13:56:30 -0400 Subject: [PATCH 09/34] 0.1.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d6bdf45..b948483 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "makeapi", - "version": "0.1.4", + "version": "0.1.5", "description": "MakeAPI for Webmaker", "main": "./lib/api.js", "scripts": { From 1876164607574fe8591654b43d85438a8fbbd8b9 Mon Sep 17 00:00:00 2001 From: Jon Buckley Date: Wed, 22 May 2013 16:24:10 -0400 Subject: [PATCH 10/34] No bug - Prevent localhost cookies from overwriting each other. r=me --- server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.js b/server.js index dc4a31a..08d91c0 100755 --- a/server.js +++ b/server.js @@ -36,7 +36,7 @@ app.use( express.static( path.join( __dirname + "/public" ) ) ); app.use( express.bodyParser() ); app.use( express.cookieParser() ); app.use( express.cookieSession({ - key: "express.sid", + key: "makeapi.sid", secret: env.get( "SESSION_SECRET" ), cookie: { maxAge: 2678400000 // 31 days. Persona saves session data for 1 month From b4a9df7cf75958e5a6c4c04a8b67cfd42f86a303 Mon Sep 17 00:00:00 2001 From: Matthew Schranz Date: Thu, 23 May 2013 13:20:26 -0400 Subject: [PATCH 11/34] Fix Bug 875389 - Allow for searching against the MakeAPI w/ contentType --- public/js/make-api.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/public/js/make-api.js b/public/js/make-api.js index 619ca01..10b5b87 100644 --- a/public/js/make-api.js +++ b/public/js/make-api.js @@ -338,6 +338,15 @@ var module = module || undefined; return this; }, + contentType: function( contentType ) { + this.searchFilters.push({ + term: { + contentType: contentType + } + }); + return this; + }, + id: function( id ) { this.searchFilters.push({ query: { From ca572b4599a0661be39f33b069cb2c6ca5133306 Mon Sep 17 00:00:00 2001 From: Matthew Schranz Date: Thu, 23 May 2013 14:01:40 -0400 Subject: [PATCH 12/34] 0.1.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b948483..8773532 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "makeapi", - "version": "0.1.5", + "version": "0.1.6", "description": "MakeAPI for Webmaker", "main": "./lib/api.js", "scripts": { From 97e871397b6e3121ddce5886729376da3c60ed99 Mon Sep 17 00:00:00 2001 From: scottdowne Date: Thu, 23 May 2013 11:47:57 -0400 Subject: [PATCH 13/34] [#875322] Expose a remixUrl in the makeapi. --- lib/models/make.js | 14 +++++++++++--- public/js/make-api.js | 6 +++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/models/make.js b/lib/models/make.js index 6721783..73a6722 100644 --- a/lib/models/make.js +++ b/lib/models/make.js @@ -33,6 +33,14 @@ module.exports = function( environment, mongoInstance ) { unique: true, es_index: "not_analyzed" }, + remixUrl: { + type: String, + es_indexed: true, + validate: validate({ + passIfEmpty: true + }, "isUrl" ), + es_index: "not_analyzed" + }, contentType: { type: String, es_indexed: true, @@ -123,9 +131,9 @@ module.exports = function( environment, mongoInstance ) { } }); - Make.publicFields = [ "url", "contentType", "locale", "locales", - "title", "description", "author", - "published", "tags", "thumbnail", "email", "remixedFrom" ]; + Make.publicFields = [ "url", "remixUrl", "contentType", "locale", "locales", + "title", "description", "author", "published", "tags", + "thumbnail", "email", "remixedFrom" ]; return Make; }; diff --git a/public/js/make-api.js b/public/js/make-api.js index 10b5b87..4c4a481 100644 --- a/public/js/make-api.js +++ b/public/js/make-api.js @@ -200,9 +200,9 @@ var module = module || undefined; }; // Extend wrapped with contents of make - [ "url", "contentType", "locale", "title", "description", - "author", "published", "tags", "thumbnail", "email", - "remixedFrom", "_id" ].forEach( function( prop ) { + [ "url", "remixUrl", "contentType", "locale", "title", + "description", "author", "published", "tags", "thumbnail", + "email", "remixedFrom", "_id" ].forEach( function( prop ) { wrapped[ prop ] = make[ prop ]; }); From 141951c118c09bdd4305a16997b5eec41165a2b4 Mon Sep 17 00:00:00 2001 From: scottdowne Date: Thu, 23 May 2013 16:57:09 -0400 Subject: [PATCH 14/34] [makeapi] v0.1.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8773532..3c0ac78 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "makeapi", - "version": "0.1.6", + "version": "0.1.7", "description": "MakeAPI for Webmaker", "main": "./lib/api.js", "scripts": { From f3b5d7c6163c71845f5360fc4ce2bf1eb6493bc1 Mon Sep 17 00:00:00 2001 From: Matthew Schranz Date: Mon, 27 May 2013 13:05:12 -0400 Subject: [PATCH 15/34] Fix Bug 876461 - Strip out remixUrl and fix bug with remixedFrom --- lib/models/make.js | 15 +++------------ public/js/make-api.js | 2 +- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/lib/models/make.js b/lib/models/make.js index 73a6722..c71ec1c 100644 --- a/lib/models/make.js +++ b/lib/models/make.js @@ -33,14 +33,6 @@ module.exports = function( environment, mongoInstance ) { unique: true, es_index: "not_analyzed" }, - remixUrl: { - type: String, - es_indexed: true, - validate: validate({ - passIfEmpty: true - }, "isUrl" ), - es_index: "not_analyzed" - }, contentType: { type: String, es_indexed: true, @@ -95,11 +87,10 @@ module.exports = function( environment, mongoInstance ) { es_type: "String" }, remixedFrom: { - type: Number, + type: String, "default": null, es_indexed: true, - es_index: "not_analyzed", - es_type: "long" + es_index: "not_analyzed" }, createdAt: Timestamp, updatedAt: Timestamp, @@ -131,7 +122,7 @@ module.exports = function( environment, mongoInstance ) { } }); - Make.publicFields = [ "url", "remixUrl", "contentType", "locale", "locales", + Make.publicFields = [ "url", "contentType", "locale", "locales", "title", "description", "author", "published", "tags", "thumbnail", "email", "remixedFrom" ]; diff --git a/public/js/make-api.js b/public/js/make-api.js index 4c4a481..4cf8a28 100644 --- a/public/js/make-api.js +++ b/public/js/make-api.js @@ -200,7 +200,7 @@ var module = module || undefined; }; // Extend wrapped with contents of make - [ "url", "remixUrl", "contentType", "locale", "title", + [ "url", "contentType", "locale", "title", "description", "author", "published", "tags", "thumbnail", "email", "remixedFrom", "_id" ].forEach( function( prop ) { wrapped[ prop ] = make[ prop ]; From 2b33f85ed16247cb0059c7a3736d0a0d0068bcfe Mon Sep 17 00:00:00 2001 From: Matthew Schranz Date: Mon, 27 May 2013 11:07:48 -0400 Subject: [PATCH 16/34] Fix Bug 876419 - Search for projects remixed from another project --- public/js/make-api.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/public/js/make-api.js b/public/js/make-api.js index 4cf8a28..81df3e6 100644 --- a/public/js/make-api.js +++ b/public/js/make-api.js @@ -347,6 +347,15 @@ var module = module || undefined; return this; }, + remixedFrom: function( projectID ) { + this.searchFilters.push({ + term: { + remixedFrom: projectID + } + }); + return this; + }, + id: function( id ) { this.searchFilters.push({ query: { From 7551e3ae7e891ce460ca9bdf2f2947f4c1860f7d Mon Sep 17 00:00:00 2001 From: Matthew Schranz Date: Tue, 28 May 2013 10:18:45 -0400 Subject: [PATCH 17/34] 0.1.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3c0ac78..fb17fe4 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "makeapi", - "version": "0.1.7", + "version": "0.1.8", "description": "MakeAPI for Webmaker", "main": "./lib/api.js", "scripts": { From 2a87e489bbd028e8520ba1b86ddb89001cb49d6c Mon Sep 17 00:00:00 2001 From: Christopher De Cairos Date: Mon, 27 May 2013 15:52:04 -0400 Subject: [PATCH 18/34] Fix Bug 876407 - Search by username/subdomain, get maker username/subdomain in search results --- Gruntfile.js | 3 +- lib/maker.js | 5 +- lib/models/make.js | 6 +- package.json | 1 + public/js/make-api.js | 24 ++-- public/js/search.js | 52 +++---- public/search.html | 196 +++++++++++++++++++-------- public/stylesheets/bootstrap.min.css | 9 ++ routes/make.js | 75 ++++++++-- 9 files changed, 256 insertions(+), 115 deletions(-) create mode 100644 public/stylesheets/bootstrap.min.css diff --git a/Gruntfile.js b/Gruntfile.js index 19bc465..6f5648d 100755 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -4,7 +4,8 @@ module.exports = function( grunt ) { csslint: { files: [ - "public/**/*.css" + "public/gallery/**/*.css", + "public/stylesheets/search.css" ] }, jshint: { diff --git a/lib/maker.js b/lib/maker.js index 135c600..b3ee7e1 100644 --- a/lib/maker.js +++ b/lib/maker.js @@ -2,9 +2,10 @@ var loginAPI = require( "webmaker-loginapi"); module.exports = function() { var env = new require( "habitat" )(), - isAdmin = loginAPI( env.get( "LOGIN_SERVER_URL_WITH_AUTH" ) ).isAdmin; + api = loginAPI( env.get( "LOGIN_SERVER_URL_WITH_AUTH" ) ); return { - isAdmin: isAdmin + isAdmin: api.isAdmin, + getUser: api.getUser }; }; diff --git a/lib/models/make.js b/lib/models/make.js index c71ec1c..e8475bb 100644 --- a/lib/models/make.js +++ b/lib/models/make.js @@ -6,6 +6,7 @@ module.exports = function( environment, mongoInstance ) { var mongoosastic = require( "mongoosastic" ), validate = require( "mongoose-validator" ).validate, + deferred = require( "deferred" ), env = environment, url = require( "url" ), mongoose = mongoInstance, @@ -72,8 +73,7 @@ module.exports = function( environment, mongoInstance ) { required: true, validate: validate( "isEmail" ), es_indexed: true, - es_index: "not_analyzed", - select: false + es_index: "not_analyzed" }, published: { type: Boolean, @@ -124,7 +124,7 @@ module.exports = function( environment, mongoInstance ) { Make.publicFields = [ "url", "contentType", "locale", "locales", "title", "description", "author", "published", "tags", - "thumbnail", "email", "remixedFrom" ]; + "thumbnail", "remixedFrom" ]; return Make; }; diff --git a/package.json b/package.json index fb17fe4..8e9b21b 100755 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ ], "license": "MPL-2.0", "dependencies": { + "deferred": "0.6.4", "express": "~3.0.6", "Faker": "0.5.8", "habitat": "0.4.1", diff --git a/public/js/make-api.js b/public/js/make-api.js index 81df3e6..6359a1f 100644 --- a/public/js/make-api.js +++ b/public/js/make-api.js @@ -191,10 +191,10 @@ var module = module || undefined; .then( callback ); }, - update: function( callback ) { + update: function( email, callback ) { callback = callback || function(){}; getMakeInstance() - .update( wrapped._id, { maker: wrapped.email, make: wrapped }, callback ); + .update( wrapped._id, { maker: email, make: wrapped }, callback ); } }; @@ -202,7 +202,7 @@ var module = module || undefined; // Extend wrapped with contents of make [ "url", "contentType", "locale", "title", "description", "author", "published", "tags", "thumbnail", - "email", "remixedFrom", "_id" ].forEach( function( prop ) { + "username", "remixedFrom", "_id" ].forEach( function( prop ) { wrapped[ prop ] = make[ prop ]; }); @@ -223,6 +223,7 @@ var module = module || undefined; var BASE_QUERY = { query: { filtered: { + filter: {}, query: { match_all: {} } @@ -235,6 +236,7 @@ var module = module || undefined; sortBy: [], size: DEFAULT_SIZE, pageNum: 1, + makerID: "", find: function( options ) { options = options || {}; @@ -257,12 +259,8 @@ var module = module || undefined; return this; }, - email: function( name ) { - this.searchFilters.push({ - term: { - email: name - } - }); + user: function( id ) { + this.makerID = id; return this; }, @@ -374,7 +372,6 @@ var module = module || undefined; searchQuery.from = ( this.pageNum - 1 ) * this.size; if ( this.searchFilters.length ) { - searchQuery.query.filtered.filter = {}; searchQuery.query.filtered.filter.and = this.searchFilters; } @@ -382,6 +379,11 @@ var module = module || undefined; searchQuery.sort = this.sortBy; } + if ( this.makerID ) { + searchQuery.makerID = this.makerID; + this.makerID = ""; + } + this.size = DEFAULT_SIZE; this.pageNum = 1; this.searchFilters = []; @@ -394,7 +396,7 @@ var module = module || undefined; callback( err ); } else { // Wrap resulting makes with some extra API. - var hits = data.hits || []; + var hits = data; for( var i = 0; i < hits.length; i++ ) { hits[ i ] = wrap( hits[ i ], options ); } diff --git a/public/js/search.js b/public/js/search.js index 1c54847..fee8dfd 100644 --- a/public/js/search.js +++ b/public/js/search.js @@ -5,8 +5,6 @@ document.addEventListener( "DOMContentLoaded", function() { var makeURL = document.getElementById( "makeURL" ), - username = document.getElementById( "username" ), - password = document.getElementById( "password" ), searchTags = document.getElementById( "tags" ), searchAuthor = document.getElementById( "author" ), size = document.getElementById( "size" ), @@ -14,16 +12,24 @@ makeTagPrefix = document.getElementById( "tag-prefix" ), idSearch = document.getElementById( "search-make-id" ), sortBy = document.getElementById( "sort-field" ), + makerID = document.getElementById( "search-make-username" ), searchResult = document.getElementById( "search-result" ); function make() { var url = makeURL.value || ""; return Make({ - apiURL: url, - auth: username.value + ":" + password.value + apiURL: url }); } + function processResult( error, data ) { + if ( error ) { + searchResult.value = JSON.stringify( error, null, 2 ); + } else { + searchResult.value = JSON.stringify( data, null, 2 ); + } + } + window.grabTags = function() { make() .tags({ @@ -33,13 +39,7 @@ .sortByField( sortBy.value, document.querySelector( "input[name='direction']:checked" ).value ) .limit( size.value ) .page( page.value || 1 ) - .then(function( error, data ) { - if ( error ) { - searchResult.value = JSON.stringify( error, null, 2 ); - return; - } - searchResult.value = JSON.stringify( data, null, 2 ); - }); + .then( processResult ); }; window.myProjects = function() { @@ -52,25 +52,13 @@ .page( page.value || 1 ) .author( searchAuthor.value ) .sortByField( sortBy.value, document.querySelector( "input[name='direction']:checked" ).value ) - .then(function( error, data ) { - if ( error ) { - searchResult.value = JSON.stringify( error, null, 2 ); - return; - } - searchResult.value = JSON.stringify( data, null, 2 ); - }); + .then( processResult ); }; window.findProject = function() { make() .find( { id: idSearch.value } ) - .then(function( error, data ) { - if ( error ) { - searchResult.value = JSON.stringify( error, null, 2 ); - return; - } - searchResult.value = JSON.stringify( data, null, 2 ); - }); + .then( processResult ); }; window.prefixSearch = function() { @@ -79,13 +67,13 @@ .limit( size.value ) .page( page.value || 1 ) .sortByField( sortBy.value, document.querySelector( "input[name='direction']:checked" ).value ) - .then(function( error, data ) { - if ( error ) { - searchResult.value = JSON.stringify( error, null, 2 ); - return; - } - searchResult.value = JSON.stringify( data, null, 2 ); - }); + .then( processResult ); + }; + + window.usernameSearch = function() { + make() + .user( makerID.value ) + .then( processResult ); }; function getData() { diff --git a/public/search.html b/public/search.html index bbff91e..425402d 100644 --- a/public/search.html +++ b/public/search.html @@ -6,68 +6,150 @@ - Demo Search Page - + MakeAPI Search Tool - + - -

Make API Client Library Test Page

-
- MakeAPI URL: -
-
- Username: -
-
- Password: -
-

Search

-
- Tags(CSV) -
-
- Execution( > 1 tags ): -
- And - Or -
-
-
- Tag prefix -
-
- Author: -
-
- Number of Results: -
-
- Page: -
-
- Sort By Field: -
-
- sort direction: -
- asc - desc -
-
-
-
-
- -

-
- ID + +
+
+
+ + Make API Client Library Test Page + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +

search by tag prefix - i.e. "asdf" in "asdf:foo"

+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+
+
- -

- diff --git a/public/stylesheets/bootstrap.min.css b/public/stylesheets/bootstrap.min.css new file mode 100644 index 0000000..b6428e6 --- /dev/null +++ b/public/stylesheets/bootstrap.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap v2.3.2 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{width:auto\9;height:auto;max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img,.google-maps img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}label,select,button,input[type="button"],input[type="reset"],input[type="submit"],input[type="radio"],input[type="checkbox"]{cursor:pointer}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}@media print{*{color:#000!important;text-shadow:none!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) ")"}.ir a:after,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}@page{margin:.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#333;background-color:#fff}a{color:#08c;text-decoration:none}a:hover,a:focus{color:#005580;text-decoration:underline}.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.1)}.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span12{width:940px}.span11{width:860px}.span10{width:780px}.span9{width:700px}.span8{width:620px}.span7{width:540px}.span6{width:460px}.span5{width:380px}.span4{width:300px}.span3{width:220px}.span2{width:140px}.span1{width:60px}.offset12{margin-left:980px}.offset11{margin-left:900px}.offset10{margin-left:820px}.offset9{margin-left:740px}.offset8{margin-left:660px}.offset7{margin-left:580px}.offset6{margin-left:500px}.offset5{margin-left:420px}.offset4{margin-left:340px}.offset3{margin-left:260px}.offset2{margin-left:180px}.offset1{margin-left:100px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.127659574468085%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%}.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%}.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%}.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%}.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%}.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%}.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%}.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%}.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%}.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%}.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%}.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%}.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%}.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%}.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%}.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%}.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%}.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%}.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%}.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%}.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%}.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%}.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%}.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%}.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%}.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%}.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%}.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%}.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%}.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%}.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%}.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%}.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%}.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%}.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%}[class*="span"].hide,.row-fluid [class*="span"].hide{display:none}[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;line-height:0;content:""}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;line-height:0;content:""}.container-fluid:after{clear:both}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px}small{font-size:85%}strong{font-weight:bold}em{font-style:italic}cite{font-style:normal}.muted{color:#999}a.muted:hover,a.muted:focus{color:#808080}.text-warning{color:#c09853}a.text-warning:hover,a.text-warning:focus{color:#a47e3c}.text-error{color:#b94a48}a.text-error:hover,a.text-error:focus{color:#953b39}.text-info{color:#3a87ad}a.text-info:hover,a.text-info:focus{color:#2d6987}.text-success{color:#468847}a.text-success:hover,a.text-success:focus{color:#356635}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:bold;line-height:20px;color:inherit;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{line-height:40px}h1{font-size:38.5px}h2{font-size:31.5px}h3{font-size:24.5px}h4{font-size:17.5px}h5{font-size:14px}h6{font-size:11.9px}h1 small{font-size:24.5px}h2 small{font-size:17.5px}h3 small{font-size:14px}h4 small{font-size:14px}.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #eee}ul,ol{padding:0;margin:0 0 10px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}li{line-height:20px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}ul.inline,ol.inline{margin-left:0;list-style:none}ul.inline>li,ol.inline>li{display:inline-block;*display:inline;padding-right:5px;padding-left:5px;*zoom:1}dl{margin-bottom:20px}dt,dd{line-height:20px}dt{font-weight:bold}dd{margin-left:10px}.dl-horizontal{*zoom:1}.dl-horizontal:before,.dl-horizontal:after{display:table;line-height:0;content:""}.dl-horizontal:after{clear:both}.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}hr{margin:20px 0;border:0;border-top:1px solid #eee;border-bottom:1px solid #fff}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{margin-bottom:0;font-size:17.5px;font-weight:300;line-height:1.25}blockquote small{display:block;line-height:20px;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:20px;font-style:normal;line-height:20px}code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:12px;color:#333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}code{padding:2px 4px;color:#d14;white-space:nowrap;background-color:#f7f7f9;border:1px solid #e1e1e8}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:20px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}pre.prettyprint{margin-bottom:20px}pre code{padding:0;color:inherit;white-space:pre;white-space:pre-wrap;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}form{margin:0 0 20px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:15px;color:#999}label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px}input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:10px;font-size:14px;line-height:20px;color:#555;vertical-align:middle;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}input,textarea,.uneditable-input{width:206px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#fff;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-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 linear .2s,box-shadow linear .2s;-moz-transition:border linear .2s,box-shadow linear .2s;-o-transition:border linear .2s,box-shadow linear .2s;transition:border linear .2s,box-shadow linear .2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;*margin-top:0;line-height:normal}input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px}select{width:220px;background-color:#fff;border:1px solid #ccc}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.uneditable-input,.uneditable-textarea{color:#999;cursor:not-allowed;background-color:#fcfcfc;border-color:#ccc;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);box-shadow:inset 0 1px 2px rgba(0,0,0,0.025)}.uneditable-input{overflow:hidden;white-space:nowrap}.uneditable-textarea{width:auto;height:auto}input:-moz-placeholder,textarea:-moz-placeholder{color:#999}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#999}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#999}.radio,.checkbox{min-height:20px;padding-left:20px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-20px}.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}.input-mini{width:60px}.input-small{width:90px}.input-medium{width:150px}.input-large{width:210px}.input-xlarge{width:270px}.input-xxlarge{width:530px}input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0}.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:926px}input.span11,textarea.span11,.uneditable-input.span11{width:846px}input.span10,textarea.span10,.uneditable-input.span10{width:766px}input.span9,textarea.span9,.uneditable-input.span9{width:686px}input.span8,textarea.span8,.uneditable-input.span8{width:606px}input.span7,textarea.span7,.uneditable-input.span7{width:526px}input.span6,textarea.span6,.uneditable-input.span6{width:446px}input.span5,textarea.span5,.uneditable-input.span5{width:366px}input.span4,textarea.span4,.uneditable-input.span4{width:286px}input.span3,textarea.span3,.uneditable-input.span3{width:206px}input.span2,textarea.span2,.uneditable-input.span2{width:126px}input.span1,textarea.span1,.uneditable-input.span1{width:46px}.controls-row{*zoom:1}.controls-row:before,.controls-row:after{display:table;line-height:0;content:""}.controls-row:after{clear:both}.controls-row [class*="span"],.row-fluid .controls-row [class*="span"]{float:left}.controls-row .checkbox[class*="span"],.controls-row .radio[class*="span"]{padding-top:5px}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eee}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent}.control-group.warning .control-label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853}.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853}.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.control-group.error .control-label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48}.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48}.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.control-group.success .control-label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847}.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847}.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847}.control-group.info .control-label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad}.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad}.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3}.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#b94a48;border-color:#ee5f5b}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7}.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1}.form-actions:before,.form-actions:after{display:table;line-height:0;content:""}.form-actions:after{clear:both}.help-block,.help-inline{color:#595959}.help-block{display:block;margin-bottom:10px}.help-inline{display:inline-block;*display:inline;padding-left:5px;vertical-align:middle;*zoom:1}.input-append,.input-prepend{display:inline-block;margin-bottom:10px;font-size:0;white-space:nowrap;vertical-align:middle}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input,.input-append .dropdown-menu,.input-prepend .dropdown-menu,.input-append .popover,.input-prepend .popover{font-size:14px}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;vertical-align:top;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2}.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 #fff;background-color:#eee;border:1px solid #ccc}.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn,.input-append .btn-group>.dropdown-toggle,.input-prepend .btn-group>.dropdown-toggle{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append .active,.input-prepend .active{background-color:#a9dba9;border-color:#46a546}.input-prepend .add-on,.input-prepend .btn{margin-right:-1px}.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input+.btn-group .btn:last-child,.input-append select+.btn-group .btn:last-child,.input-append .uneditable-input+.btn-group .btn:last-child{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append .add-on,.input-append .btn,.input-append .btn-group{margin-left:-1px}.input-append .add-on:last-child,.input-append .btn:last-child,.input-append .btn-group:last-child>.dropdown-toggle{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append input+.btn-group .btn,.input-prepend.input-append select+.btn-group .btn,.input-prepend.input-append .uneditable-input+.btn-group .btn{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .btn-group:first-child{margin-left:0}input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;margin-bottom:0;vertical-align:middle;*zoom:1}.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none}.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block}.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0}.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0}.control-group{margin-bottom:10px}legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate}.form-horizontal .control-group{margin-bottom:20px;*zoom:1}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;line-height:0;content:""}.form-horizontal .control-group:after{clear:both}.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right}.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0}.form-horizontal .controls:first-child{*padding-left:180px}.form-horizontal .help-block{margin-bottom:0}.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block,.form-horizontal .uneditable-input+.help-block,.form-horizontal .input-prepend+.help-block,.form-horizontal .input-append+.help-block{margin-top:10px}.form-horizontal .form-actions{padding-left:180px}table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0}.table{width:100%;margin-bottom:20px}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #ddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.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 th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.table-bordered th,.table-bordered td{border-left:1px solid #ddd}.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child>th:first-child,.table-bordered tbody:first-child tr:first-child>td:first-child,.table-bordered tbody:first-child tr:first-child>th:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered thead:first-child tr:first-child>th:last-child,.table-bordered tbody:first-child tr:first-child>td:last-child,.table-bordered tbody:first-child tr:first-child>th:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-bordered thead:last-child tr:last-child>th:first-child,.table-bordered tbody:last-child tr:last-child>td:first-child,.table-bordered tbody:last-child tr:last-child>th:first-child,.table-bordered tfoot:last-child tr:last-child>td:first-child,.table-bordered tfoot:last-child tr:last-child>th:first-child{-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px}.table-bordered thead:last-child tr:last-child>th:last-child,.table-bordered tbody:last-child tr:last-child>td:last-child,.table-bordered tbody:last-child tr:last-child>th:last-child,.table-bordered tfoot:last-child tr:last-child>td:last-child,.table-bordered tfoot:last-child tr:last-child>th:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px}.table-bordered tfoot+tbody:last-child tr:last-child td:first-child{-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-moz-border-radius-bottomleft:0}.table-bordered tfoot+tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-bottomright:0}.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.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 td[class*="span"],table th[class*="span"],.row-fluid table td[class*="span"],.row-fluid table th[class*="span"]{display:table-cell;float:none;margin-left:0}.table td.span1,.table th.span1{float:none;width:44px;margin-left:0}.table td.span2,.table th.span2{float:none;width:124px;margin-left:0}.table td.span3,.table th.span3{float:none;width:204px;margin-left:0}.table td.span4,.table th.span4{float:none;width:284px;margin-left:0}.table td.span5,.table th.span5{float:none;width:364px;margin-left:0}.table td.span6,.table th.span6{float:none;width:444px;margin-left:0}.table td.span7,.table th.span7{float:none;width:524px;margin-left:0}.table td.span8,.table th.span8{float:none;width:604px;margin-left:0}.table td.span9,.table th.span9{float:none;width:684px;margin-left:0}.table td.span10,.table th.span10{float:none;width:764px;margin-left:0}.table td.span11,.table th.span11{float:none;width:844px;margin-left:0}.table td.span12,.table th.span12{float:none;width:924px;margin-left:0}.table tbody tr.success>td{background-color:#dff0d8}.table tbody tr.error>td{background-color:#f2dede}.table tbody tr.warning>td{background-color:#fcf8e3}.table tbody tr.info>td{background-color:#d9edf7}.table-hover tbody tr.success:hover>td{background-color:#d0e9c6}.table-hover tbody tr.error:hover>td{background-color:#ebcccc}.table-hover tbody tr.warning:hover>td{background-color:#faf2cc}.table-hover tbody tr.info:hover>td{background-color:#c4e3f3}[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;margin-top:1px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:focus>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>li>a:focus>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:focus>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"],.dropdown-submenu:focus>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png")}.icon-glass{background-position:0 0}.icon-music{background-position:-24px 0}.icon-search{background-position:-48px 0}.icon-envelope{background-position:-72px 0}.icon-heart{background-position:-96px 0}.icon-star{background-position:-120px 0}.icon-star-empty{background-position:-144px 0}.icon-user{background-position:-168px 0}.icon-film{background-position:-192px 0}.icon-th-large{background-position:-216px 0}.icon-th{background-position:-240px 0}.icon-th-list{background-position:-264px 0}.icon-ok{background-position:-288px 0}.icon-remove{background-position:-312px 0}.icon-zoom-in{background-position:-336px 0}.icon-zoom-out{background-position:-360px 0}.icon-off{background-position:-384px 0}.icon-signal{background-position:-408px 0}.icon-cog{background-position:-432px 0}.icon-trash{background-position:-456px 0}.icon-home{background-position:0 -24px}.icon-file{background-position:-24px -24px}.icon-time{background-position:-48px -24px}.icon-road{background-position:-72px -24px}.icon-download-alt{background-position:-96px -24px}.icon-download{background-position:-120px -24px}.icon-upload{background-position:-144px -24px}.icon-inbox{background-position:-168px -24px}.icon-play-circle{background-position:-192px -24px}.icon-repeat{background-position:-216px -24px}.icon-refresh{background-position:-240px -24px}.icon-list-alt{background-position:-264px -24px}.icon-lock{background-position:-287px -24px}.icon-flag{background-position:-312px -24px}.icon-headphones{background-position:-336px -24px}.icon-volume-off{background-position:-360px -24px}.icon-volume-down{background-position:-384px -24px}.icon-volume-up{background-position:-408px -24px}.icon-qrcode{background-position:-432px -24px}.icon-barcode{background-position:-456px -24px}.icon-tag{background-position:0 -48px}.icon-tags{background-position:-25px -48px}.icon-book{background-position:-48px -48px}.icon-bookmark{background-position:-72px -48px}.icon-print{background-position:-96px -48px}.icon-camera{background-position:-120px -48px}.icon-font{background-position:-144px -48px}.icon-bold{background-position:-167px -48px}.icon-italic{background-position:-192px -48px}.icon-text-height{background-position:-216px -48px}.icon-text-width{background-position:-240px -48px}.icon-align-left{background-position:-264px -48px}.icon-align-center{background-position:-288px -48px}.icon-align-right{background-position:-312px -48px}.icon-align-justify{background-position:-336px -48px}.icon-list{background-position:-360px -48px}.icon-indent-left{background-position:-384px -48px}.icon-indent-right{background-position:-408px -48px}.icon-facetime-video{background-position:-432px -48px}.icon-picture{background-position:-456px -48px}.icon-pencil{background-position:0 -72px}.icon-map-marker{background-position:-24px -72px}.icon-adjust{background-position:-48px -72px}.icon-tint{background-position:-72px -72px}.icon-edit{background-position:-96px -72px}.icon-share{background-position:-120px -72px}.icon-check{background-position:-144px -72px}.icon-move{background-position:-168px -72px}.icon-step-backward{background-position:-192px -72px}.icon-fast-backward{background-position:-216px -72px}.icon-backward{background-position:-240px -72px}.icon-play{background-position:-264px -72px}.icon-pause{background-position:-288px -72px}.icon-stop{background-position:-312px -72px}.icon-forward{background-position:-336px -72px}.icon-fast-forward{background-position:-360px -72px}.icon-step-forward{background-position:-384px -72px}.icon-eject{background-position:-408px -72px}.icon-chevron-left{background-position:-432px -72px}.icon-chevron-right{background-position:-456px -72px}.icon-plus-sign{background-position:0 -96px}.icon-minus-sign{background-position:-24px -96px}.icon-remove-sign{background-position:-48px -96px}.icon-ok-sign{background-position:-72px -96px}.icon-question-sign{background-position:-96px -96px}.icon-info-sign{background-position:-120px -96px}.icon-screenshot{background-position:-144px -96px}.icon-remove-circle{background-position:-168px -96px}.icon-ok-circle{background-position:-192px -96px}.icon-ban-circle{background-position:-216px -96px}.icon-arrow-left{background-position:-240px -96px}.icon-arrow-right{background-position:-264px -96px}.icon-arrow-up{background-position:-289px -96px}.icon-arrow-down{background-position:-312px -96px}.icon-share-alt{background-position:-336px -96px}.icon-resize-full{background-position:-360px -96px}.icon-resize-small{background-position:-384px -96px}.icon-plus{background-position:-408px -96px}.icon-minus{background-position:-433px -96px}.icon-asterisk{background-position:-456px -96px}.icon-exclamation-sign{background-position:0 -120px}.icon-gift{background-position:-24px -120px}.icon-leaf{background-position:-48px -120px}.icon-fire{background-position:-72px -120px}.icon-eye-open{background-position:-96px -120px}.icon-eye-close{background-position:-120px -120px}.icon-warning-sign{background-position:-144px -120px}.icon-plane{background-position:-168px -120px}.icon-calendar{background-position:-192px -120px}.icon-random{width:16px;background-position:-216px -120px}.icon-comment{background-position:-240px -120px}.icon-magnet{background-position:-264px -120px}.icon-chevron-up{background-position:-288px -120px}.icon-chevron-down{background-position:-313px -119px}.icon-retweet{background-position:-336px -120px}.icon-shopping-cart{background-position:-360px -120px}.icon-folder-close{width:16px;background-position:-384px -120px}.icon-folder-open{width:16px;background-position:-408px -120px}.icon-resize-vertical{background-position:-432px -119px}.icon-resize-horizontal{background-position:-456px -118px}.icon-hdd{background-position:0 -144px}.icon-bullhorn{background-position:-24px -144px}.icon-bell{background-position:-48px -144px}.icon-certificate{background-position:-72px -144px}.icon-thumbs-up{background-position:-96px -144px}.icon-thumbs-down{background-position:-120px -144px}.icon-hand-right{background-position:-144px -144px}.icon-hand-left{background-position:-168px -144px}.icon-hand-up{background-position:-192px -144px}.icon-hand-down{background-position:-216px -144px}.icon-circle-arrow-right{background-position:-240px -144px}.icon-circle-arrow-left{background-position:-264px -144px}.icon-circle-arrow-up{background-position:-288px -144px}.icon-circle-arrow-down{background-position:-312px -144px}.icon-globe{background-position:-336px -144px}.icon-wrench{background-position:-360px -144px}.icon-tasks{background-position:-384px -144px}.icon-filter{background-position:-408px -144px}.icon-briefcase{background-position:-432px -144px}.icon-fullscreen{background-position:-456px -144px}.dropup,.dropdown{position:relative}.dropdown-toggle{*margin-bottom:-3px}.dropdown-toggle:active,.open .dropdown-toggle{outline:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.dropdown .caret{margin-top:8px;margin-left:2px}.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;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus,.dropdown-submenu:hover>a,.dropdown-submenu:focus>a{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;outline:0;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.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;cursor:default;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open{*z-index:1000}.open>.dropdown-menu{display:block}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left: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 #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropup .dropdown-submenu>.dropdown-menu{top:auto;bottom:0;margin-top:0;margin-bottom:-2px;-webkit-border-radius:5px 5px 5px 0;-moz-border-radius:5px 5px 5px 0;border-radius:5px 5px 5px 0}.dropdown-submenu>a:after{display:block;float:right;width:0;height:0;margin-top:5px;margin-right:-10px;border-color:transparent;border-left-color:#ccc;border-style:solid;border-width:5px 0 5px 5px;content:" "}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:10px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.dropdown .dropdown-menu .nav-header{padding-right:20px;padding-left:20px}.typeahead{z-index:1051;margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-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-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-moz-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.collapse.in{height:auto}.close{float:right;font-size:20px;font-weight:bold;line-height:20px;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:.4;filter:alpha(opacity=40)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.btn{display:inline-block;*display:inline;padding:4px 12px;margin-bottom:0;*margin-left:.3em;font-size:14px;line-height:20px;color:#333;text-align:center;text-shadow:0 1px 1px rgba(255,255,255,0.75);vertical-align:middle;cursor:pointer;background-color:#f5f5f5;*background-color:#e6e6e6;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border:1px solid #ccc;*border:0;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);*zoom:1;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:focus,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover,.btn:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:11px 19px;font-size:17.5px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:11.9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:0 6px;font-size:10.5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;*background-color:#04c;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;*background-color:#f89406;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;*background-color:#bd362f;background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-repeat:repeat-x;border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;*background-color:#51a351;background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-repeat:repeat-x;border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;*background-color:#2f96b4;background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-repeat:repeat-x;border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;*background-color:#222;background-image:-moz-linear-gradient(top,#444,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-repeat:repeat-x;border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:focus,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{color:#08c;cursor:pointer;border-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover,.btn-link:focus{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus{color:#333;text-decoration:none}.btn-group{position:relative;display:inline-block;*display:inline;*margin-left:.3em;font-size:0;white-space:nowrap;vertical-align:middle;*zoom:1}.btn-group:first-child{*margin-left:0}.btn-group+.btn-group{margin-left:5px}.btn-toolbar{margin-top:10px;margin-bottom:10px;font-size:0}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group{margin-left:5px}.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn+.btn{margin-left:-1px}.btn-group>.btn,.btn-group>.dropdown-menu,.btn-group>.popover{font-size:14px}.btn-group>.btn-mini{font-size:10.5px}.btn-group>.btn-small{font-size:11.9px}.btn-group>.btn-large{font-size:17.5px}.btn-group>.btn:first-child{margin-left:0;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{*padding-top:5px;padding-right:8px;*padding-bottom:5px;padding-left:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn-group>.btn-mini+.dropdown-toggle{*padding-top:2px;padding-right:5px;*padding-bottom:2px;padding-left:5px}.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px}.btn-group>.btn-large+.dropdown-toggle{*padding-top:7px;padding-right:12px;*padding-bottom:7px;padding-left:12px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6}.btn-group.open .btn-primary.dropdown-toggle{background-color:#04c}.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406}.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f}.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351}.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222}.btn .caret{margin-top:8px;margin-left:0}.btn-large .caret{margin-top:6px}.btn-large .caret{border-top-width:5px;border-right-width:5px;border-left-width:5px}.btn-mini .caret,.btn-small .caret{margin-top:8px}.dropup .btn-large .caret{border-bottom-width:5px}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#fff;border-bottom-color:#fff}.btn-group-vertical{display:inline-block;*display:inline;*zoom:1}.btn-group-vertical>.btn{display:block;float:none;max-width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical>.btn+.btn{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.btn-group-vertical>.btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0}.btn-group-vertical>.btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.alert,.alert h4{color:#c09853}.alert h4{margin:0}.alert .close{position:relative;top:-2px;right:-21px;line-height:20px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-success h4{color:#468847}.alert-danger,.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-danger h4,.alert-error h4{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-info h4{color:#3a87ad}.alert-block{padding-top:14px;padding-bottom:14px}.alert-block>p,.alert-block>ul{margin-bottom:0}.alert-block p+p{margin-top:5px}.nav{margin-bottom:20px;margin-left:0;list-style:none}.nav>li>a{display:block}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li>a>img{max-width:none}.nav>.pull-right{float:right}.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase}.nav li+.nav-header{margin-top:9px}.nav-list{padding-right:15px;padding-left:15px;margin-bottom:0}.nav-list>li>a,.nav-list .nav-header{margin-right:-15px;margin-left:-15px;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.nav-list>li>a{padding:3px 15px}.nav-list>.active>a,.nav-list>.active>a:hover,.nav-list>.active>a:focus{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.2);background-color:#08c}.nav-list [class^="icon-"],.nav-list [class*=" icon-"]{margin-right:2px}.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.nav-tabs,.nav-pills{*zoom:1}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;line-height:0;content:""}.nav-tabs:after,.nav-pills:after{clear:both}.nav-tabs>li,.nav-pills>li{float:left}.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{margin-bottom:-1px}.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus{border-color:#eee #eee #ddd}.nav-tabs>.active>a,.nav-tabs>.active>a:hover,.nav-tabs>.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nav-pills>.active>a,.nav-pills>.active>a:hover,.nav-pills>.active>a:focus{color:#fff;background-color:#08c}.nav-stacked>li{float:none}.nav-stacked>li>a{margin-right:0}.nav-tabs.nav-stacked{border-bottom:0}.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-topleft:4px}.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomright:4px;-moz-border-radius-bottomleft:4px}.nav-tabs.nav-stacked>li>a:hover,.nav-tabs.nav-stacked>li>a:focus{z-index:2;border-color:#ddd}.nav-pills.nav-stacked>li>a{margin-bottom:3px}.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px}.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nav .dropdown-toggle .caret{margin-top:6px;border-top-color:#08c;border-bottom-color:#08c}.nav .dropdown-toggle:hover .caret,.nav .dropdown-toggle:focus .caret{border-top-color:#005580;border-bottom-color:#005580}.nav-tabs .dropdown-toggle .caret{margin-top:8px}.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.nav-tabs .active .dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.nav>.dropdown.active>a:hover,.nav>.dropdown.active>a:focus{cursor:pointer}.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover,.nav>li.dropdown.open.active>a:focus{color:#fff;background-color:#999;border-color:#999}.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret,.nav li.dropdown.open a:focus .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:1;filter:alpha(opacity=100)}.tabs-stacked .open>a:hover,.tabs-stacked .open>a:focus{border-color:#999}.tabbable{*zoom:1}.tabbable:before,.tabbable:after{display:table;line-height:0;content:""}.tabbable:after{clear:both}.tab-content{overflow:auto}.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.tabs-below>.nav-tabs{border-top:1px solid #ddd}.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0}.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.tabs-below>.nav-tabs>li>a:hover,.tabs-below>.nav-tabs>li>a:focus{border-top-color:#ddd;border-bottom-color:transparent}.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover,.tabs-below>.nav-tabs>.active>a:focus{border-color:transparent #ddd #ddd #ddd}.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none}.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px}.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd}.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.tabs-left>.nav-tabs>li>a:hover,.tabs-left>.nav-tabs>li>a:focus{border-color:#eee #ddd #eee #eee}.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover,.tabs-left>.nav-tabs .active>a:focus{border-color:#ddd transparent #ddd #ddd;*border-right-color:#fff}.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd}.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.tabs-right>.nav-tabs>li>a:hover,.tabs-right>.nav-tabs>li>a:focus{border-color:#eee #eee #eee #ddd}.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover,.tabs-right>.nav-tabs .active>a:focus{border-color:#ddd #ddd #ddd transparent;*border-left-color:#fff}.nav>.disabled>a{color:#999}.nav>.disabled>a:hover,.nav>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent}.navbar{*position:relative;*z-index:2;margin-bottom:20px;overflow:visible}.navbar-inner{min-height:40px;padding-right:20px;padding-left:20px;background-color:#fafafa;background-image:-moz-linear-gradient(top,#fff,#f2f2f2);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#f2f2f2));background-image:-webkit-linear-gradient(top,#fff,#f2f2f2);background-image:-o-linear-gradient(top,#fff,#f2f2f2);background-image:linear-gradient(to bottom,#fff,#f2f2f2);background-repeat:repeat-x;border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff2f2f2',GradientType=0);*zoom:1;-webkit-box-shadow:0 1px 4px rgba(0,0,0,0.065);-moz-box-shadow:0 1px 4px rgba(0,0,0,0.065);box-shadow:0 1px 4px rgba(0,0,0,0.065)}.navbar-inner:before,.navbar-inner:after{display:table;line-height:0;content:""}.navbar-inner:after{clear:both}.navbar .container{width:auto}.nav-collapse.collapse{height:auto;overflow:visible}.navbar .brand{display:block;float:left;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777;text-shadow:0 1px 0 #fff}.navbar .brand:hover,.navbar .brand:focus{text-decoration:none}.navbar-text{margin-bottom:0;line-height:40px;color:#777}.navbar-link{color:#777}.navbar-link:hover,.navbar-link:focus{color:#333}.navbar .divider-vertical{height:40px;margin:0 9px;border-right:1px solid #fff;border-left:1px solid #f2f2f2}.navbar .btn,.navbar .btn-group{margin-top:5px}.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn,.navbar .input-prepend .btn-group,.navbar .input-append .btn-group{margin-top:0}.navbar-form{margin-bottom:0;*zoom:1}.navbar-form:before,.navbar-form:after{display:table;line-height:0;content:""}.navbar-form:after{clear:both}.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px}.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0}.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px}.navbar-form .input-append,.navbar-form .input-prepend{margin-top:5px;white-space:nowrap}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0}.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0}.navbar-search .search-query{padding:4px 14px;margin-bottom:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.navbar-static-top{position:static;margin-bottom:0}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px}.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-right:0;padding-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.navbar-fixed-top{top:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:0 1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 10px rgba(0,0,0,0.1);box-shadow:0 1px 10px rgba(0,0,0,0.1)}.navbar-fixed-bottom{bottom:0}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:0 -1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 -1px 10px rgba(0,0,0,0.1);box-shadow:0 -1px 10px rgba(0,0,0,0.1)}.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0}.navbar .nav.pull-right{float:right;margin-right:0}.navbar .nav>li{float:left}.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777;text-decoration:none;text-shadow:0 1px 0 #fff}.navbar .nav .dropdown-toggle .caret{margin-top:8px}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{color:#333;text-decoration:none;background-color:transparent}.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#555;text-decoration:none;background-color:#e5e5e5;-webkit-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);box-shadow:inset 0 3px 8px rgba(0,0,0,0.125)}.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-right:5px;margin-left:5px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#ededed;*background-color:#e5e5e5;background-image:-moz-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f2f2f2),to(#e5e5e5));background-image:-webkit-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-o-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:linear-gradient(to bottom,#f2f2f2,#e5e5e5);background-repeat:repeat-x;border-color:#e5e5e5 #e5e5e5 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2',endColorstr='#ffe5e5e5',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075)}.navbar .btn-navbar:hover,.navbar .btn-navbar:focus,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#fff;background-color:#e5e5e5;*background-color:#d9d9d9}.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#ccc \9}.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,0.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.25)}.btn-navbar .icon-bar+.icon-bar{margin-top:3px}.navbar .nav>li>.dropdown-menu:before{position:absolute;top:-7px;left:9px;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:''}.navbar .nav>li>.dropdown-menu:after{position:absolute;top:-6px;left:10px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.navbar-fixed-bottom .nav>li>.dropdown-menu:before{top:auto;bottom:-7px;border-top:7px solid #ccc;border-bottom:0;border-top-color:rgba(0,0,0,0.2)}.navbar-fixed-bottom .nav>li>.dropdown-menu:after{top:auto;bottom:-6px;border-top:6px solid #fff;border-bottom:0}.navbar .nav li.dropdown>a:hover .caret,.navbar .nav li.dropdown>a:focus .caret{border-top-color:#333;border-bottom-color:#333}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{color:#555;background-color:#e5e5e5}.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777;border-bottom-color:#777}.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{right:12px;left:auto}.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{right:13px;left:auto}.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{right:100%;left:auto;margin-right:-1px;margin-left:0;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top,#222,#111);background-image:-webkit-gradient(linear,0 0,0 100%,from(#222),to(#111));background-image:-webkit-linear-gradient(top,#222,#111);background-image:-o-linear-gradient(top,#222,#111);background-image:linear-gradient(to bottom,#222,#111);background-repeat:repeat-x;border-color:#252525;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff111111',GradientType=0)}.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#999;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover,.navbar-inverse .brand:focus,.navbar-inverse .nav>li>a:focus{color:#fff}.navbar-inverse .brand{color:#999}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#fff;background-color:#111}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover,.navbar-inverse .navbar-link:focus{color:#fff}.navbar-inverse .divider-vertical{border-right-color:#222;border-left-color:#111}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{color:#fff;background-color:#111}.navbar-inverse .nav li.dropdown>a:hover .caret,.navbar-inverse .nav li.dropdown>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-search .search-query{color:#fff;background-color:#515151;border-color:#111;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#333;text-shadow:0 1px 0 #fff;background-color:#fff;border:0;outline:0;-webkit-box-shadow:0 0 3px rgba(0,0,0,0.15);-moz-box-shadow:0 0 3px rgba(0,0,0,0.15);box-shadow:0 0 3px rgba(0,0,0,0.15)}.navbar-inverse .btn-navbar{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e0e0e;*background-color:#040404;background-image:-moz-linear-gradient(top,#151515,#040404);background-image:-webkit-gradient(linear,0 0,0 100%,from(#151515),to(#040404));background-image:-webkit-linear-gradient(top,#151515,#040404);background-image:-o-linear-gradient(top,#151515,#040404);background-image:linear-gradient(to bottom,#151515,#040404);background-repeat:repeat-x;border-color:#040404 #040404 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515',endColorstr='#ff040404',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:focus,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#fff;background-color:#040404;*background-color:#000}.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#000 \9}.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.breadcrumb>li{display:inline-block;*display:inline;text-shadow:0 1px 0 #fff;*zoom:1}.breadcrumb>li>.divider{padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{margin:20px 0}.pagination ul{display:inline-block;*display:inline;margin-bottom:0;margin-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*zoom:1;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination ul>li{display:inline}.pagination ul>li>a,.pagination ul>li>span{float:left;padding:4px 12px;line-height:20px;text-decoration:none;background-color:#fff;border:1px solid #ddd;border-left-width:0}.pagination ul>li>a:hover,.pagination ul>li>a:focus,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#f5f5f5}.pagination ul>.active>a,.pagination ul>.active>span{color:#999;cursor:default}.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover,.pagination ul>.disabled>a:focus{color:#999;cursor:default;background-color:transparent}.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.pagination-centered{text-align:center}.pagination-right{text-align:right}.pagination-large ul>li>a,.pagination-large ul>li>span{padding:11px 19px;font-size:17.5px}.pagination-large ul>li:first-child>a,.pagination-large ul>li:first-child>span{-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.pagination-large ul>li:last-child>a,.pagination-large ul>li:last-child>span{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.pagination-mini ul>li:first-child>a,.pagination-small ul>li:first-child>a,.pagination-mini ul>li:first-child>span,.pagination-small ul>li:first-child>span{-webkit-border-bottom-left-radius:3px;border-bottom-left-radius:3px;-webkit-border-top-left-radius:3px;border-top-left-radius:3px;-moz-border-radius-bottomleft:3px;-moz-border-radius-topleft:3px}.pagination-mini ul>li:last-child>a,.pagination-small ul>li:last-child>a,.pagination-mini ul>li:last-child>span,.pagination-small ul>li:last-child>span{-webkit-border-top-right-radius:3px;border-top-right-radius:3px;-webkit-border-bottom-right-radius:3px;border-bottom-right-radius:3px;-moz-border-radius-topright:3px;-moz-border-radius-bottomright:3px}.pagination-small ul>li>a,.pagination-small ul>li>span{padding:2px 10px;font-size:11.9px}.pagination-mini ul>li>a,.pagination-mini ul>li>span{padding:0 6px;font-size:10.5px}.pager{margin:20px 0;text-align:center;list-style:none;*zoom:1}.pager:before,.pager:after{display:table;line-height:0;content:""}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#f5f5f5}.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;cursor:default;background-color:#fff}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop,.modal-backdrop.fade.in{opacity:.8;filter:alpha(opacity=80)}.modal{position:fixed;top:10%;left:50%;z-index:1050;width:560px;margin-left:-280px;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;outline:0;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.modal.fade{top:-25%;-webkit-transition:opacity .3s linear,top .3s ease-out;-moz-transition:opacity .3s linear,top .3s ease-out;-o-transition:opacity .3s linear,top .3s ease-out;transition:opacity .3s linear,top .3s ease-out}.modal.fade.in{top:10%}.modal-header{padding:9px 15px;border-bottom:1px solid #eee}.modal-header .close{margin-top:2px}.modal-header h3{margin:0;line-height:30px}.modal-body{position:relative;max-height:400px;padding:15px;overflow-y:auto}.modal-form{margin-bottom:0}.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;*zoom:1;-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff}.modal-footer:before,.modal-footer:after{display:table;line-height:0;content:""}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.tooltip{position:absolute;z-index:1030;display:block;font-size:11px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.8;filter:alpha(opacity=80)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0}.thumbnails{margin-left:-20px;list-style:none;*zoom:1}.thumbnails:before,.thumbnails:after{display:table;line-height:0;content:""}.thumbnails:after{clear:both}.row-fluid .thumbnails{margin-left:0}.thumbnails>li{float:left;margin-bottom:20px;margin-left:20px}.thumbnail{display:block;padding:4px;line-height:20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.055);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.055);box-shadow:0 1px 3px rgba(0,0,0,0.055);-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}a.thumbnail:hover,a.thumbnail:focus{border-color:#08c;-webkit-box-shadow:0 1px 4px rgba(0,105,214,0.25);-moz-box-shadow:0 1px 4px rgba(0,105,214,0.25);box-shadow:0 1px 4px rgba(0,105,214,0.25)}.thumbnail>img{display:block;max-width:100%;margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px;color:#555}.media,.media-body{overflow:hidden;*overflow:visible;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{margin-left:0;list-style:none}.label,.badge{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);white-space:nowrap;vertical-align:baseline;background-color:#999}.label{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding-right:9px;padding-left:9px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}.label:empty,.badge:empty{display:none}a.label:hover,a.label:focus,a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.label-important,.badge-important{background-color:#b94a48}.label-important[href],.badge-important[href]{background-color:#953b39}.label-warning,.badge-warning{background-color:#f89406}.label-warning[href],.badge-warning[href]{background-color:#c67605}.label-success,.badge-success{background-color:#468847}.label-success[href],.badge-success[href]{background-color:#356635}.label-info,.badge-info{background-color:#3a87ad}.label-info[href],.badge-info[href]{background-color:#2d6987}.label-inverse,.badge-inverse{background-color:#333}.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a}.btn .label,.btn .badge{position:relative;top:-1px}.btn-mini .label,.btn-mini .badge{top:0}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f7f7f7;background-image:-moz-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f9f9f9));background-image:-webkit-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-o-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);background-repeat:repeat-x;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress .bar{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top,#149bdf,#0480be);background-image:-webkit-gradient(linear,0 0,0 100%,from(#149bdf),to(#0480be));background-image:-webkit-linear-gradient(top,#149bdf,#0480be);background-image:-o-linear-gradient(top,#149bdf,#0480be);background-image:linear-gradient(to bottom,#149bdf,#0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15)}.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px}.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top,#ee5f5b,#c43c35);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#c43c35));background-image:-webkit-linear-gradient(top,#ee5f5b,#c43c35);background-image:-o-linear-gradient(top,#ee5f5b,#c43c35);background-image:linear-gradient(to bottom,#ee5f5b,#c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffc43c35',GradientType=0)}.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top,#62c462,#57a957);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#57a957));background-image:-webkit-linear-gradient(top,#62c462,#57a957);background-image:-o-linear-gradient(top,#62c462,#57a957);background-image:linear-gradient(to bottom,#62c462,#57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff57a957',GradientType=0)}.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top,#5bc0de,#339bb9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#339bb9));background-image:-webkit-linear-gradient(top,#5bc0de,#339bb9);background-image:-o-linear-gradient(top,#5bc0de,#339bb9);background-image:linear-gradient(to bottom,#5bc0de,#339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff339bb9',GradientType=0)}.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0)}.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.accordion{margin-bottom:20px}.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.accordion-heading{border-bottom:0}.accordion-heading .accordion-toggle{display:block;padding:8px 15px}.accordion-toggle{cursor:pointer}.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5}.carousel{position:relative;margin-bottom:20px;line-height:1}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-moz-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-align:center;background:#222;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;filter:alpha(opacity=50)}.carousel-control.right{right:15px;left:auto}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-indicators{position:absolute;top:15px;right:15px;z-index:5;margin:0;list-style:none}.carousel-indicators li{display:block;float:left;width:10px;height:10px;margin-left:5px;text-indent:-999px;background-color:#ccc;background-color:rgba(255,255,255,0.25);border-radius:5px}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:0;bottom:0;left:0;padding:15px;background:#333;background:rgba(0,0,0,0.75)}.carousel-caption h4,.carousel-caption p{line-height:20px;color:#fff}.carousel-caption h4{margin:0 0 5px}.carousel-caption p{margin-bottom:0}.hero-unit{padding:60px;margin-bottom:30px;font-size:18px;font-weight:200;line-height:30px;color:inherit;background-color:#eee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;color:inherit}.hero-unit li{line-height:30px}.pull-right{float:right}.pull-left{float:left}.hide{display:none}.show{display:block}.invisible{visibility:hidden}.affix{position:fixed} diff --git a/routes/make.js b/routes/make.js index d8b0f2b..d1a0082 100644 --- a/routes/make.js +++ b/routes/make.js @@ -10,7 +10,10 @@ module.exports = function( makeCtor, env ) { var Make = makeCtor, metrics = require( "../lib/metrics" )( env ), - querystring = require( "querystring" ); + maker = require( "../lib/maker" )(), + querystring = require( "querystring" ), + deferred = require( "deferred" ), + getUser = deferred.promisify( maker.getUser ); function handleError( res, err, code, type ){ metrics.increment( "make." + type + ".error" ); @@ -31,12 +34,52 @@ module.exports = function( makeCtor, env ) { make[ field ] = body[ field ] || null; }); + make.email = body.email; make.createdAt = Date.now(); make.save(function( err, make ){ return handleSave( res, err, make, type ); }); } + function getUserNames( res, results ) { + var searchHit; + + // Query for each Make's creator and attach their username to the Make + deferred.map( results.hits, function( make ) { + + // Query the Login API for User data using the email attached to the Make + return getUser( make.email ) + .then(function onSuccess( user ) { + // Create new object and copy the makes public fields to it + searchHit = {}; + Make.publicFields.forEach(function( val ) { + searchHit[ val ] = make[ val ]; + }); + // Attach the Maker's subdomain(username) and return the result + searchHit.username = user.subdomain; + return searchHit; + }, function onError( err ) { + handleError( res, err, 500, "search" ); + }); + }) + .then(function onSuccess( result ) { + metrics.increment( "make.search.success" ); + res.json( result ); + }, function onError( err ) { + handleError( res, err, 500, "search" ); + }); + } + + function doSearch( res, searchData ) { + Make.search( searchData, function( err, results ) { + if ( err ) { + return handleError( res, err, 500, "search" ); + } else { + getUserNames( res, results ); + } + }); + } + return { create: function( req, res ) { updateFields( res, new Make(), req.body.make, "create" ); @@ -83,6 +126,11 @@ module.exports = function( makeCtor, env ) { } catch ( err ) { return handleError ( res, "Unable to parse search data.", 400, "search" ); } + + if ( !searchData.query.filtered.filter.and ) { + searchData.query.filtered.filter.and = []; + } + filters = searchData.query.filtered.filter.and; // We have to unescape any URLs that were present in the data @@ -93,14 +141,23 @@ module.exports = function( makeCtor, env ) { } } - Make.search( searchData, function( err, results ) { - if ( err ) { - return handleError( res, err, 500, "search" ); - } else { - metrics.increment( "make.search.success" ); - res.json( results ); - } - }); + if ( searchData.makerID ) { + return maker.getUser( searchData.makerID, function( err, userData ) { + if ( err ) { + return handleError( res, "Specified user does not exist", 400, "search" ); + } + searchData.query.filtered.filter.and.push({ + term: { + email: userData.email + } + }); + delete searchData.makerID; + doSearch( res, searchData ); + }); + } + + doSearch( res, searchData ); + }, healthcheck: function( req, res ) { res.json({ http: "okay" }); From 6d1cdbdd8f7d80cac59f3a9660266c84973bca5f Mon Sep 17 00:00:00 2001 From: Christopher De Cairos Date: Tue, 28 May 2013 11:22:01 -0400 Subject: [PATCH 19/34] 0.1.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8e9b21b..1871948 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "makeapi", - "version": "0.1.8", + "version": "0.1.9", "description": "MakeAPI for Webmaker", "main": "./lib/api.js", "scripts": { From b1362b16b0816eeaa73d4a4dafc1bc41427a05b8 Mon Sep 17 00:00:00 2001 From: Christopher De Cairos Date: Tue, 28 May 2013 10:14:20 -0400 Subject: [PATCH 20/34] Fix Bug 876527 - Don't overwrite existing make fields on an update, concatenate tag arrays, and determine which date field to timestamp. --- routes/make.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/routes/make.js b/routes/make.js index d1a0082..c9c2a72 100644 --- a/routes/make.js +++ b/routes/make.js @@ -31,11 +31,26 @@ module.exports = function( makeCtor, env ) { function updateFields( res, make, body, type ) { Make.publicFields.forEach( function( field ) { - make[ field ] = body[ field ] || null; + // only update if the field exists on the body + if ( field in body ) { + // arrays need to be concatenated to avoid overwriting elements in the original. + if ( Array.isArray( make[ field ] ) ) { + make[ field ] = make[ field ].concat( body[ field ] ); + } else { + make[ field ] = body[ field ]; + } + } }); make.email = body.email; - make.createdAt = Date.now(); + + // If createdAt doesn't exist, we know this is a Create, otherwise stamp updatedAt + if ( !make.createdAt ) { + make.createdAt = Date.now(); + } else { + make.updatedAt = Date.now(); + } + make.save(function( err, make ){ return handleSave( res, err, make, type ); }); From c2d0d46c184f85a35b76870a956162b6c0ecc900 Mon Sep 17 00:00:00 2001 From: Matthew Schranz Date: Wed, 29 May 2013 13:01:33 -0400 Subject: [PATCH 21/34] Fix Bug 877213 - Send emailHash for user back with searches --- public/js/make-api.js | 2 +- routes/make.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/public/js/make-api.js b/public/js/make-api.js index 6359a1f..da7344e 100644 --- a/public/js/make-api.js +++ b/public/js/make-api.js @@ -202,7 +202,7 @@ var module = module || undefined; // Extend wrapped with contents of make [ "url", "contentType", "locale", "title", "description", "author", "published", "tags", "thumbnail", - "username", "remixedFrom", "_id" ].forEach( function( prop ) { + "username", "remixedFrom", "_id", "emailHash" ].forEach( function( prop ) { wrapped[ prop ] = make[ prop ]; }); diff --git a/routes/make.js b/routes/make.js index c9c2a72..fd49a5a 100644 --- a/routes/make.js +++ b/routes/make.js @@ -72,6 +72,7 @@ module.exports = function( makeCtor, env ) { }); // Attach the Maker's subdomain(username) and return the result searchHit.username = user.subdomain; + searchHit.emailHash = user.emailHash; return searchHit; }, function onError( err ) { handleError( res, err, 500, "search" ); From 656d443d94dfafe1c19fe861b1e8dc7209bf770a Mon Sep 17 00:00:00 2001 From: Matthew Schranz Date: Wed, 29 May 2013 14:14:03 -0400 Subject: [PATCH 22/34] Fix Bug 877270 - _id was being removed from make objects --- routes/make.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/routes/make.js b/routes/make.js index fd49a5a..16258a4 100644 --- a/routes/make.js +++ b/routes/make.js @@ -70,6 +70,10 @@ module.exports = function( makeCtor, env ) { Make.publicFields.forEach(function( val ) { searchHit[ val ] = make[ val ]; }); + // _id is not a part of our public fields. We need to manually assign + // it to the object we are returning + searchHit._id = make._id; + // Attach the Maker's subdomain(username) and return the result searchHit.username = user.subdomain; searchHit.emailHash = user.emailHash; From 0f763a0ee02f1a56b38aaf97ce6a2276cb3818b6 Mon Sep 17 00:00:00 2001 From: Matthew Schranz Date: Tue, 28 May 2013 10:40:18 -0400 Subject: [PATCH 23/34] Fix Bug 876452 - Consumers of the API should be able to access the property _id by looking at .id --- lib/models/make.js | 6 ++++++ public/js/make-api.js | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/lib/models/make.js b/lib/models/make.js index e8475bb..0b3ba53 100644 --- a/lib/models/make.js +++ b/lib/models/make.js @@ -102,6 +102,12 @@ module.exports = function( environment, mongoInstance ) { } }); + schema.set( "toJSON", { virtuals: true } ); + + schema.virtual( "id" ).get(function() { + return this._id; + }); + // Hooks schema.pre( "save", function ( next ) { this.updatedAt = Date.now(); diff --git a/public/js/make-api.js b/public/js/make-api.js index da7344e..e311b91 100644 --- a/public/js/make-api.js +++ b/public/js/make-api.js @@ -206,6 +206,10 @@ var module = module || undefined; wrapped[ prop ] = make[ prop ]; }); + // Virtuals will only be exposed while still on the server end + // forcing us to still manually expose it for client side users. + wrapped.id = wrapped._id; + return wrapped; } From b56801d27f75534a49bb2661b89193f7228d8677 Mon Sep 17 00:00:00 2001 From: Christopher De Cairos Date: Wed, 29 May 2013 15:02:00 -0400 Subject: [PATCH 24/34] Bug 871700 - filter out duplicate tags --- routes/make.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/routes/make.js b/routes/make.js index 16258a4..91d1a83 100644 --- a/routes/make.js +++ b/routes/make.js @@ -35,7 +35,9 @@ module.exports = function( makeCtor, env ) { if ( field in body ) { // arrays need to be concatenated to avoid overwriting elements in the original. if ( Array.isArray( make[ field ] ) ) { - make[ field ] = make[ field ].concat( body[ field ] ); + make[ field ] = make[ field ].concat( body[ field ] ).filter(function( tag, pos, arr ) { + return arr.indexOf( tag ) === pos; + }); } else { make[ field ] = body[ field ]; } From 9b843b2421a276fab161409698f77eacca449909 Mon Sep 17 00:00:00 2001 From: Christopher De Cairos Date: Wed, 29 May 2013 15:12:04 -0400 Subject: [PATCH 25/34] 0.1.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1871948..746348d 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "makeapi", - "version": "0.1.9", + "version": "0.1.10", "description": "MakeAPI for Webmaker", "main": "./lib/api.js", "scripts": { From 173bc060f8183cf145baad1a27d9a2320fe85d75 Mon Sep 17 00:00:00 2001 From: Matthew Schranz Date: Wed, 29 May 2013 16:23:22 -0400 Subject: [PATCH 26/34] Fix Bug 877325 - updatedAt and createdAt weren't being return with make objects --- public/js/make-api.js | 3 ++- routes/make.js | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/public/js/make-api.js b/public/js/make-api.js index e311b91..f0ef91f 100644 --- a/public/js/make-api.js +++ b/public/js/make-api.js @@ -202,7 +202,8 @@ var module = module || undefined; // Extend wrapped with contents of make [ "url", "contentType", "locale", "title", "description", "author", "published", "tags", "thumbnail", - "username", "remixedFrom", "_id", "emailHash" ].forEach( function( prop ) { + "username", "remixedFrom", "_id", "emailHash", "createdAt", + "updatedAt" ].forEach( function( prop ) { wrapped[ prop ] = make[ prop ]; }); diff --git a/routes/make.js b/routes/make.js index 91d1a83..00a8739 100644 --- a/routes/make.js +++ b/routes/make.js @@ -72,9 +72,11 @@ module.exports = function( makeCtor, env ) { Make.publicFields.forEach(function( val ) { searchHit[ val ] = make[ val ]; }); - // _id is not a part of our public fields. We need to manually assign - // it to the object we are returning + // _id, createdAt and updatedAt are not apart of our public fields. + // We need to manually assign it to the object we are returning searchHit._id = make._id; + searchHit.createdAt = make.createdAt; + searchHit.updatedAt = make.updatedAt; // Attach the Maker's subdomain(username) and return the result searchHit.username = user.subdomain; From 9ba769ade461afefcc4c44cce765abc348f6224f Mon Sep 17 00:00:00 2001 From: Christopher De Cairos Date: Thu, 30 May 2013 11:11:36 -0400 Subject: [PATCH 27/34] Fix Bug 877635 - update references to User.subdomain to User.username --- routes/make.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/make.js b/routes/make.js index 00a8739..1124119 100644 --- a/routes/make.js +++ b/routes/make.js @@ -78,8 +78,8 @@ module.exports = function( makeCtor, env ) { searchHit.createdAt = make.createdAt; searchHit.updatedAt = make.updatedAt; - // Attach the Maker's subdomain(username) and return the result - searchHit.username = user.subdomain; + // Attach the Maker's username and return the result + searchHit.username = user.username; searchHit.emailHash = user.emailHash; return searchHit; }, function onError( err ) { From e3e796bc8e5a26f825af947bce4b8b6f1cf90164 Mon Sep 17 00:00:00 2001 From: Matthew Schranz Date: Mon, 3 Jun 2013 09:31:55 -0400 Subject: [PATCH 28/34] Fix Bug 878779 - createdAt was defaulting to a date based on when server was last started --- lib/models/make.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/models/make.js b/lib/models/make.js index 0b3ba53..aadc31a 100644 --- a/lib/models/make.js +++ b/lib/models/make.js @@ -18,7 +18,6 @@ module.exports = function( environment, mongoInstance ) { var Timestamp = { type: Number, - "default": Date.now(), es_type: "long", es_indexed: true, es_index: "not_analyzed" From a47985390b1f9ac4f0f816de2d62f132125c5f0b Mon Sep 17 00:00:00 2001 From: Matthew Schranz Date: Thu, 30 May 2013 12:09:23 -0400 Subject: [PATCH 29/34] Fix Bug 877372 - Add Title and Description as full text searches --- lib/models/make.js | 6 ++---- public/js/make-api.js | 26 ++++++++++++++++++++++++++ public/js/search.js | 18 ++++++++++++++++++ public/search.html | 26 ++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/lib/models/make.js b/lib/models/make.js index aadc31a..50b65a5 100644 --- a/lib/models/make.js +++ b/lib/models/make.js @@ -48,13 +48,11 @@ module.exports = function( environment, mongoInstance ) { title: { type: String, es_indexed: true, - required: true, - es_index: "not_analyzed" + required: true }, description: { type: String, - es_indexed: true, - es_index: "not_analyzed" + es_indexed: true }, thumbnail: { type: String, diff --git a/public/js/make-api.js b/public/js/make-api.js index f0ef91f..cb5d2d9 100644 --- a/public/js/make-api.js +++ b/public/js/make-api.js @@ -370,6 +370,32 @@ var module = module || undefined; return this; }, + title: function( title ) { + this.searchFilters.push({ + query: { + query_string: { + query: title, + fields: [ "title" ], + default_operator: "AND" + } + } + }); + return this; + }, + + description: function( description ) { + this.searchFilters.push({ + query: { + query_string: { + query: description, + fields: [ "description" ], + default_operator: "AND" + } + } + }); + return this; + }, + then: function( callback ) { var searchQuery = BASE_QUERY; diff --git a/public/js/search.js b/public/js/search.js index fee8dfd..2238832 100644 --- a/public/js/search.js +++ b/public/js/search.js @@ -30,6 +30,24 @@ } } + window.searchTitle = function() { + make() + .title( document.getElementById( "title" ).value ) + .sortByField( sortBy.value, document.querySelector( "input[name='direction']:checked" ).value ) + .limit( size.value ) + .page( page.value || 1 ) + .then( processResult ); + }; + + window.searchDescription = function() { + make() + .description( document.getElementById( "description" ).value ) + .sortByField( sortBy.value, document.querySelector( "input[name='direction']:checked" ).value ) + .limit( size.value ) + .page( page.value || 1 ) + .then( processResult ); + }; + window.grabTags = function() { make() .tags({ diff --git a/public/search.html b/public/search.html index 425402d..9e15442 100644 --- a/public/search.html +++ b/public/search.html @@ -34,6 +34,20 @@
+
+ +
+ +
+
+ +
+ +
+ +
+
+
@@ -104,6 +118,18 @@
+
+
+ +
+
+ +
+
+ +
+
+
From 73ebdf80abe5e23c78e0d0ecc6598cd56f473181 Mon Sep 17 00:00:00 2001 From: Matthew Schranz Date: Mon, 3 Jun 2013 16:53:08 -0400 Subject: [PATCH 30/34] 0.1.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 746348d..67f6279 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "makeapi", - "version": "0.1.10", + "version": "0.1.11", "description": "MakeAPI for Webmaker", "main": "./lib/api.js", "scripts": { From 704815a30a4887bac3355c9cca12c54f3c294d09 Mon Sep 17 00:00:00 2001 From: alicoding Date: Mon, 3 Jun 2013 20:54:24 -0400 Subject: [PATCH 31/34] Fix Bug 877693 - correct env.sample in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 20ec8f6..f77db0c 100755 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Execute `npm install` in the application directory: Copy and edit your .env file. -- This should never be committed to the repo. Ensure that you fill in the ALLOWED_USERS variable. ``` -cp .env.sample .env +cp env.sample .env ``` #### Running the Services From 038a1c1e7dd003fff9291e3b673184d54e359666 Mon Sep 17 00:00:00 2001 From: Christopher De Cairos Date: Wed, 22 May 2013 20:23:13 -0400 Subject: [PATCH 32/34] Fix Bug 875134 - Make Editor tool for Admin webmaker accounts --- .npmignore | 3 + Gruntfile.js | 2 +- env.sample | 3 + lib/maker.js | 11 -- lib/middleware.js | 36 ++++- package.json | 5 +- public/images/calendar.gif | Bin 0 -> 1035 bytes public/js/admin.js | 144 ++++++++++++++++++ public/js/external/jquery.event.drag-2.2.js | 10 ++ public/js/external/slick.core.js | 7 + public/js/external/slick.editors.js | 7 + public/js/external/slick.grid.js | 15 ++ public/js/login.js | 43 ++++++ public/stylesheets/bootstrap.custom.css | 36 +++++ public/stylesheets/slick.grid.css | 157 ++++++++++++++++++++ routes/admin.js | 25 ++++ routes/index.js | 10 +- routes/make.js | 7 +- server.js | 36 ++++- views/admin.html | 78 ++++++++++ views/login.html | 33 ++++ 21 files changed, 635 insertions(+), 33 deletions(-) delete mode 100644 lib/maker.js create mode 100644 public/images/calendar.gif create mode 100644 public/js/admin.js create mode 100644 public/js/external/jquery.event.drag-2.2.js create mode 100644 public/js/external/slick.core.js create mode 100644 public/js/external/slick.editors.js create mode 100644 public/js/external/slick.grid.js create mode 100644 public/js/login.js create mode 100644 public/stylesheets/bootstrap.custom.css create mode 100644 public/stylesheets/slick.grid.css create mode 100644 routes/admin.js create mode 100644 views/admin.html create mode 100644 views/login.html diff --git a/.npmignore b/.npmignore index c0d0ad9..a510091 100644 --- a/.npmignore +++ b/.npmignore @@ -5,5 +5,8 @@ Gruntfile.js server.js node_modules/* public/js/search.js +public/js/README.md +public/js/admin.js +public/js/external/* public/stylesheets/* views/* diff --git a/Gruntfile.js b/Gruntfile.js index 6f5648d..92eff2a 100755 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -17,7 +17,7 @@ module.exports = function( grunt ) { "Gruntfile.js", "server.js", "lib/**/*.js", - "public/**/*.js", + "public/js/*.js", "routes/**/*.js", "test/**/*.js" ] diff --git a/env.sample b/env.sample index 10f7c9f..0f191b5 100644 --- a/env.sample +++ b/env.sample @@ -21,6 +21,9 @@ export ELASTIC_SEARCH_URL='http://localhost:9200' # You shouldn't use this username/password combo yourself export ALLOWED_USERS='testuser:password' +# Persona +export AUDIENCE="http://webmaker.mofostaging.net" + # statsd metrics collection. If the following are left empty, no stats # will be collected or sent to a server. Only STATSD_HOST and STATSD_PORT # are required. STATSD_PREFIX is an optional prefix for all stats (defaults diff --git a/lib/maker.js b/lib/maker.js deleted file mode 100644 index b3ee7e1..0000000 --- a/lib/maker.js +++ /dev/null @@ -1,11 +0,0 @@ -var loginAPI = require( "webmaker-loginapi"); - -module.exports = function() { - var env = new require( "habitat" )(), - api = loginAPI( env.get( "LOGIN_SERVER_URL_WITH_AUTH" ) ); - - return { - isAdmin: api.isAdmin, - getUser: api.getUser - }; -}; diff --git a/lib/middleware.js b/lib/middleware.js index 6bbbf53..32592a4 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -2,12 +2,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -module.exports = function( env ) { - +module.exports = function( loginApi, env ) { var qs = require( "querystring" ), userList = qs.parse( env.get( "ALLOWED_USERS" ), ",", ":" ), - tags = require( "./tags" )(), - maker = require( "./maker" )(); + tags = require( "./tags" )(); return { // Use with express.basicAuth middleware @@ -25,7 +23,6 @@ module.exports = function( env ) { if ( typeof req.body.make === "string" ) { req.body.make = qs.parse( req.body.make ); } - var makerID = req.body.maker, makeTags = req.body.make.tags, appTags = req.body.make.appTags; @@ -33,7 +30,7 @@ module.exports = function( env ) { makeTags = typeof makeTags === "string" ? [makeTags] : makeTags; appTags = typeof appTags === "string" ? [appTags] : appTags; - maker.isAdmin( makerID, function( err, isAdmin ) { + loginApi.isAdmin( makerID, function( err, isAdmin ) { if ( err ) { return res.json( 500, { error: err } ); } @@ -56,6 +53,33 @@ module.exports = function( env ) { next(); }); + }, + adminAuth: function( req, res, next ) { + var email = req.session ? req.session.email : ""; + if ( email ) { + loginApi.isAdmin( email, function( err, isAdmin ) { + if ( err || !isAdmin ) { + return res.json( 403, { reason: "forbidden" } ); + } + next(); + }); + } else { + res.redirect( 302, "/login" ); + } + }, + verifyPersonaLogin: function( err, req, res, email ) { + if ( err ) { + return res.json({ status: "failure", reason: err }); + } + loginApi.isAdmin( email, function( error, isAdmin ) { + var out; + if ( error || !isAdmin ) { + out = { status: "failure", reason: error || "you are not authorised to view this page." }; + } else { + out = { status: "okay", email: email }; + } + res.json(out); + }); } }; }; diff --git a/package.json b/package.json index 67f6279..1da602e 100755 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "dependencies": { "deferred": "0.6.4", "express": "~3.0.6", + "express-persona": "0.0.8", "Faker": "0.5.8", "habitat": "0.4.1", "mongoose": "~3.5.2", @@ -36,10 +37,10 @@ "mongoosastic": "0.0.11", "newrelic": "0.9.20", "node-statsd": "0.0.7", - "nunjucks": "~0.1.8a", + "nunjucks": "0.1.8a", "request": "2.20.0", "querystring": "0.2.0", - "webmaker-loginapi": "0.1.1" + "webmaker-loginapi": "0.1.2" }, "devDependencies": { "grunt": "~0.4.1", diff --git a/public/images/calendar.gif b/public/images/calendar.gif new file mode 100644 index 0000000000000000000000000000000000000000..90fd2e17fe247d3252977a0fb5b9e15aa0a513fb GIT binary patch literal 1035 zcmZ?wbhEHb6krfw_|5)@S9f&o11MQ?xO!cw>Iaru@>)1r=M0E4P+ZZ7mII zIULeKgDU;7-PP>>t`%1~&E2Rss zmoK_ev+zRIlAE$=f(b?AOC-O$p6(*{}&|uUzqrRQPTg#DgT!MQR@E+qCNbq%HSb);wrm_poFAqn?eAdp14k-|}?A)@PHqKcBko;gs#qr|o(M^svF)$&Z_>+Z^f#E-c4#;{?o?zg3!SJ6`#$&^S1A0Mo zOd^|}cuZ7utZDF2x^QZKw}f%r8-))i=S!QW?1-3n$c0l#`Co>>jVJxRayDfzPBbdB z@d(x%aio0O$kr>VAGIN%@zD`^9;T^(W-@hfEzoLM$MA{iMIURU+pmy=lUPqr(`R8} zQ7}$#a!{(>!NA06dGg`n cVfH2m1!<$04Gs?(n;)OBm5A{(P++hI0EaJh%K!iX literal 0 HcmV?d00001 diff --git a/public/js/admin.js b/public/js/admin.js new file mode 100644 index 0000000..5010cb3 --- /dev/null +++ b/public/js/admin.js @@ -0,0 +1,144 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +$(function() { + + var Slick = window.Slick, + + formatters = { + date: function( row, cell, val ) { + var newDate; + + try { + newDate = val ? new Date( val ) : "N/A"; + } catch( e ) { + newDate = Date.now(); + // Alert User to Error? - need error infrastructure + } + return newDate; + }, + tags: function( row, cell, val ) { + return Array.isArray( val ) ? val.join( "," ) : val; + } + }; + + var COLUMNS = [ + { id: "url", name: "Url", field: "url", + editor: Slick.Editors.Text + }, + { id: "contentType", name: "Content Type", field: "contentType", + editor: Slick.Editors.Text + }, + { id: "locale", name: "Locale", field: "locale", + editor: Slick.Editors.Text + }, + { id: "title", name: "Title", field: "title", + editor: Slick.Editors.Text + }, + { id: "description", name: "Description", field: "description", + editor: Slick.Editors.Text + }, + { id: "thumbnail", name: "Thumbnail Url", field: "thumbnail", + editor: Slick.Editors.Text + }, + { id: "author", name: "Author", field: "author" }, + { id: "tags", name: "Tags", field: "tags", + formatter: formatters.tags, + editor: Slick.Editors.Text + }, + { id: "remixedFrom", name: "Remixed From", field: "remixedFrom" }, + { id: "createdAt", name: "Created At", field: "createdAt", + formatter: formatters.date, + editor: Slick.Editors.Date + }, + { id: "updatedAt", name: "Updated At", field: "updatedAt", + formatter: formatters.date, + editor: Slick.Editors.Date + }, + { id: "deletedAt", name: "Deleted At", field: "deletedAt", + formatter: formatters.date, + editor: Slick.Editors.Date + } + ]; + + var options = { + autoEdit: false, + editable: true, + autoHeight: true, + enableTextSelectionOnCells: true, + defaultColumnWidth: 150, + topPanelHeight: 200 + }, + make = window.Make({ + apiURL: "/admin" + }), + tagSearchInput = $( "#search-tag" ), + searchBtn = $( "#search" ), + gridArea = $( ".data-table-area" ), + identity = $( "#identity" ).text(), + grid, + data; + + function updateMake( e, data ) { + var make = data.item; + + make.tags = Array.isArray( make.tags )? make.tags : make.tags.split( "," ); + make.email = identity; + + make.update( identity, function( err, data ) { + if ( err ) { + console.log( err ); + return; + } + // Better Success/Failure notification + }); + } + + function createGrid( err, data ) { + if ( err ) { + console.log( err ); + // need better error handling + return; + } + grid = new Slick.Grid( gridArea, data, COLUMNS, options ); + grid.onCellChange.subscribe( updateMake ); + } + + function doSearch() { + make + .tags( tagSearchInput.val().split( "," ) ) + .then( createGrid ); + } + + searchBtn.click( doSearch ); + tagSearchInput.keypress(function( e ) { + if ( e.which === 13 ) { + e.preventDefault(); + e.stopPropagation(); + doSearch(); + tagSearchInput.blur(); + } + }); + + // SSO + + var logout = $( "#logout" ); + + logout.click(function(){ + navigator.idSSO.logout(); + }); + + navigator.idSSO.watch({ + onlogin: function() {}, + onlogout: function() { + var request = new XMLHttpRequest(); + + request.open( "POST", "/persona/logout", true ); + request.addEventListener( "loadend", function() { + window.location.replace( "./login" ); + }, false); + request.send(); + } + }); +}); diff --git a/public/js/external/jquery.event.drag-2.2.js b/public/js/external/jquery.event.drag-2.2.js new file mode 100644 index 0000000..0f5ce18 --- /dev/null +++ b/public/js/external/jquery.event.drag-2.2.js @@ -0,0 +1,10 @@ +/*! + * jquery.event.drag - v 2.2 + * Copyright (c) 2010 Three Dub Media - http://threedubmedia.com + * Open Source MIT License - http://threedubmedia.com/code/license + */ +// Created: 2008-06-04 +// Updated: 2012-05-21 +// REQUIRES: jquery 1.7.x + +(function(a){a.fn.drag=function(b,c,d){var e="string"==typeof b?b:"",f=a.isFunction(b)?b:a.isFunction(c)?c:null;return 0!==e.indexOf("drag")&&(e="drag"+e),d=(b==f?c:d)||{},f?this.bind(e,d,f):this.trigger(e)};var b=a.event,c=b.special,d=c.drag={defaults:{which:1,distance:0,not:":input",handle:null,relative:!1,drop:!0,click:!1},datakey:"dragdata",noBubble:!0,add:function(b){var c=a.data(this,d.datakey),e=b.data||{};c.related+=1,a.each(d.defaults,function(a){void 0!==e[a]&&(c[a]=e[a])})},remove:function(){a.data(this,d.datakey).related-=1},setup:function(){if(!a.data(this,d.datakey)){var c=a.extend({related:0},d.defaults);a.data(this,d.datakey,c),b.add(this,"touchstart mousedown",d.init,c),this.attachEvent&&this.attachEvent("ondragstart",d.dontstart)}},teardown:function(){var c=a.data(this,d.datakey)||{};c.related||(a.removeData(this,d.datakey),b.remove(this,"touchstart mousedown",d.init),d.textselect(!0),this.detachEvent&&this.detachEvent("ondragstart",d.dontstart))},init:function(e){if(!d.touched){var g,f=e.data;if(!(0!=e.which&&f.which>0&&e.which!=f.which||a(e.target).is(f.not)||f.handle&&!a(e.target).closest(f.handle,e.currentTarget).length||(d.touched="touchstart"==e.type?this:null,f.propagates=1,f.mousedown=this,f.interactions=[d.interaction(this,f)],f.target=e.target,f.pageX=e.pageX,f.pageY=e.pageY,f.dragging=null,g=d.hijack(e,"draginit",f),!f.propagates)))return g=d.flatten(g),g&&g.length&&(f.interactions=[],a.each(g,function(){f.interactions.push(d.interaction(this,f))})),f.propagates=f.interactions.length,f.drop!==!1&&c.drop&&c.drop.handler(e,f),d.textselect(!1),d.touched?b.add(d.touched,"touchmove touchend",d.handler,f):b.add(document,"mousemove mouseup",d.handler,f),!d.touched||f.live?!1:void 0}},interaction:function(b,c){var e=a(b)[c.relative?"position":"offset"]()||{top:0,left:0};return{drag:b,callback:new d.callback,droppable:[],offset:e}},handler:function(e){var f=e.data;switch(e.type){case!f.dragging&&"touchmove":e.preventDefault();case!f.dragging&&"mousemove":if(Math.pow(e.pageX-f.pageX,2)+Math.pow(e.pageY-f.pageY,2)++l);return c.type=i.type,c.originalEvent=i.event,d.flatten(f.results)}},properties:function(a,b,c){var e=c.callback;return e.drag=c.drag,e.proxy=c.proxy||c.drag,e.startX=b.pageX,e.startY=b.pageY,e.deltaX=a.pageX-b.pageX,e.deltaY=a.pageY-b.pageY,e.originalX=c.offset.left,e.originalY=c.offset.top,e.offsetX=e.originalX+e.deltaX,e.offsetY=e.originalY+e.deltaY,e.drop=d.flatten((c.drop||[]).slice()),e.available=d.flatten((c.droppable||[]).slice()),e},element:function(a){return a&&(a.jquery||1==a.nodeType)?a:void 0},flatten:function(b){return a.map(b,function(b){return b&&b.jquery?a.makeArray(b):b&&b.length?d.flatten(b):b})},textselect:function(b){a(document)[b?"unbind":"bind"]("selectstart",d.dontstart).css("MozUserSelect",b?"":"none"),document.unselectable=b?"off":"on"},dontstart:function(){return!1},callback:function(){}};d.callback.prototype={update:function(){c.drop&&this.available.length&&a.each(this.available,function(a){c.drop.locate(this,a)})}};var e=b.dispatch;b.dispatch=function(b){return a.data(this,"suppress."+b.type)-(new Date).getTime()>0?(a.removeData(this,"suppress."+b.type),void 0):e.apply(this,arguments)};var f=b.fixHooks.touchstart=b.fixHooks.touchmove=b.fixHooks.touchend=b.fixHooks.touchcancel={props:"clientX clientY pageX pageY screenX screenY".split(" "),filter:function(b,c){if(c){var d=c.touches&&c.touches[0]||c.changedTouches&&c.changedTouches[0]||null;d&&a.each(f.props,function(a,c){b[c]=d[c]})}return b}};c.draginit=c.dragstart=c.dragend=d})(jQuery); diff --git a/public/js/external/slick.core.js b/public/js/external/slick.core.js new file mode 100644 index 0000000..c517139 --- /dev/null +++ b/public/js/external/slick.core.js @@ -0,0 +1,7 @@ +/*** + * Contains core SlickGrid classes. + * @module Core + * @namespace Slick + */ + +(function(a){function b(){var a=!1,b=!1;this.stopPropagation=function(){a=!0},this.isPropagationStopped=function(){return a},this.stopImmediatePropagation=function(){b=!0},this.isImmediatePropagationStopped=function(){return b}}function c(){var a=[];this.subscribe=function(b){a.push(b)},this.unsubscribe=function(b){for(var c=a.length-1;c>=0;c--)a[c]===b&&a.splice(c,1)},this.notify=function(c,d,e){d=d||new b,e=e||this;for(var f,g=0;a.length>g&&!d.isPropagationStopped()&&!d.isImmediatePropagationStopped();g++)f=a[g].call(e,d,c);return f}}function d(){var a=[];this.subscribe=function(b,c){return a.push({event:b,handler:c}),b.subscribe(c),this},this.unsubscribe=function(b,c){for(var d=a.length;d--;)if(a[d].event===b&&a[d].handler===c)return a.splice(d,1),b.unsubscribe(c),void 0;return this},this.unsubscribeAll=function(){for(var b=a.length;b--;)a[b].event.unsubscribe(a[b].handler);return a=[],this}}function e(a,b,c,d){void 0===c&&void 0===d&&(c=a,d=b),this.fromRow=Math.min(a,c),this.fromCell=Math.min(b,d),this.toRow=Math.max(a,c),this.toCell=Math.max(b,d),this.isSingleRow=function(){return this.fromRow==this.toRow},this.isSingleCell=function(){return this.fromRow==this.toRow&&this.fromCell==this.toCell},this.contains=function(a,b){return a>=this.fromRow&&this.toRow>=a&&b>=this.fromCell&&this.toCell>=b},this.toString=function(){return this.isSingleCell()?"("+this.fromRow+":"+this.fromCell+")":"("+this.fromRow+":"+this.fromCell+" - "+this.toRow+":"+this.toCell+")"}}function f(){this.__nonDataRow=!0}function g(){this.__group=!0,this.level=0,this.count=0,this.value=null,this.title=null,this.collapsed=!1,this.totals=null,this.rows=[],this.groups=null,this.groupingKey=null}function h(){this.__groupTotals=!0,this.group=null}function i(){var a=null;this.isActive=function(b){return b?a===b:null!==a},this.activate=function(b){if(b!==a){if(null!==a)throw"SlickGrid.EditorLock.activate: an editController is still active, can't activate another editController";if(!b.commitCurrentEdit)throw"SlickGrid.EditorLock.activate: editController must implement .commitCurrentEdit()";if(!b.cancelCurrentEdit)throw"SlickGrid.EditorLock.activate: editController must implement .cancelCurrentEdit()";a=b}},this.deactivate=function(b){if(a!==b)throw"SlickGrid.EditorLock.deactivate: specified editController is not the currently active one";a=null},this.commitCurrentEdit=function(){return a?a.commitCurrentEdit():!0},this.cancelCurrentEdit=function(){return a?a.cancelCurrentEdit():!0}}a.extend(!0,window,{Slick:{Event:c,EventData:b,EventHandler:d,Range:e,NonDataRow:f,Group:g,GroupTotals:h,EditorLock:i,GlobalEditorLock:new i}}),g.prototype=new f,g.prototype.equals=function(a){return this.value===a.value&&this.count===a.count&&this.collapsed===a.collapsed},h.prototype=new f})(jQuery); diff --git a/public/js/external/slick.editors.js b/public/js/external/slick.editors.js new file mode 100644 index 0000000..7822366 --- /dev/null +++ b/public/js/external/slick.editors.js @@ -0,0 +1,7 @@ +/*** + * Contains basic SlickGrid editors. + * @module Editors + * @namespace Slick + */ + +(function($){$.extend(true,window,{"Slick":{"Editors":{"Text":TextEditor,"Integer":IntegerEditor,"Date":DateEditor,"YesNoSelect":YesNoSelectEditor,"Checkbox":CheckboxEditor,"PercentComplete":PercentCompleteEditor,"LongText":LongTextEditor}}});function TextEditor(args){var $input;var defaultValue;var scope=this;this.init=function(){$input=$("").appendTo(args.container).bind("keydown.nav",function(e){if(e.keyCode===$.ui.keyCode.LEFT||e.keyCode===$.ui.keyCode.RIGHT){e.stopImmediatePropagation()}}).focus().select()};this.destroy=function(){$input.remove()};this.focus=function(){$input.focus()};this.getValue=function(){return $input.val()};this.setValue=function(val){$input.val(val)};this.loadValue=function(item){defaultValue=item[args.column.field]||"";$input.val(defaultValue);$input[0].defaultValue=defaultValue;$input.select()};this.serializeValue=function(){return $input.val()};this.applyValue=function(item,state){item[args.column.field]=state};this.isValueChanged=function(){return(!($input.val()==""&&defaultValue==null))&&($input.val()!=defaultValue)};this.validate=function(){if(args.column.validator){var validationResults=args.column.validator($input.val());if(!validationResults.valid){return validationResults}}return{valid:true,msg:null}};this.init()}function IntegerEditor(args){var $input;var defaultValue;var scope=this;this.init=function(){$input=$("");$input.bind("keydown.nav",function(e){if(e.keyCode===$.ui.keyCode.LEFT||e.keyCode===$.ui.keyCode.RIGHT){e.stopImmediatePropagation()}});$input.appendTo(args.container);$input.focus().select()};this.destroy=function(){$input.remove()};this.focus=function(){$input.focus()};this.loadValue=function(item){defaultValue=item[args.column.field];$input.val(defaultValue);$input[0].defaultValue=defaultValue;$input.select()};this.serializeValue=function(){return parseInt($input.val(),10)||0};this.applyValue=function(item,state){item[args.column.field]=state};this.isValueChanged=function(){return(!($input.val()==""&&defaultValue==null))&&($input.val()!=defaultValue)};this.validate=function(){if(isNaN($input.val())){return{valid:false,msg:"Please enter a valid integer"}}return{valid:true,msg:null}};this.init()}function DateEditor(args){var $input;var defaultValue;var scope=this;var calendarOpen=false;this.init=function(){$input=$("");$input.appendTo(args.container);$input.focus().select();$input.datepicker({showOn:"button",buttonImageOnly:true,buttonImage:"../images/calendar.gif",beforeShow:function(){calendarOpen=true},onClose:function(){calendarOpen=false}});$input.width($input.width()-18)};this.destroy=function(){$.datepicker.dpDiv.stop(true,true);$input.datepicker("hide");$input.datepicker("destroy");$input.remove()};this.show=function(){if(calendarOpen){$.datepicker.dpDiv.stop(true,true).show()}};this.hide=function(){if(calendarOpen){$.datepicker.dpDiv.stop(true,true).hide()}};this.position=function(position){if(!calendarOpen){return}$.datepicker.dpDiv.css("top",position.top+30).css("left",position.left)};this.focus=function(){$input.focus()};this.loadValue=function(item){defaultValue=item[args.column.field];$input.val(defaultValue);$input[0].defaultValue=defaultValue;$input.select()};this.serializeValue=function(){return $input.val()};this.applyValue=function(item,state){item[args.column.field]=state};this.isValueChanged=function(){return(!($input.val()==""&&defaultValue==null))&&($input.val()!=defaultValue)};this.validate=function(){return{valid:true,msg:null}};this.init()}function YesNoSelectEditor(args){var $select;var defaultValue;var scope=this;this.init=function(){$select=$("");$select.appendTo(args.container);$select.focus()};this.destroy=function(){$select.remove()};this.focus=function(){$select.focus()};this.loadValue=function(item){$select.val((defaultValue=item[args.column.field])?"yes":"no");$select.select()};this.serializeValue=function(){return($select.val()=="yes")};this.applyValue=function(item,state){item[args.column.field]=state};this.isValueChanged=function(){return($select.val()!=defaultValue)};this.validate=function(){return{valid:true,msg:null}};this.init()}function CheckboxEditor(args){var $select;var defaultValue;var scope=this;this.init=function(){$select=$("");$select.appendTo(args.container);$select.focus()};this.destroy=function(){$select.remove()};this.focus=function(){$select.focus()};this.loadValue=function(item){defaultValue=!!item[args.column.field];if(defaultValue){$select.attr("checked","checked")}else{$select.removeAttr("checked")}};this.serializeValue=function(){return!!$select.attr("checked")};this.applyValue=function(item,state){item[args.column.field]=state};this.isValueChanged=function(){return(this.serializeValue()!==defaultValue)};this.validate=function(){return{valid:true,msg:null}};this.init()}function PercentCompleteEditor(args){var $input,$picker;var defaultValue;var scope=this;this.init=function(){$input=$("");$input.width($(args.container).innerWidth()-25);$input.appendTo(args.container);$picker=$("
").appendTo(args.container);$picker.append("
");$picker.find(".editor-percentcomplete-buttons").append("

");$input.focus().select();$picker.find(".editor-percentcomplete-slider").slider({orientation:"vertical",range:"min",value:defaultValue,slide:function(event,ui){$input.val(ui.value)}});$picker.find(".editor-percentcomplete-buttons button").bind("click",function(e){$input.val($(this).attr("val"));$picker.find(".editor-percentcomplete-slider").slider("value",$(this).attr("val"))})};this.destroy=function(){$input.remove();$picker.remove()};this.focus=function(){$input.focus()};this.loadValue=function(item){$input.val(defaultValue=item[args.column.field]);$input.select()};this.serializeValue=function(){return parseInt($input.val(),10)||0};this.applyValue=function(item,state){item[args.column.field]=state};this.isValueChanged=function(){return(!($input.val()==""&&defaultValue==null))&&((parseInt($input.val(),10)||0)!=defaultValue)};this.validate=function(){if(isNaN(parseInt($input.val(),10))){return{valid:false,msg:"Please enter a valid positive number"}}return{valid:true,msg:null}};this.init()}function LongTextEditor(args){var $input,$wrapper;var defaultValue;var scope=this;this.init=function(){var $container=$("body");$wrapper=$("
").appendTo($container);$input=$("