diff --git a/factories/Common.php b/factories/Common.php index dfae934d0..e222266ea 100644 --- a/factories/Common.php +++ b/factories/Common.php @@ -12,19 +12,20 @@ */ $factory->define(\OpenCFP\Domain\Model\User::class, function (\Faker\Generator $faker) { return [ - 'email' => $faker->unique()->safeEmail, - 'password' => \password_hash('secret', PASSWORD_BCRYPT), - 'activated' => 1, - 'first_name' => $faker->firstName, - 'last_name' => $faker->lastName, - 'company' => $faker->company, - 'twitter' => '@' . $faker->userName, - 'activated_at' => $faker->dateTimeInInterval('-2 months', '-1 months'), - 'last_login' => $faker->dateTimeInInterval('-5 days', 'now'), - 'transportation' => $faker->randomElement([0, 1]), - 'hotel' => $faker->randomElement([0, 1]), - 'info' => $faker->realText(), - 'bio' => $faker->realText(), + 'email' => $faker->unique()->safeEmail, + 'password' => \password_hash('secret', PASSWORD_BCRYPT), + 'activated' => 1, + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'company' => $faker->company, + 'twitter' => '@' . $faker->userName, + 'joindin_username' => $faker->userName, + 'activated_at' => $faker->dateTimeInInterval('-2 months', '-1 months'), + 'last_login' => $faker->dateTimeInInterval('-5 days', 'now'), + 'transportation' => $faker->randomElement([0, 1]), + 'hotel' => $faker->randomElement([0, 1]), + 'info' => $faker->realText(), + 'bio' => $faker->realText(), ]; }); diff --git a/migrations/20181002234013_create_user_joind_in_username_column.php b/migrations/20181002234013_create_user_joind_in_username_column.php new file mode 100644 index 000000000..7b028d58e --- /dev/null +++ b/migrations/20181002234013_create_user_joind_in_username_column.php @@ -0,0 +1,73 @@ +getAdapter()->getAdapter(); + $options = $adapter->getOptions(); + $this->capsule = new Capsule(); + $this->capsule->addConnection([ + 'driver' => 'mysql', + 'database' => $options['name'], + ]); + $this->capsule->getConnection()->setPdo($adapter->getConnection()); + $this->capsule->bootEloquent(); + $this->capsule->setAsGlobal(); + } + + public function up(): void + { + // Create joindin_username + $this->table('users') + ->addColumn('joindin_username', 'string', ['null' => true]) + ->update(); + + // Go through each record in user, strip out (https://joind.in/user/) and copy to joindin_username + $joindInRegex = '/^https:\/\/joind\.in\/user\/(.{1,100})$/'; + + $users = EloquentUser::all(); + + foreach ($users as $user) { + if (\preg_match($joindInRegex, $user->url, $matches) === 1) { + $user->joindin_username = $matches[1]; + $user->url = null; + $user->save(); + } + } + } + + public function down(): void + { + // Go through each record in user, update `url` to move the joindin_username to there + $users = EloquentUser::all(); + + foreach ($users as $user) { + $user->url = $user->joindin_username + ? 'https://joind.in/user/' . $user->joindin_username + : null; + $user->joindin_username = null; + $user->save(); + } + // Drop the joindin_username column + $this->table('users') + ->removeColumn('joindin_username') + ->update(); + } +} diff --git a/resources/views/admin/speaker/view.twig b/resources/views/admin/speaker/view.twig index 70df39c88..2ccdbb62d 100644 --- a/resources/views/admin/speaker/view.twig +++ b/resources/views/admin/speaker/view.twig @@ -15,6 +15,15 @@
{% if speaker.company %} {{ speaker.company }}{% endif %} {% if speaker.twitter %} @{{ speaker.twitter }}{% endif %} + {% if speaker.joindInUsername %} + + + {{ speaker.joindInUsername }} + + + {% endif %} {% if speaker.email %} {{ speaker.email }}{% endif %}
diff --git a/resources/views/dashboard.twig b/resources/views/dashboard.twig index 4d36cfdff..546f80320 100644 --- a/resources/views/dashboard.twig +++ b/resources/views/dashboard.twig @@ -11,7 +11,7 @@ function deleteTalk(tid) { if (data.delete == 'ok') { $("#talk-"+tid).remove(); } else if (data.delete == 'no-user') { - alert("You must be logged in to delete talks") + alert("You must be logged in to delete talks"); } else { alert("Unable to delete talk"); } @@ -47,11 +47,22 @@ function deleteTalk(tid) { {% if profile.company %}

{{ profile.company }}

{% endif %} + {% if profile.twitter %} - @{{ profile.twitter }} + @{{ profile.twitter }} + {% endif %} + + {% if profile.joindInUsername %} +
+ {{ profile.joindInUsername }} + {% endif %} + {% if profile.url %} - | {{ profile.url }} +
+ {{ profile.url }} {% endif %} diff --git a/resources/views/forms/_user.twig b/resources/views/forms/_user.twig index ab24ee879..a7f035eb1 100644 --- a/resources/views/forms/_user.twig +++ b/resources/views/forms/_user.twig @@ -20,10 +20,13 @@ - - + + - {% if site.online_conference == false %} + + + + {% if site.online_conference == false %}

Travel Assistance

diff --git a/src/Domain/Speaker/SpeakerProfile.php b/src/Domain/Speaker/SpeakerProfile.php index 965dfda46..a5bc0b7d4 100755 --- a/src/Domain/Speaker/SpeakerProfile.php +++ b/src/Domain/Speaker/SpeakerProfile.php @@ -140,6 +140,36 @@ public function getTwitterUrl(): string return 'https:://twitter.com/' . $twitter; } + /** + * @throws NotAllowedException + * + * @return null|string + */ + public function getJoindInUsername() + { + $this->assertAllowedToSee('joindin_username'); + + return $this->speaker->joindin_username; + } + + /** + * @throws NotAllowedException + * + * @return null|string + */ + public function getJoindInUrl() + { + $this->assertAllowedToSee('joindin_username'); + + $username = $this->speaker->joindin_username; + + if ($username === null || \trim($username) === '') { + return ''; + } + + return 'https://joind.in/user/' . $username; + } + /** * @throws NotAllowedException * diff --git a/src/Http/Action/Profile/EditAction.php b/src/Http/Action/Profile/EditAction.php index 7642f830b..8e76ac46b 100755 --- a/src/Http/Action/Profile/EditAction.php +++ b/src/Http/Action/Profile/EditAction.php @@ -73,22 +73,23 @@ public function __invoke(HttpFoundation\Request $request): HttpFoundation\Respon $speakerData = Model\User::find($user->getId())->toArray(); $content = $this->twig->render('user/edit.twig', [ - 'email' => $user->getLogin(), - 'first_name' => $speakerData['first_name'], - 'last_name' => $speakerData['last_name'], - 'company' => $speakerData['company'], - 'twitter' => $speakerData['twitter'], - 'url' => $speakerData['url'], - 'speaker_info' => $speakerData['info'], - 'speaker_bio' => $speakerData['bio'], - 'speaker_photo' => $speakerData['photo_path'], - 'preview_photo' => $this->path->uploadPath() . $speakerData['photo_path'], - 'airport' => $speakerData['airport'], - 'transportation' => $speakerData['transportation'], - 'hotel' => $speakerData['hotel'], - 'id' => $user->getId(), - 'formAction' => $this->urlGenerator->generate('user_update'), - 'buttonInfo' => 'Update Profile', + 'email' => $user->getLogin(), + 'first_name' => $speakerData['first_name'], + 'last_name' => $speakerData['last_name'], + 'company' => $speakerData['company'], + 'twitter' => $speakerData['twitter'], + 'joindin_username' => $speakerData['joindin_username'], + 'url' => $speakerData['url'], + 'speaker_info' => $speakerData['info'], + 'speaker_bio' => $speakerData['bio'], + 'speaker_photo' => $speakerData['photo_path'], + 'preview_photo' => $this->path->uploadPath() . $speakerData['photo_path'], + 'airport' => $speakerData['airport'], + 'transportation' => $speakerData['transportation'], + 'hotel' => $speakerData['hotel'], + 'id' => $user->getId(), + 'formAction' => $this->urlGenerator->generate('user_update'), + 'buttonInfo' => 'Update Profile', ]); return new HttpFoundation\Response($content); diff --git a/src/Http/Action/Profile/ProcessAction.php b/src/Http/Action/Profile/ProcessAction.php index 9ea095922..748b72df4 100755 --- a/src/Http/Action/Profile/ProcessAction.php +++ b/src/Http/Action/Profile/ProcessAction.php @@ -126,18 +126,19 @@ public function __invoke(HttpFoundation\Request $request): HttpFoundation\Respon private function getFormData(HttpFoundation\Request $request): array { return [ - 'email' => $request->get('email'), - 'user_id' => $request->get('id'), - 'first_name' => $request->get('first_name'), - 'last_name' => $request->get('last_name'), - 'company' => $request->get('company'), - 'twitter' => $request->get('twitter'), - 'url' => $request->get('url'), - 'airport' => $request->get('airport'), - 'transportation' => (int) $request->get('transportation'), - 'hotel' => (int) $request->get('hotel'), - 'speaker_info' => $request->get('speaker_info') ?: null, - 'speaker_bio' => $request->get('speaker_bio') ?: null, + 'email' => $request->get('email'), + 'user_id' => $request->get('id'), + 'first_name' => $request->get('first_name'), + 'last_name' => $request->get('last_name'), + 'company' => $request->get('company'), + 'twitter' => $request->get('twitter'), + 'joindin_username' => $request->get('joindin_username'), + 'url' => $request->get('url'), + 'airport' => $request->get('airport'), + 'transportation' => (int) $request->get('transportation'), + 'hotel' => (int) $request->get('hotel'), + 'speaker_info' => $request->get('speaker_info') ?: null, + 'speaker_bio' => $request->get('speaker_bio') ?: null, ]; } diff --git a/src/Http/Form/SignupForm.php b/src/Http/Form/SignupForm.php index 44acecfcd..bb605fb8a 100755 --- a/src/Http/Form/SignupForm.php +++ b/src/Http/Form/SignupForm.php @@ -34,6 +34,7 @@ class SignupForm extends Form 'hotel', 'speaker_photo', 'agree_coc', + 'joindin_username', 'url', ]; @@ -61,7 +62,7 @@ public function validateAll(string $action = 'create'): bool $validSpeakerBio = $this->validateSpeakerBio(); } - return $this->validateEmail() && $validPasswords && $this->validateFirstName() && $this->validateLastName() && $this->validateUrl() && $validSpeakerInfo && $validSpeakerBio && $this->validateSpeakerPhoto() && $agreeCoc; + return $this->validateEmail() && $validPasswords && $this->validateFirstName() && $this->validateLastName() && $this->validateUrl() && $validSpeakerInfo && $validSpeakerBio && $this->validateSpeakerPhoto() && $agreeCoc && $this->validateJoindInUsername(); } public function validateSpeakerPhoto(): bool @@ -201,15 +202,29 @@ public function validateLastName(): bool return $validationResponse; } + public function validateJoindInUsername(): bool + { + if (!isset($this->cleanData['joindin_username']) + || $this->cleanData['joindin_username'] == '' + || \preg_match('/^[a-zA-Z0-9\-_\.]{1,100}$/', $this->cleanData['joindin_username']) + ) { + return true; + } + + $this->addErrorMessage('Please enter a valid joind.in username.'); + + return false; + } + public function validateUrl(): bool { - if (\preg_match('/^https:\/\/joind\.in\/user\/[a-zA-Z0-9\-_\.]{1,100}$/', $this->cleanData['url']) + if (\filter_var($this->cleanData['url'], FILTER_VALIDATE_URL) !== false || !isset($this->cleanData['url']) || $this->cleanData['url'] == '' ) { return true; } - $this->addErrorMessage('You did not enter a valid joind.in URL'); + $this->addErrorMessage('Please enter a valid URL.'); return false; } diff --git a/tests/Integration/Http/Action/Signup/ProcessActionTest.php b/tests/Integration/Http/Action/Signup/ProcessActionTest.php index 2681db310..165110c38 100755 --- a/tests/Integration/Http/Action/Signup/ProcessActionTest.php +++ b/tests/Integration/Http/Action/Signup/ProcessActionTest.php @@ -28,22 +28,23 @@ public function signUpWorksCorrectly() $password = $faker->password; $response = $this->post('/signup', [ - 'first_name' => $faker->firstName, - 'last_name' => $faker->lastName, - 'email' => $faker->email, - 'company' => null, - 'twitter' => null, - 'url' => 'https://joind.in/user/abc123', - 'password' => $password, - 'password2' => $password, - 'airport' => null, - 'speaker_info' => null, - 'speaker_bio' => null, - 'transportation' => null, - 'hotel' => null, - 'buttonInfo' => 'Create my speaker profile', - 'coc' => 1, - 'privacy' => 1, + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'email' => $faker->email, + 'company' => null, + 'twitter' => null, + 'url' => 'https://example.com', + 'joindin_username' => $faker->userName, + 'password' => $password, + 'password2' => $password, + 'airport' => null, + 'speaker_info' => null, + 'speaker_bio' => null, + 'transportation' => null, + 'hotel' => null, + 'buttonInfo' => 'Create my speaker profile', + 'coc' => 1, + 'privacy' => 1, ]); $this->assertResponseIsRedirect($response); @@ -61,22 +62,23 @@ public function signUpWithoutJoindInWorks() $password = $faker->password; $response = $this->post('/signup', [ - 'first_name' => $faker->firstName, - 'last_name' => $faker->lastName, - 'email' => $faker->email, - 'company' => null, - 'twitter' => null, - 'url' => null, - 'password' => $password, - 'password2' => $password, - 'airport' => null, - 'speaker_info' => null, - 'speaker_bio' => null, - 'transportation' => null, - 'hotel' => null, - 'buttonInfo' => 'Create my speaker profile', - 'coc' => 1, - 'privacy' => 1, + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'email' => $faker->email, + 'company' => null, + 'twitter' => null, + 'url' => null, + 'joindin_username' => null, + 'password' => $password, + 'password2' => $password, + 'airport' => null, + 'speaker_info' => null, + 'speaker_bio' => null, + 'transportation' => null, + 'hotel' => null, + 'buttonInfo' => 'Create my speaker profile', + 'coc' => 1, + 'privacy' => 1, ]); $this->assertSessionHasFlashMessage("You've successfully created your account!", $this->session()); diff --git a/tests/Unit/Domain/Speaker/SpeakerProfileTest.php b/tests/Unit/Domain/Speaker/SpeakerProfileTest.php index 8079624cd..4f675f16a 100644 --- a/tests/Unit/Domain/Speaker/SpeakerProfileTest.php +++ b/tests/Unit/Domain/Speaker/SpeakerProfileTest.php @@ -338,6 +338,96 @@ public function getTwitterUrlReturnsTwitterUrlWhenTwitterPropertyIsNeitherHidden $this->assertSame($expected, $profile->getTwitterUrl()); } + /** + * @test + */ + public function getJoindInUsernameThrowsNotAllowedExceptionIfPropertyIsHidden(): void + { + $hiddenProperties = [ + 'joindin_username', + ]; + + $speaker = $this->createUserMock(); + + $profile = new SpeakerProfile($speaker, $hiddenProperties); + + $this->expectException(NotAllowedException::class); + + $profile->getJoindInUsername(); + } + + /** + * @test + */ + public function getJoindInUsernameReturnsJoindInUsernameIfPropertyIsNotHidden(): void + { + $joindinUsername = $this->faker()->userName; + + $speaker = $this->createUserMock([ + 'joindin_username' => $joindinUsername, + ]); + + $profile = new SpeakerProfile($speaker); + + $this->assertSame($joindinUsername, $profile->getJoindInUsername()); + } + + /** + * @test + * @dataProvider providerEmptyValue + * + * @param null|string $value + */ + public function getJoindInUrlThrowsNotAllowedExceptionIfPropertyIsHidden(): void + { + $hiddenProperties = [ + 'joindin_username', + ]; + + $speaker = $this->createUserMock(); + + $profile = new SpeakerProfile($speaker, $hiddenProperties); + + $this->expectException(NotAllowedException::class); + + $profile->getJoindInUrl(); + } + + /** + * @test + * @dataProvider providerEmptyValue + * + * @param null|string $value + */ + public function getJoindInUrlReturnsEmptyStringWhenJoindInUsernameIsNotHiddenButEmpty($value): void + { + $speaker = $this->createUserMock([ + 'joindin_username' => $value, + ]); + + $profile = new SpeakerProfile($speaker); + + $this->assertSame('', $profile->getJoindInUrl()); + } + + /** + * @test + */ + public function getJoindInUrlReturnsJoindInUrlIfPropertyIsNotHidden(): void + { + $joindinUsername = $this->faker()->userName; + + $speaker = $this->createUserMock([ + 'joindin_username' => $joindinUsername, + ]); + + $expectedUrl = 'https://joind.in/user/' . $joindinUsername; + + $profile = new SpeakerProfile($speaker); + + $this->assertSame($expectedUrl, $profile->getJoindInUrl()); + } + /** * @test */ diff --git a/tests/Unit/Http/Form/SignupFormTest.php b/tests/Unit/Http/Form/SignupFormTest.php index bea1b47c3..c6ea72c60 100755 --- a/tests/Unit/Http/Form/SignupFormTest.php +++ b/tests/Unit/Http/Form/SignupFormTest.php @@ -286,8 +286,45 @@ public function lastNameProvider(): array } /** - * Test that the Joind.in URL is being validated correctly + * @test * + * @param string $joindin_username + * @param bool $expectedResponse + * @dataProvider joindInUsernameProvider + */ + public function joindInUsernameIsValidatedCorrectly($joindin_username, $expectedResponse) + { + $data['joindin_username'] = $joindin_username; + + $form = new \OpenCFP\Http\Form\SignupForm($data, $this->purifier); + $form->sanitize(); + + $this->assertSame( + $expectedResponse, + $form->validateJoindInUsername(), + 'Did not validate joind.in username as expected' + ); + } + + public function joindInUsernameProvider(): array + { + return [ + [null, true], + [false, true], + ['', true], + ['abc123', true], + ['do re mi', false], + ['_FirstLast', true], + ['first-last', true], + ['first@last', false], + ['first#last', false], + ['first.last', true], + [\str_repeat('X', 100), true], + [\str_repeat('X', 101), false], + ]; + } + + /** * @test * * @param string $url @@ -307,34 +344,19 @@ public function urlIsValidatedCorrectly($url, $expectedResponse) ); } - /** - * Data provider for urlIsValidatedCorrectly - * - * @return array - */ public function urlProvider(): array { - $validBaseUrl = 'https://joind.in/user/'; - $longUrl = ''; - - for ($x = 1; $x <= 256; ++$x) { - $longUrl .= 'X'; - } - return [ - [$validBaseUrl . 'abc123', true], - [$validBaseUrl, false], [null, true], [false, true], ['', true], - ['http://example.net', false], - ['http://joind.in/user/abc123', false], - [$validBaseUrl . 'do re mi', false], - [$validBaseUrl . '_FirstLast', true], - [$validBaseUrl . 'first-last', true], - [$validBaseUrl . 'firstname.last', true], - [$validBaseUrl . '._name_.', true], - [$validBaseUrl . $longUrl, false], + ['http://example.net', true], + ['example', false], + ['http://example.com/do re mi', false], + ['http://example.com/_FirstLast', true], + ['http://example.com/first-last', true], + ['https://', false], + ['$19.95 plus shipping and handling', false], ]; } diff --git a/web/assets/css/app.css b/web/assets/css/app.css index 3a0a93d96..a23690888 100644 --- a/web/assets/css/app.css +++ b/web/assets/css/app.css @@ -1,5 +1,6 @@ -@import url(//fonts.googleapis.com/css?family=Roboto|Zilla+Slab);/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}header,nav{display:block}h1{font-size:2em;margin:.67em 0}main{display:block}a{background-color:transparent;-webkit-text-decoration-skip:objects}b,strong{font-weight:inherit;font-weight:bolder}code{font-family:monospace,monospace;font-size:1em}img{border-style:none}svg:not(:root){overflow:hidden}button,input,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}textarea{overflow:auto}[type=checkbox]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}[hidden]{display:none}html{font-family:sans-serif}h1,h2,h3,h4,p{margin:0}button{background:transparent;padding:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}ul{margin:0}[tabindex="-1"]:focus{outline:none!important}*,:after,:before{border:0 solid #dae4e9}[type=button],[type=reset],[type=submit],button{border-radius:0}img{max-width:100%}button,input,select,textarea{font-family:inherit}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:inherit;opacity:.5}input:-ms-input-placeholder,input::-ms-input-placeholder,textarea:-ms-input-placeholder,textarea::-ms-input-placeholder{color:inherit;opacity:.5}input::placeholder,textarea::placeholder{color:inherit;opacity:.5}.btn{padding:.75rem 1.5rem}.btn{border-radius:.25rem}.btn-brand{color:#fff;border-width:2px}.btn-brand{background-color:#e04c3e;border-color:#e04c3e}.btn-white{background-color:#fff;color:#e04c3e}.btn-white,.btn-white-o{border-width:2px;border-color:#fff}.btn-white-o{color:#fff}.btn-white-o:hover{background-color:#fff;color:#e04c3e}.btn-brand-o{border-width:2px;border-color:#e04c3e;color:#e04c3e}.btn-brand-o:hover{background-color:#e04c3e;color:#fff}.container{width:100%}@media (min-width:576px){.container{max-width:576px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:992px){.container{max-width:992px}}@media (min-width:1200px){.container{max-width:1200px}}.list-reset{list-style:none;padding:0}.bg-grey-dark{background-color:#70818a}.bg-grey-lighter{background-color:#f3f7f9}.bg-red-lightest{background-color:#fcebea}.bg-orange-lightest{background-color:#fff5eb}.bg-green-lightest{background-color:#e3fcec}.bg-teal-lightest{background-color:#e8fffe}.bg-blue-lightest{background-color:#eff8ff}.bg-indigo-lightest{background-color:#e6e8ff}.bg-brand{background-color:#e04c3e}.bg-cover{background-size:cover}.border-grey{border-color:#9babb4}.border-grey-light{border-color:#dae4e9}.border-white{border-color:#fff}.border-orange{border-color:#f6993f}.border-green-dark{border-color:#1f9d55}.border-teal{border-color:#4dc0b5}.border-indigo{border-color:#6574cd}.border-brand{border-color:#e04c3e}.rounded{border-radius:.25rem}.rounded-sm{border-radius:.125rem}.rounded-full{border-radius:9999px}.border-4{border-width:4px}.border{border-width:1px}.border-t-4{border-top-width:4px}.border-b-4{border-bottom-width:4px}.border-l-4{border-left-width:4px}.border-t-8{border-top-width:8px}.border-r{border-right-width:1px}.border-b{border-bottom-width:1px}.cursor-pointer{cursor:pointer}.block{display:block}.inline-block{display:inline-block}.table{display:table}.hidden{display:none}.flex{display:-webkit-box;display:-ms-flexbox;display:flex}.items-center{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.justify-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.justify-between{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.flex-1{-webkit-box-flex:1;-ms-flex:1;flex:1}.font-serif{font-family:Zilla Slab,serif}.font-thin{font-weight:200}.font-normal{font-weight:400}.font-bold{font-weight:700}.h-10{height:2.5rem}.h-16{height:4rem}.h-full{height:100%}.leading-loose{line-height:2}.m-0{margin:0}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-4{margin-top:1rem;margin-bottom:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mr-1{margin-right:.25rem}.mt-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.mb-2{margin-bottom:.5rem}.ml-2{margin-left:.5rem}.mt-3{margin-top:.75rem}.mr-3{margin-right:.75rem}.mb-3{margin-bottom:.75rem}.ml-3{margin-left:.75rem}.mt-4{margin-top:1rem}.mr-4{margin-right:1rem}.mb-4{margin-bottom:1rem}.ml-4{margin-left:1rem}.mt-6{margin-top:1.5rem}.mr-6{margin-right:1.5rem}.mb-6{margin-bottom:1.5rem}.mt-8{margin-top:2rem}.mr-8{margin-right:2rem}.mb-8{margin-bottom:2rem}.ml-8{margin-left:2rem}.mr-16{margin-right:4rem}.-mt-2{margin-top:-.5rem}.-mt-4{margin-top:-1rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-8{padding:2rem}.p-24{padding:6rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-4{padding-left:1rem;padding-right:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.fixed{position:fixed}.absolute{position:absolute}.shadow{-webkit-box-shadow:0 2px 4px 0 rgba(0,0,0,.1);box-shadow:0 2px 4px 0 rgba(0,0,0,.1)}.text-center{text-align:center}.text-right{text-align:right}.text-soft{color:#9babb4}.text-dark{color:#364349}.text-brand{color:#e04c3e}.text-grey-dark{color:#70818a}.text-grey{color:#9babb4}.text-white{color:#fff}.text-red-dark{color:#cc1f1a}.text-orange-darkest{color:#542605}.text-orange-dark{color:#de751f}.text-green-dark{color:#1f9d55}.text-teal-darkest{color:#0d3331}.text-indigo-darkest{color:#191e38}.text-indigo-dark{color:#5661b3}.hover\:text-dark:hover{color:#364349}.hover\:text-brand:hover{color:#e04c3e}.text-xs{font-size:.75rem}.text-sm{font-size:.875rem}.text-base{font-size:1rem}.text-lg{font-size:1.125rem}.text-5xl{font-size:3rem}.uppercase{text-transform:uppercase}.underline{text-decoration:underline}.hover\:underline:hover{text-decoration:underline}.whitespace-no-wrap{white-space:nowrap}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.w-10{width:2.5rem}.w-16{width:4rem}.w-32{width:8rem}.w-1\/2{width:50%}.w-2\/3{width:66.66667%}.w-1\/4{width:25%}.w-2\/5{width:40%}.w-3\/5{width:60%}.w-1\/6{width:16.66667%}.w-5\/6{width:83.33333%}.w-full{width:100%} +@import url(//fonts.googleapis.com/css?family=Roboto|Zilla+Slab);/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}header,nav{display:block}h1{font-size:2em;margin:.67em 0}main{display:block}a{background-color:transparent;-webkit-text-decoration-skip:objects}b,strong{font-weight:inherit;font-weight:bolder}code{font-family:monospace,monospace;font-size:1em}img{border-style:none}svg:not(:root){overflow:hidden}button,input,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}textarea{overflow:auto}[type=checkbox]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}[hidden]{display:none}html{font-family:sans-serif}h1,h2,h3,h4,p{margin:0}button{background:transparent;padding:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}ul{margin:0}[tabindex="-1"]:focus{outline:none!important}*,:after,:before{border:0 solid #dae4e9}img{border-style:solid}[type=button],[type=reset],[type=submit],button{border-radius:0}img{max-width:100%}button,input,select,textarea{font-family:inherit}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:inherit;opacity:.5}input::-ms-input-placeholder,textarea::-ms-input-placeholder{color:inherit;opacity:.5}input::placeholder,textarea::placeholder{color:inherit;opacity:.5}.btn{padding:.75rem 1.5rem}.btn{border-radius:.25rem}.btn-brand{color:#fff;border-width:2px}.btn-brand{background-color:#e04c3e;border-color:#e04c3e}.btn-white{background-color:#fff;color:#e04c3e}.btn-white,.btn-white-o{border-width:2px;border-color:#fff}.btn-white-o{color:#fff}.btn-white-o:hover{background-color:#fff;color:#e04c3e}.btn-brand-o{border-width:2px;border-color:#e04c3e;color:#e04c3e}.btn-brand-o:hover{background-color:#e04c3e;color:#fff}.container{width:100%}@media (min-width:576px){.container{max-width:576px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:992px){.container{max-width:992px}}@media (min-width:1200px){.container{max-width:1200px}}.list-reset{list-style:none;padding:0}.bg-grey-dark{background-color:#70818a}.bg-grey-lighter{background-color:#f3f7f9}.bg-red-lightest{background-color:#fcebea}.bg-orange-lightest{background-color:#fff5eb}.bg-green-lightest{background-color:#e3fcec}.bg-teal-lightest{background-color:#e8fffe}.bg-blue-lightest{background-color:#eff8ff}.bg-indigo-lightest{background-color:#e6e8ff}.bg-brand{background-color:#e04c3e}.bg-cover{background-size:cover}.border-grey{border-color:#9babb4}.border-grey-light{border-color:#dae4e9}.border-white{border-color:#fff}.border-orange{border-color:#f6993f}.border-green-dark{border-color:#1f9d55}.border-teal{border-color:#4dc0b5}.border-indigo{border-color:#6574cd}.border-brand{border-color:#e04c3e}.rounded{border-radius:.25rem}.rounded-sm{border-radius:.125rem}.rounded-full{border-radius:9999px}.border-4{border-width:4px}.border{border-width:1px}.border-t-4{border-top-width:4px}.border-b-4{border-bottom-width:4px}.border-l-4{border-left-width:4px}.border-t-8{border-top-width:8px}.border-r{border-right-width:1px}.border-b{border-bottom-width:1px}.cursor-pointer{cursor:pointer}.block{display:block}.inline-block{display:inline-block}.table{display:table}.hidden{display:none}.flex{display:-webkit-box;display:-ms-flexbox;display:flex}.items-center{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.justify-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.justify-between{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.flex-1{-webkit-box-flex:1;-ms-flex:1;flex:1}.font-serif{font-family:Zilla Slab,serif}.font-thin{font-weight:200}.font-normal{font-weight:400}.font-bold{font-weight:700}.h-10{height:2.5rem}.h-16{height:4rem}.h-full{height:100%}.leading-loose{line-height:2}.m-0{margin:0}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-4{margin-top:1rem;margin-bottom:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mr-1{margin-right:.25rem}.mt-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.mb-2{margin-bottom:.5rem}.ml-2{margin-left:.5rem}.mt-3{margin-top:.75rem}.mr-3{margin-right:.75rem}.mb-3{margin-bottom:.75rem}.ml-3{margin-left:.75rem}.mt-4{margin-top:1rem}.mr-4{margin-right:1rem}.mb-4{margin-bottom:1rem}.ml-4{margin-left:1rem}.mt-6{margin-top:1.5rem}.mr-6{margin-right:1.5rem}.mb-6{margin-bottom:1.5rem}.mt-8{margin-top:2rem}.mr-8{margin-right:2rem}.mb-8{margin-bottom:2rem}.ml-8{margin-left:2rem}.mr-16{margin-right:4rem}.-mt-2{margin-top:-.5rem}.-mt-4{margin-top:-1rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-8{padding:2rem}.p-24{padding:6rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-4{padding-left:1rem;padding-right:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.fixed{position:fixed}.absolute{position:absolute}.shadow{-webkit-box-shadow:0 2px 4px 0 rgba(0,0,0,.1);box-shadow:0 2px 4px 0 rgba(0,0,0,.1)}.text-center{text-align:center}.text-right{text-align:right}.text-soft{color:#9babb4}.text-dark{color:#364349}.text-brand{color:#e04c3e}.text-grey-dark{color:#70818a}.text-grey{color:#9babb4}.text-white{color:#fff}.text-red-dark{color:#cc1f1a}.text-orange-darkest{color:#542605}.text-orange-dark{color:#de751f}.text-green-dark{color:#1f9d55}.text-teal-darkest{color:#0d3331}.text-indigo-darkest{color:#191e38}.text-indigo-dark{color:#5661b3}.hover\:text-dark:hover{color:#364349}.hover\:text-brand:hover{color:#e04c3e}.text-xs{font-size:.75rem}.text-sm{font-size:.875rem}.text-base{font-size:1rem}.text-lg{font-size:1.125rem}.text-5xl{font-size:3rem}.uppercase{text-transform:uppercase}.underline{text-decoration:underline}.hover\:underline:hover{text-decoration:underline}.whitespace-no-wrap{white-space:nowrap}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.w-10{width:2.5rem}.w-16{width:4rem}.w-32{width:8rem}.w-1\/2{width:50%}.w-2\/3{width:66.66667%}.w-1\/4{width:25%}.w-2\/5{width:40%}.w-3\/5{width:60%}.w-1\/6{width:16.66667%}.w-5\/6{width:83.33333%}.w-full{width:100%} + /*! * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:FontAwesome;src:url("/assets/fonts/fontawesome-webfont.eot?v=4.7.0");src:url("/assets/fonts/fontawesome-webfont.eot?#iefix&v=4.7.0") format("embedded-opentype"),url("/assets/fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"),url("/assets/fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"),url("/assets/fonts/fontawesome-webfont.ttf?v=4.7.0") format("truetype"),url("/assets/fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular") format("svg");font-weight:400;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-5x{font-size:5em}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-star:before{content:"\F005"}.fa-clock-o:before{content:"\F017"}.fa-plane:before{content:"\F072"}.fa-comments:before{content:"\F086"}.fa-trophy:before{content:"\F091"}.fa-upload:before{content:"\F093"}.fa-twitter:before{content:"\F099"}.fa-credit-card:before{content:"\F09D"}.fa-bullhorn:before{content:"\F0A1"}.fa-envelope:before{content:"\F0E0"}.fa-file-text-o:before{content:"\F0F6"}.fa-check-square:before{content:"\F14A"}.fa-thumbs-up:before{content:"\F164"}.fa-thumbs-down:before{content:"\F165"}.fa-graduation-cap:before{content:"\F19D"}.fa-building:before{content:"\F1AD"}.fa-television:before{content:"\F26C"}.fa-commenting-o:before{content:"\F27B"}.fa-user-circle:before{content:"\F2BD"}html{overflow-y:scroll;-webkit-box-sizing:border-box;box-sizing:border-box}*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue;font-size:1rem;line-height:1.5;color:#364349;margin:0;padding-bottom:2rem}[role=button],button{cursor:pointer}img{vertical-align:middle}button,input{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue}a{color:#364349;text-decoration:none}h1,h2,h3,p{margin-bottom:.5rem}ul{list-style:disc;margin-top:.75rem;margin-bottom:.75rem;padding-left:2rem}ul ul{margin:0}ul>li{margin-bottom:.25rem}label{color:#70818a;font-size:.875rem}input[type=email],input[type=password],input[type=text]{padding:.5rem .75rem;width:100%}input[type=email],input[type=password],input[type=text],select{margin-bottom:.5rem;display:block;border-width:1px;border-color:#9babb4;border-radius:.125rem}select{padding:.5rem}textarea{padding:.75rem .5rem;width:100%;resize:vertical;border-radius:.125rem;border-color:#9babb4}div:empty{display:none}.pagerfanta a,.pagerfanta span{padding-left:.5rem;padding-right:.5rem}.opencfp-login,.opencfp-signup{color:#fff;width:16.66667%;min-width:20rem} \ No newline at end of file + */@font-face{font-family:FontAwesome;src:url("/assets/fonts/fontawesome-webfont.eot?v=4.7.0");src:url("/assets/fonts/fontawesome-webfont.eot?#iefix&v=4.7.0") format("embedded-opentype"),url("/assets/fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"),url("/assets/fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"),url("/assets/fonts/fontawesome-webfont.ttf?v=4.7.0") format("truetype"),url("/assets/fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular") format("svg");font-weight:400;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-star:before{content:"\F005"}.fa-clock-o:before{content:"\F017"}.fa-plane:before{content:"\F072"}.fa-comments:before{content:"\F086"}.fa-trophy:before{content:"\F091"}.fa-upload:before{content:"\F093"}.fa-twitter:before{content:"\F099"}.fa-credit-card:before{content:"\F09D"}.fa-bullhorn:before{content:"\F0A1"}.fa-envelope:before{content:"\F0E0"}.fa-file-text-o:before{content:"\F0F6"}.fa-desktop:before{content:"\F108"}.fa-check-square:before{content:"\F14A"}.fa-thumbs-up:before{content:"\F164"}.fa-thumbs-down:before{content:"\F165"}.fa-graduation-cap:before{content:"\F19D"}.fa-building:before{content:"\F1AD"}.fa-television:before{content:"\F26C"}.fa-commenting-o:before{content:"\F27B"}.fa-user-circle:before{content:"\F2BD"}html{overflow-y:scroll;-webkit-box-sizing:border-box;box-sizing:border-box}*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue;font-size:1rem;line-height:1.5;color:#364349;margin:0;padding-bottom:2rem}[role=button],button{cursor:pointer}img{vertical-align:middle}button,input{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue}a{color:#364349;text-decoration:none}h1,h2,h3,p{margin-bottom:.5rem}ul{list-style:disc;margin-top:.75rem;margin-bottom:.75rem;padding-left:2rem}ul ul{margin:0}ul>li{margin-bottom:.25rem}label{color:#70818a;font-size:.875rem}input[type=email],input[type=password],input[type=text]{padding:.5rem .75rem;width:100%}input[type=email],input[type=password],input[type=text],select{margin-bottom:.5rem;display:block;border-width:1px;border-color:#9babb4;border-radius:.125rem}select{padding:.5rem}textarea{padding:.75rem .5rem;width:100%;resize:vertical;border-radius:.125rem;border-color:#9babb4}div:empty{display:none}.pagerfanta a,.pagerfanta span{padding-left:.5rem;padding-right:.5rem}.opencfp-login,.opencfp-signup{color:#fff;width:16.66667%;min-width:20rem} \ No newline at end of file