Commit bcde193f authored by Korko's avatar Korko
Browse files

Add SMS possibility and tests

parent 2916c95a
......@@ -5,3 +5,4 @@ Homestead.json
.env
/app/Http/Controllers/RecoveryFormController.php
/resources/views/recoveryForm.blade.php
sftp-config.json
\ No newline at end of file
......@@ -5,6 +5,7 @@ namespace Korko\SecretSanta\Http\Controllers;
use Korko\SecretSanta\Http\Requests\RandomFormRequest;
use Korko\SecretSanta\Libs\Randomizer;
use Mail;
use SmsWave;
use Statsd;
class RandomFormController extends Controller
......@@ -15,37 +16,67 @@ class RandomFormController extends Controller
}
public function handle(RandomFormRequest $request)
{
$participants = $this->getParticipants($request);
$hat = Randomizer::randomize($participants);
$this->sendMessages($request, $participants, $hat);
$message = 'Envoyé avec succès !';
return $request->ajax() ? [$message] : redirect('/')->with('message', $message);
}
protected function getParticipants(Request $request)
{
$names = $request->input('name');
$emails = $request->input('email');
$phones = $request->input('phone');
$partners = $request->input('partner', []);
$participants = [];
for ($i = 0; $i < count($names); $i++) {
$participants[$i] = [
'name' => $names[$i],
'email' => $emails[$i],
'partner' => !empty($partners[$i]) ? $names[$partners[$i]] : null,
];
$participants[$i] = array(
'name' => $names[$i],
'email' => $emails[$i],
'phone' => $phones[$i],
'partner' => !empty($partners[$i]) ? $names[$partners[$i]] : null
);
}
Statsd::gauge('draws', '+1');
Statsd::gauge('participants', '+'.count($participants));
$hat = Randomizer::randomize($participants);
return $participants;
}
protected function sendMessages(Request $request, array $participants, array $hat)
{
foreach ($hat as $santaIdx => $targetName) {
$santa = $participants[$santaIdx];
$content = str_replace(['{SANTA}', '{TARGET}'], [$santa['name'], $targetName], $request->input('content'));
$this->sendMessage($request, $santa, $targetName);
}
}
Mail::raw($content, function ($m) use ($santa, $request) {
$m->to($santa['email'], $santa['name'])->subject($request->input('title'));
});
protected function sendMessages(Request $request, array $santa, $targetName)
{
if(!empty($santa['email'])) {
Statsd::gauge('email', '+1');
$this->sendMail($santa, $targetName, $request->input('title'), $request->input('contentMail'))
}
$message = 'Envoyé avec succès !';
if(!empty($santa['phone'])) {
Statsd::gauge('phone', '+1');
$contentSms = str_replace(['{SANTA}', '{TARGET}'], [$santa['name'], $targetName], $request->input('contentSMS'));
SmsWave::send($santa['phone'], $contentSms);
}
}
return $request->ajax() ? [$message] : redirect('/')->with('message', $message);
protected function sendMail($santa, $targetName, $title, $content) {
$contentMail = str_replace(['{SANTA}', '{TARGET}'], [$santa['name'], $targetName], $content);
Mail::raw($contentMail, function ($m) use ($santa, $title) {
$m->to($santa['email'], $santa['name'])->subject($title);
});
}
}
......@@ -23,20 +23,26 @@ class RandomFormRequest extends Request
{
$rules = [
'g-recaptcha-response' => 'required|recaptcha',
'name' => 'required|arrayunique',
'title' => 'required|string',
'content' => 'required|contains:{TARGET}',
'email' => 'array',
'number' => 'array',
'partner' => 'array',
'name' => 'required|array|min:2|arrayunique',
'email' => 'array',
'phone' => 'array',
'partner' => 'array'
];
foreach ($this->request->get('name') as $key => $name) {
if(!empty($this->request->get('name'))) {
$rules += [
'email.'.$key => 'required_if:number.'.$key.',null|email',
'number.'.$key => 'required_if:email.'.$key.',null|numeric|regex:#336\d{8}',
'partner.'.$key => 'sometimes|string|fieldin:name',
'title' => 'required_with:'.implode(',', array_map(function($key) { return 'email.'.$key; }, array_keys($this->request->get('name', [])))).'|string',
'contentMail' => 'required_with:'.implode(',', array_map(function($key) { return 'email.'.$key; }, array_keys($this->request->get('name', [])))).'|contains:{TARGET}',
'contentSMS' => 'required_with:'.implode(',', array_map(function($key) { return 'phone.'.$key; }, array_keys($this->request->get('name', [])))).'|contains:{TARGET}'
];
foreach ($this->request->get('name') as $key => $name) {
$rules += [
'email.'.$key => 'required_without:phone.'.$key.'|email',
'phone.'.$key => 'required_without:email.'.$key.'|numeric|regex:#0[67]\d{8}#',
'partner.'.$key => 'sometimes|numeric|fieldinkeys:name,'.$key
];
}
}
return $rules;
......
......@@ -3,7 +3,6 @@
namespace Korko\SecretSanta\Providers;
use Illuminate\Support\ServiceProvider;
use Validator;
class AppServiceProvider extends ServiceProvider
{
......@@ -14,28 +13,7 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
Validator::extend('arrayunique', function ($attribute, $value, $parameters, $validator) {
return is_array($value) && count($value) === count(array_unique($value));
});
Validator::extend('fieldin', function ($attribute, $value, $parameters, $validator) {
return count($parameters) === 1 &&
isset($validator->getData()[$parameters[0]]) &&
is_array($validator->getData()[$parameters[0]]) &&
array_key_exists($value, $validator->getData()[$parameters[0]]);
});
Validator::replacer('fieldin', function ($message, $attribute, $rule, $parameters) {
return str_replace(':other', $parameters[0], $message);
});
Validator::extend('contains', function ($attribute, $value, $parameters, $validator) {
return is_string($value) && array_reduce((array) $parameters, function ($return, $find) use ($value) {
return $return && strpos($value, $find) !== false;
}, true);
});
Validator::replacer('contains', function ($message, $attribute, $rule, $parameters) {
return str_replace(':values', implode(', ', $parameters), $message);
});
//
}
/**
......
......@@ -3,6 +3,8 @@ Theme Name: MeatKing
Version: 1.14.20
Author: ThemeWagon
Tiny bug when width <780px, there an horizontal scrollbar
TABLE OF CONTENTS
01 - General and Typography
02 - Header
......@@ -243,7 +245,9 @@ img {
}
.media-body p::first-line {
}
#how div.well {
font-style: italic;
}
/* ==========================================================================
06. Form
......@@ -267,6 +271,15 @@ img {
#form.success .success-wrapper {
display: block;
}
.col-xs-0 {
width: 1%;
}
.border-left {
border-left: 1px solid #ddd;
}
.border-right {
border-right: 1px solid #ddd;
}
/* ==========================================================================
......@@ -302,7 +315,7 @@ footer .inner {
font-size: 22px;
}
}
@media screen and (min-width: 768px) {
@media (min-width: 768px) {
.navbar-nav {
float: none !important;
width: 100%;
......@@ -317,6 +330,19 @@ footer .inner {
margin-right: 110px;
}
}
@media (min-width: 992px) {
#contact > fieldset:first-child {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
border-right: 2px solid #ccc;
}
}
@media (max-width: 1199px) {
.participant-remove span:nth-child(2) {
display: none;
}
}
/* ==========================================================================
......
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -60,6 +60,34 @@ $(document.body).on('blur', '.participant-name', function(event) {
});
$('#form-mail-group').prop('disabled', 'disabled');
$(document.body).on('blur', '.participant-email', function(event) {
$('#form-mail-group').prop('disabled', 'disabled');
$('.participant-email').each(function() {
if($(this).val()){
$('#form-mail-group').prop('disabled', false);
return false;
}
});
});
$('#form-sms-group').prop('disabled', 'disabled');
$(document.body).on('blur', '.participant-phone', function(event) {
$('#form-sms-group').prop('disabled', 'disabled');
$('.participant-phone').each(function() {
if($(this).val()){
$('#form-sms-group').prop('disabled', false);
return false;
}
});
});
// Validate a phone number field
var locked = false;
$("#form form").submit(function(e) {
......
......@@ -76,10 +76,6 @@ return [
'unique' => 'The :attribute has already been taken.',
'url' => 'The :attribute format is invalid.',
'arrayunique' => 'The :attribute array contains duplicate values.',
'contains' => 'The :attribute field must contains the following values: :values.',
'fieldin' => 'The :attribute contains values which does not match with the :other field.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
......
......@@ -77,9 +77,7 @@ return [
'unique' => 'La valeur du champ :attribute est déjà utilisée.',
'url' => "Le format de l'URL de :attribute n'est pas valide.",
'arrayunique' => 'Le champ :attribute contient des valeurs similaires.',
'contains' => 'Le champ :attribute doit contenir les valeurs suivantes : :values.',
'fieldin' => 'Le champ :attribute contient des valeurs qui ne correspondent pas aux valeurs du champ :other.',
'recaptcha' => 'Le ReCaptcha n\'a pas pu être validé.',
/*
|--------------------------------------------------------------------------
......
......@@ -88,7 +88,7 @@
Secret Santa est un moyen drôle et original de s'offrir anonymement des cadeaux entre amis, collègues...<br />
Le déroulement est simple : chaque participant reçoit, de façon aléatoire, le nom de la personne à qui il devra faire un cadeau.<br />
Le montant du cadeau est généralement fixé au préalable (2€, 5€, 10€...)<br />
Le but n'est pas forcément de faire un beau cadeau mais de faire original. Soyez créatifs !
Le but n'est pas forcément de faire un beau cadeau mais d'être créatif !
</p>
</div>
</li>
......@@ -119,27 +119,39 @@
<img class="media-object" src="media/img/user-icon.png" />
</div>
<div class="media-body">
<h4 class="media-heading">Première étape : remplir les informations des participants</h4>
<h4 class="media-heading">Première étape : spécifier le nombre et les noms des participants</h4>
<p>
Pour chaque personne, indiquez un nom/prénom ou un pseudonyme. Deux participants ne peuvent avoir le même nom, sinon il est impossible de les différencier.<br />
Grâce aux boutons "Ajouter un participant" et "Enlever un participant", il est possible d'ajuster le nombre de personnes.<br />
A noter que secretsanta.fr est conçu de façon à ce qu'une personne ne puisse pas se pioicher elle-même.<br />
(Optionel) Ajoutez des exclusions. Si vous ne voulez pas que les conjoints puissent se piocher l'un l'autre, remplissez le champ "Partenaire".
Pour chaque personne, indiquez un nom/prénom ou un pseudonyme. Deux participants ne peuvent avoir le même nom, sinon il est impossible de les différencier.<br />
A noter que secretsanta.fr est conçu de façon à ce qu'une personne ne puisse pas se piocher elle-même.
</p>
</div>
</li>
<li class="media">
<div class="media-body">
<h4 class="media-heading">Seconde étape : préparer le mail</h4>
<h4 class="media-heading">Deuxième étape : remplir les informations de contact et les exclusions</h4>
<p>
Spécifiez le titre ainsi que le corps du message que chaque participant recevra.<br />
Le mot clef "{TARGET}" est obligatoire dans le corps du mail afin de donner à chaque personne sa "cible".<br />
(Optionel) Vous pouvez aussi utiliser le mot clef "{SANTA}" qui sera remplacé par le nom du destinataire du mail.
Vous avez la possibilité de choisir si les participants recevront le nom de leur cible par e-mail, par sms, ou les deux.<br />
Pour cela, précisez pour chacun une adresse e-mail et/ou un numéro de téléphone portable.
(Optionel) Ajoutez des exclusions. Si vous ne voulez pas que les conjoints puissent se piocher l'un l'autre, remplissez le champ "Partenaire".<br />
</p>
</div>
<div class="media-right media-middle">
<img class="media-object" src="media/img/paper-icon.png" />
</div>
</li>
<li class="media">
<div class="media-left media-middle">
<img class="media-object" src="media/img/mail-icon.png" />
</div>
<div class="media-body">
<h4 class="media-heading">Troisième étape : préparer le mail ou le sms</h4>
<p>
Il ne vous reste plus qu'à remplir le titre et le corps du courriel ou du SMS que les participants recevront.<br />
Le mot clef "{TARGET}" est obligatoire dans le corps du message afin de donner à chaque personne sa "cible".<br />
(Optionel) Vous pouvez aussi utiliser le mot clef "{SANTA}" qui sera remplacé par le nom du destinataire du message.
</p>
</div>
</li>
</ul>
<div class="well well-lg">Pour votre information : secretsanta.fr ne sauvegarde aucune de vos données ni ne les utilise à d'autres fins. Le code source est disponible sur <a href="https://github.com/Korko/SecretSanta">GitHub</a></div>
......@@ -178,10 +190,12 @@
<table id="participants" class="table table-hover table-numbered">
<thead>
<tr>
<th class="col-xs-4">Nom ou pseudonyme</th>
<th class="col-xs-4">Adresse e-mail</th>
<th class="col-xs-2">Partenaire (ne pourra être sa cible)</th>
<th class="col-xs-2"></th>
<th class="col-xs-3">Nom ou pseudonyme</th>
<th class="col-xs-3">Adresse e-mail</th>
<th class="col-xs-0"></th>
<th class="col-xs-2 col-lg-1">Téléphone</th>
<th class="col-xs-2">Partenaire</th>
<th class="col-xs-1 col-lg-2"></th>
</tr>
</thead>
<tbody>
......@@ -196,8 +210,14 @@
</div>
</label>
</td>
<td class="row">
<input type="email" name="email[]" required="required" placeholder="exemple : michel@aol.com" value="{{ array_get(old('email', []), $idx) }}" class="form-control" />
<td class="row border-left">
<input type="email" name="email[]" placeholder="exemple : michel@aol.com" value="{{ array_get(old('email', []), $idx) }}" class="form-control participant-email" />
</td>
<td>
et/ou
</td>
<td class="row border-right">
<input type='tel' pattern='0[67]\d{8}' maxlength="10" name="phone[]" placeholder="ex : 0612345678" value="{{ array_get(old('phone', []), $idx) }}" class="form-control participant-phone" />
</td>
<td class="row">
<select name="partner[]" class="form-control participant-partner">
......@@ -209,7 +229,7 @@
</select>
</td>
<td class="row participant-remove-wrapper">
<button type="button" class="btn btn-danger participant-remove"><span class="glyphicon glyphicon-minus"></span> Enlever ce participant</button>
<button type="button" class="btn btn-danger participant-remove"><span class="glyphicon glyphicon-minus"></span><span> Enlever ce participant</span></button>
</td>
</tr>
@endforeach
......@@ -218,16 +238,30 @@
<button type="button" class="btn btn-success participant-add"><span class="glyphicon glyphicon-plus"></span> Ajouter un participant</button>
</div>
<div class="form-group">
<label for="mailTitle">Titre du mail</label>
<input id="mailTitle" type="text" name="title" required="required" placeholder="exemple : Soirée secretsanta du 23 décembre chez Martin" value="{{ old('title') }}" class="form-control" />
</div>
<div class="form-group">
<label for="mailContent">Contenu du mail</label>
<textarea id="mailContent" name="content" required="required" placeholder="exemple : Salut {SANTA}, pour la soirée secret santa, ta cible c'est {TARGET}. Pour rappel, le montant du cadeau est de 3€ !" class="form-control" rows="3">{{ old('content') }}</textarea>
<p class="help-block">Utilisez "{SANTA}" pour le nom de celui qui recevra le mail et "{TARGET}" pour le nom de sa cible.</p>
<p class="help-block">Conseil : Pensez à rappeler la date, le lieu ainsi que le montant du cadeau.</p>
<div class="row" id="contact">
<fieldset id="form-mail-group" class="col-md-6">
<div class="form-group">
<label for="mailTitle">Titre du mail</label>
<input id="mailTitle" type="text" name="title" required="required" placeholder="ex : Soirée secretsanta du 23 décembre chez Martin" value="{{ old('title') }}" class="form-control" />
</div>
<div class="form-group">
<label for="mailContent">Contenu du mail</label>
<textarea id="mailContent" name="contentMail" required="required" placeholder="ex : Salut {SANTA}, pour la soirée secret santa, ta cible c'est {TARGET}. Pour rappel, le montant du cadeau est de 3€ !" class="form-control" rows="3">{{ old('contentMail') }}</textarea>
<p class="help-block">Utilisez "{SANTA}" pour le nom de celui qui recevra le mail et "{TARGET}" pour le nom de sa cible.</p>
<p class="help-block">Conseil : Pensez à rappeler la date, le lieu ainsi que le montant du cadeau.</p>
</div>
</fieldset>
<fieldset id="form-sms-group" class="col-md-6">
<div class="form-group">
<label for="smsContent">Contenu du sms</label>
<textarea id="smsContent" name="contentSMS" required="required" placeholder="ex : Salut {SANTA}, pour la soirée secret santa du 23 décembre chez Martin, ta cible c'est {TARGET}. Pour rappel, le montant du cadeau est de 3€ !" class="form-control" rows="3">{{ old('contentSMS') }}</textarea>
<p class="help-block">Utilisez "{SANTA}" pour le nom de celui qui recevra le sms et "{TARGET}" pour le nom de sa cible.</p>
<p class="help-block">Conseil : Pensez à rappeler la date, le lieu ainsi que le montant du cadeau.</p>
</div>
</fieldset>
</div>
<div class="form-group btn">
{!! Recaptcha::render() !!}
</div>
......
......@@ -7,28 +7,28 @@ class RandomizerTest extends TestCase
public function testDuoRandomize()
{
$this->assertEquals([0 => 'B', 1 => 'A'], Randomizer::randomize([
0 => ['name' => 'A', 'partner' => null],
1 => ['name' => 'B', 'partner' => null],
]));
0 => ['name' => 'A', 'partner' => null],
1 => ['name' => 'B', 'partner' => null],
]));
$this->assertEquals([0 => 'A', 1 => 'B'], Randomizer::randomize([
0 => ['name' => 'B', 'partner' => null],
1 => ['name' => 'A', 'partner' => null],
]));
0 => ['name' => 'B', 'partner' => null],
1 => ['name' => 'A', 'partner' => null],
]));
}
public function testTrioRandomize()
{
$this->assertEquals([0 => 'B', 1 => 'C', 2 => 'A'], Randomizer::randomize([
0 => ['name' => 'A', 'partner' => null],
1 => ['name' => 'B', 'partner' => 'A'],
2 => ['name' => 'C', 'partner' => 'B'],
0 => ['name' => 'A', 'partner' => null],
1 => ['name' => 'B', 'partner' => 'A'],
2 => ['name' => 'C', 'partner' => 'B'],
]));
$this->assertEquals([0 => 'C', 1 => 'A', 2 => 'B'], Randomizer::randomize([
0 => ['name' => 'A', 'partner' => 'B'],
1 => ['name' => 'B', 'partner' => 'C'],
2 => ['name' => 'C', 'partner' => null],
0 => ['name' => 'A', 'partner' => 'B'],
1 => ['name' => 'B', 'partner' => 'C'],
2 => ['name' => 'C', 'partner' => null],
]));
}
......@@ -39,9 +39,9 @@ class RandomizerTest extends TestCase
public function testFailRandomize()
{
$this->assertEquals([0 => 'C', 1 => 'A', 2 => 'B'], Randomizer::randomize([
0 => ['name' => 'A', 'partner' => 'B'],
1 => ['name' => 'B', 'partner' => 'A'],
2 => ['name' => 'C', 'partner' => null],
0 => ['name' => 'A', 'partner' => 'B'],
1 => ['name' => 'B', 'partner' => 'A'],
2 => ['name' => 'C', 'partner' => null],
]));
}
}
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Symfony\Component\HttpFoundation\JsonResponse;
class RequestTest extends TestCase
{
// Ignore CSRF Validation
use WithoutMiddleware;
protected function ajaxPost($url, $postArgs, $code = 200)
{
$server = ['HTTP_X-Requested-With' => 'XMLHttpRequest'];
$response = $this->call('POST', $url, $postArgs, [], [], $server);
$this->assertEquals($code, $response->status());
return json_decode($response->content(), true);
}
protected function assertArrayKeysEquals(array $keys, array $array)
{
$arraykeys = array_keys($array);
foreach($keys as $key) {
$this->assertContains($key, $arraykeys);
}
// Use the whole array to keep the value in the error message
// Use json_encode so that the value is displayed (if not, you'll only have "Array ()" as value)
$this->assertEquals('[]', json_encode(array_diff_key($array, array_flip($keys))), 'Unexpected keys');
}
// Nothing sent, recaptcha needed, at least a name
public function testEmpty()
{
$content = $this->ajaxPost('/', ['a' => 'b'], 422);
$this->assertArrayKeysEquals(['g-recaptcha-response', 'name'], $content);
}
// Just names, need recaptcha and contact informations
public function testContactInfo()
{
// At least 2 names!
$content = $this->ajaxPost('/', ['name' => ['toto']], 422);
$this->assertArrayKeysEquals(['g-recaptcha-response', 'name', 'email.0', 'phone.0'], $content);
// Ok for names but duplicates this time but no contact infos
$content = $this->ajaxPost('/', ['name' => ['toto', 'toto']], 422);
$this->assertArrayKeysEquals(['g-recaptcha-response', 'name', 'email.0', 'phone.0', 'email.1', 'phone.1'], $content);
// Ok for names but no contact infos
$content = $this->ajaxPost('/', ['name' => ['toto', 'tata']], 422);
$this->assertArrayKeysEquals(['g-recaptcha-response', 'email.0', 'phone.0', 'email.1', 'phone.1'], $content);
}
// Names and contact infos but no mail body nor sms body
public function testContactBodiesMail()
{
$content = $this->ajaxPost('/', ['name' => ['toto', 'tata'], 'email' => ['invalid@invalidformat', 'test@test.com']], 422);
$this->assertArrayKeysEquals(['g-recaptcha-response', 'email.0', 'title', 'contentMail'], $content);
$content = $this->ajaxPost('/', ['name' => ['toto', 'tata'], 'email' => ['test@test.com', 'test@test.com']], 422);
$this->assertArrayKeysEquals(['g-recaptcha-response', 'title', 'contentMail'], $content);
}
// Names and contact infos but no mail body nor sms body
public function testContactBodiesSms()
{
$content = $this->ajaxPost('/', ['name' => ['toto', 'tata'], 'phone' => ['0612345678', '55213249jgh']], 422);
$this->assertArrayKeysEquals(['g-recaptcha-response', 'phone.1', 'contentSMS'], $content);
$content = $this->ajaxPost('/', ['name' => ['toto', 'tata'], 'phone' => ['0612345678', '0612345678']], 422);
$this->assertArrayKeysEquals(['g-recaptcha-response', 'contentSMS'], $content);
}
// Names and contact infos but no mail body nor sms body
public function testContactBodiesBoth()
{
$content = $this->ajaxPost('/', ['name' => ['toto', 'tata', 'tutu'], 'email' => ['', 'test@test.com', 'test@test.com'], 'phone' => ['0612345678', '0612345678', ''], 'partners' => ['', '', '']], 422);
$this->assertArrayKeysEquals(['g-recaptcha-response', 'title', 'contentMail', 'contentSMS'], $content);
}
// Names and partners
public function testPartners()
{
$content = $this->ajaxPost('/', ['name' => ['toto', 'tata'], 'partner' => [87]], 422);
$this->assertArrayKeysEquals(['g-recaptcha-response', 'partner.0', 'phone.0', 'phone.1', 'email.0', 'email.1'], $content);
$content = $this->ajaxPost('/', ['name' => ['toto', 'tata'], 'partner' => [0]], 422);
$this->assertArrayKeysEquals(['g-recaptcha-response', 'partner.0', 'phone.0', 'phone.1', 'email.0', 'email.1'], $content);
$content = $this->ajaxPost('/', ['name' => ['toto', 'tata'], 'partner' => [1]], 422);
$this->assertArrayKeysEquals(['g-recaptcha-response', 'phone.0', 'phone.1', 'email.0', 'email.1'], $content);
}
public function testOk()
{
Recaptcha::shouldReceive('verify')->once()->andReturn(true);
Mail::shouldReceive('raw')
->once()
->with('test mail toto => tata', Mockery::on(function($closure) {
$message = Mockery::mock('Illuminate\Mailer\Message');
$message->shouldReceive('to')
->with('test@test.com', 'toto')
->andReturn(Mockery::self());
$message->shouldReceive('subject')