feat: add notion of _me_ as a contact (#2899)

This commit is contained in:
Alexis Saettler
2019-08-17 11:42:45 +02:00
committed by GitHub
parent d56f42f993
commit 697eb8f83b
26 changed files with 387 additions and 11 deletions
+2
View File
@@ -2,10 +2,12 @@
### New features:
* Add the ablity to set a 'me' contact (only API for now)
* Add stepparent/stepchild relationship
### Enhancements:
* Update to laravel cashier 10.0, and get ready with SCA/PSD2
* Add stripe webhook
* Depends on php7.3+
* Use pretty-radio and optimize vue.js components
@@ -7,6 +7,7 @@ use App\Helpers\SearchHelper;
use App\Models\Contact\Contact;
use Illuminate\Support\Collection;
use Illuminate\Database\QueryException;
use App\Services\Contact\Contact\SetMeContact;
use Illuminate\Validation\ValidationException;
use App\Services\Contact\Contact\CreateContact;
use App\Services\Contact\Contact\UpdateContact;
@@ -17,6 +18,17 @@ use App\Http\Resources\Contact\ContactWithContactFields as ContactWithContactFie
class ApiContactController extends ApiController
{
/**
* Instantiate a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('limitations')->only('setMe');
parent::__construct();
}
/**
* Get the list of the contacts.
* We will only retrieve the contacts that are "real", not the partials
@@ -175,4 +187,25 @@ class ApiContactController extends ApiController
return ContactResource::collection($contacts);
}
/**
* Set a contact as 'me'.
*
* @param Request $request
* @param int $contactId
*
* @return string
*/
public function setMe(Request $request, $contactId)
{
$data = [
'contact_id' => $contactId,
'account_id' => auth()->user()->account->id,
'user_id' => auth()->user()->id,
];
app(SetMeContact::class)->execute($data);
return $this->respond(['true']);
}
}
@@ -41,6 +41,11 @@ class AddressBook extends BaseAddressBook
'principal' => '{DAV:}owner',
'protected' => true,
],
[
'privilege' => '{DAV:}write-properties',
'principal' => '{DAV:}owner',
'protected' => true,
],
];
}
@@ -17,6 +17,7 @@ use Sabre\CalDAV\Plugin as CalDAVPlugin;
use Sabre\CardDAV\Backend\AbstractBackend;
use Sabre\CardDAV\Plugin as CardDAVPlugin;
use Sabre\DAV\Sync\Plugin as DAVSyncPlugin;
use App\Services\Contact\Contact\SetMeContact;
use App\Http\Controllers\DAV\Backend\IDAVBackend;
use App\Http\Controllers\DAV\Backend\SyncDAVBackend;
use App\Http\Controllers\DAV\DAVACL\PrincipalBackend;
@@ -71,6 +72,13 @@ class CardDAVBackend extends AbstractBackend implements SyncSupport, IDAVBackend
];
}
$me = auth()->user()->me;
if ($me) {
$des += [
'{'.CalDAVPlugin::NS_CALENDARSERVER.'}me-card' => '/'.config('laravelsabre.path').'/addressbooks/'.Auth::user()->email.'/contacts/'.$this->encodeUri($me),
];
}
return [
$des,
];
@@ -373,7 +381,19 @@ class CardDAVBackend extends AbstractBackend implements SyncSupport, IDAVBackend
*/
public function updateAddressBook($addressBookId, DAV\PropPatch $propPatch)
{
return false;
$propPatch->handle('{'.CalDAVPlugin::NS_CALENDARSERVER.'}me-card', function ($props) {
$contact = $this->getObject($props->getHref());
$data = [
'contact_id' => $contact->id,
'account_id' => auth()->user()->account->id,
'user_id' => auth()->user()->id,
];
app(SetMeContact::class)->execute($data);
return true;
});
}
/**
+2
View File
@@ -4,6 +4,7 @@ namespace App\Http\Resources\Account\User;
use App\Helpers\DateHelper;
use Illuminate\Http\Resources\Json\Resource;
use App\Http\Resources\Contact\ContactShort as ContactShortResource;
use App\Http\Resources\Settings\Currency\Currency as CurrencyResource;
class User extends Resource
@@ -22,6 +23,7 @@ class User extends Resource
'first_name' => $this->first_name,
'last_name' => $this->last_name,
'email' => $this->email,
'me_contact' => new ContactShortResource($this->me),
'timezone' => $this->timezone,
'currency' => new CurrencyResource($this->currency),
'locale' => $this->locale,
+1
View File
@@ -30,6 +30,7 @@ class Contact extends Resource
'is_partial' => (bool) $this->is_partial,
'is_active' => (bool) $this->is_active,
'is_dead' => (bool) $this->is_dead,
'is_me' => $this->isMe(),
'last_called' => $this->when(! $this->is_partial, $this->getLastCalled()),
'last_activity_together' => $this->when(! $this->is_partial, $this->getLastActivityDate()),
'stay_in_touch_frequency' => $this->when(! $this->is_partial, $this->stay_in_touch_frequency),
@@ -27,6 +27,7 @@ class ContactShort extends Contact
'gender_type' => is_null($this->gender) ? null : $this->gender->type,
'is_partial' => (bool) $this->is_partial,
'is_dead' => (bool) $this->is_dead,
'is_me' => $this->isMe(),
'information' => [
'birthdate' => [
'is_age_based' => (is_null($this->birthdate) ? null : (bool) $this->birthdate->is_age_based),
@@ -28,6 +28,7 @@ class ContactWithContactFields extends Contact
'is_partial' => (bool) $this->is_partial,
'is_active' => (bool) $this->is_active,
'is_dead' => (bool) $this->is_dead,
'is_me' => $this->isMe(),
'last_called' => $this->when(! $this->is_partial, $this->getLastCalled()),
'last_activity_together' => $this->when(! $this->is_partial, $this->getLastActivityDate()),
'stay_in_touch_frequency' => $this->when(! $this->is_partial, $this->stay_in_touch_frequency),
+10
View File
@@ -418,6 +418,16 @@ class Contact extends Model
return $this->hasMany(Occupation::class);
}
/**
* Test if this is the 'me' contact.
*
* @return bool
*/
public function isMe()
{
return $this->id == auth()->user()->me_contact_id;
}
/**
* Sort the contacts according a given criteria.
* @param Builder $builder
+12
View File
@@ -8,6 +8,7 @@ use App\Models\Journal\Day;
use App\Models\Settings\Term;
use App\Helpers\RequestHelper;
use App\Models\Account\Account;
use App\Models\Contact\Contact;
use App\Helpers\CountriesHelper;
use App\Models\Settings\Currency;
use Illuminate\Support\Facades\DB;
@@ -17,6 +18,7 @@ use Illuminate\Support\Facades\App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Validation\UnauthorizedException;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Foundation\Auth\User as Authenticatable;
@@ -177,6 +179,16 @@ class User extends Authenticatable implements MustVerifyEmail
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the 'me' contact.
*
* @return HasOne
*/
public function me()
{
return $this->hasOne(Contact::class, 'id', 'me_contact_id');
}
/**
* Get the term records associated with the user.
*
+1
View File
@@ -87,6 +87,7 @@ class AppServiceProvider extends ServiceProvider
\App\Services\Contact\Call\UpdateCall::class => \App\Services\Contact\Call\UpdateCall::class,
\App\Services\Contact\Contact\CreateContact::class => \App\Services\Contact\Contact\CreateContact::class,
\App\Services\Contact\Contact\DestroyContact::class => \App\Services\Contact\Contact\DestroyContact::class,
\App\Services\Contact\Contact\SetMeContact::class => \App\Services\Contact\Contact\SetMeContact::class,
\App\Services\Contact\Contact\UpdateBirthdayInformation::class => \App\Services\Contact\Contact\UpdateBirthdayInformation::class,
\App\Services\Contact\Contact\UpdateContact::class => \App\Services\Contact\Contact\UpdateContact::class,
\App\Services\Contact\Contact\UpdateDeceasedInformation::class => \App\Services\Contact\Contact\UpdateDeceasedInformation::class,
@@ -0,0 +1,50 @@
<?php
namespace App\Services\Contact\Contact;
use App\Models\User\User;
use App\Services\BaseService;
use App\Models\Contact\Contact;
class SetMeContact extends BaseService
{
/**
* Get the validation rules that apply to the service.
*
* @return array
*/
public function rules()
{
return [
'account_id' => 'required|integer|exists:accounts,id',
'user_id' => 'required|integer|exists:users,id',
'contact_id' => 'required|integer|exists:contacts,id',
];
}
/**
* Set a contact as 'me' contact.
*
* @param array $data
* @return User
*/
public function execute(array $data) : User
{
$this->validate($data);
$user = User::where('account_id', $data['account_id'])
->findOrFail($data['user_id']);
if ($user->account->hasLimitations()) {
abort(402);
}
$contact = Contact::where('account_id', $data['account_id'])
->findOrFail($data['contact_id']);
$user->me_contact_id = $contact->id;
$user->save();
return $user;
}
}
+1
View File
@@ -72,6 +72,7 @@ $db = [
'driver' => 'sqlite',
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => env('DB_PREFIX', ''),
'foreign_key_constraints' => true,
],
'mysql' => [
@@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddMeContactOnUser extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->unsignedInteger('me_contact_id')->after('email')->nullable();
$table->foreign('me_contact_id')->references('id')->on('contacts')->onDelete('set null');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropForeign(['me_contact_id']);
$table->dropColumn('me_contact_id');
});
}
}
+3
View File
@@ -11,8 +11,11 @@
# Redirect .well-known urls (https://en.wikipedia.org/wiki/List_of_/.well-known/_services_offered_by_webservers)
RewriteCond %{REQUEST_URI} .well-known/carddav
RewriteRule ^ /dav/ [L,R=301]
RewriteCond %{REQUEST_URI} .well-known/caldav
RewriteRule ^ /dav/ [L,R=301]
RewriteCond %{REQUEST_URI} .well-known/security.txt
RewriteRule ^ /security.txt [L,R=301]
# old carddav url
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{
"/js/manifest.js": "/js/manifest.js?id=01c8731923a46c30aaed",
"/js/vendor.js": "/js/vendor.js?id=46a1680b35e3530942a8",
"/js/vendor.js": "/js/vendor.js?id=4638be4cb4d81526ae20",
"/js/app.js": "/js/app.js?id=807ac6ae4faa6baf4790",
"/css/app-ltr.css": "/css/app-ltr.css?id=d1bc921f46ec60c3bbca",
"/css/app-rtl.css": "/css/app-rtl.css?id=72920cbd345b2fe95fd4",
+1
View File
@@ -64,6 +64,7 @@ return [
'list_link_to_archived_contacts' => 'List of archived contacts',
// Header
'me' => 'This is you',
'edit_contact_information' => 'Edit contact information',
'contact_archive' => 'Archive contact',
'contact_unarchive' => 'Unarchive contact',
+7
View File
@@ -1,4 +1,7 @@
<div class="ph3 ph5-ns pv2 cf w-100 mt4 mt0-ns">
<div class="alert alert-success tc">
{{ trans('people.me') }}
</div>
<div class="mw9 center dt w-100 box-shadow pa4 relative">
{{-- AVATAR --}}
@@ -33,6 +36,7 @@
</li>
{{-- LAST ACTIVITY --}}
@if (! $contact->isMe())
<li class="mb2 mb0-ns dn di-ns tc {{ htmldir() == 'ltr' ? 'mr3-ns' : 'ml3-ns' }}">
<span class="{{ htmldir() == 'ltr' ? 'mr1' : 'ml1' }}">@include('partials.icons.header_people')</span>
@if (is_null($contact->getLastActivityDate()))
@@ -41,8 +45,10 @@
{{ trans('people.last_activity_date', ['date' => \App\Helpers\DateHelper::getShortDate($contact->getLastActivityDate())]) }}
@endif
</li>
@endif
{{-- LAST CALLED --}}
@if (! $contact->isMe())
<li class="mb2 mb0-ns dn di-ns tc {{ htmldir() == 'ltr' ? 'mr3-ns' : 'ml3-ns' }}">
<span class="{{ htmldir() == 'ltr' ? 'mr1' : 'ml1' }}">@include('partials.icons.header_call')</span>
@if (is_null($contact->getLastCalled()))
@@ -51,6 +57,7 @@
{{ trans('people.last_called', ['date' => \App\Helpers\DateHelper::getShortDate($contact->getLastCalled())]) }}
@endif
</li>
@endif
{{-- DESCRIPTION --}}
@if ($contact->description)
+9 -5
View File
@@ -81,22 +81,26 @@
<div class="flex items-center justify-center flex-column">
<div class='cf dib'>
@if (! $contact->isMe())
<span @click="updateDefaultProfileView('life-events')" :class="[global_profile_default_view == 'life-events' ? 'f6 fl bb bt br bl ph3 pv2 dib b br2 br--left bl mb4 b--gray-monica' : 'f6 fl bb bt br ph3 pv2 dib bg-gray-monica br2 br--left bl pointer mb4 b--gray-monica']">
@if (auth()->user()->profile_new_life_event_badge_seen == false)
<span class="bg-light-green f7 mr2 ph2 pv1 br2">{{ trans('app.new') }}</span>
@endif
{{ trans('people.life_event_list_tab_life_events') }} ({{ $contact->lifeEvents()->count() }})
</span>
@endif
<span @click="updateDefaultProfileView('notes')" :class="[global_profile_default_view == 'notes' ? 'f6 fl bb bt ph3 pv2 dib b br--right br mb4 b--gray-monica' : 'f6 fl bb bt ph3 pv2 dib bg-gray-monica br--right br pointer mb4 b--gray-monica']">{{ trans('people.life_event_list_tab_other') }}</span>
<span @click="updateDefaultProfileView('photos')" :class="[global_profile_default_view == 'photos' ? 'f6 fl bb bt ph3 pv2 dib b br2 br--right br mb4 b--gray-monica' : 'f6 fl bb bt ph3 pv2 dib bg-gray-monica br2 br--right br pointer mb4 b--gray-monica']">Photos</span>
</div>
</div>
@if (! $contact->isMe())
<div v-if="global_profile_default_view == 'life-events'">
<div class="row section">
@include('people.life-events.index')
</div>
</div>
@endif
<div v-if="global_profile_default_view == 'notes'">
@if ($modules->contains('key', 'notes'))
@@ -107,19 +111,19 @@
</div>
@endif
@if ($modules->contains('key', 'conversations'))
@if ($modules->contains('key', 'conversations') && ! $contact->isMe())
<div class="row section">
@include('people.conversations.index')
</div>
@endif
@if ($modules->contains('key', 'phone_calls'))
@if ($modules->contains('key', 'phone_calls') && ! $contact->isMe())
<div class="row section calls">
@include('people.calls.index')
</div>
@endif
@if ($modules->contains('key', 'activities'))
@if ($modules->contains('key', 'activities') && ! $contact->isMe())
<div class="row section activities">
@include('activities.index')
</div>
@@ -137,13 +141,13 @@
</div>
@endif
@if ($modules->contains('key', 'gifts'))
@if ($modules->contains('key', 'gifts') && ! $contact->isMe())
<div class="row section">
@include('people.gifts.index')
</div>
@endif
@if ($modules->contains('key', 'debts'))
@if ($modules->contains('key', 'debts') && ! $contact->isMe())
<div class="row section debts">
@include('people.debt.index')
</div>
+2 -2
View File
@@ -14,7 +14,7 @@
@endif
{{-- Introductions --}}
@if ($modules->contains('key', 'how_you_met'))
@if ($modules->contains('key', 'how_you_met') && ! $contact->isMe())
@include('people.introductions.index')
@endif
@@ -24,6 +24,6 @@
@endif
{{-- Food preferences --}}
@if ($modules->contains('key', 'food_preferences'))
@if ($modules->contains('key', 'food_preferences') && ! $contact->isMe())
@include('people.food-preferences.index')
@endif
+1
View File
@@ -19,6 +19,7 @@ Route::group(['middleware' => ['auth:api']], function () {
// Contacts
Route::apiResource('contacts', 'ApiContactController');
Route::put('/contacts/{contact}/setMe', 'ApiContactController@setMe');
// Genders
Route::apiResource('genders', 'Account\\ApiGenderController');
@@ -27,6 +27,7 @@ class ApiContactControllerTest extends ApiTestCase
'is_starred',
'is_partial',
'is_dead',
'is_me',
'last_called',
'last_activity_together',
'stay_in_touch_frequency',
@@ -1352,4 +1353,44 @@ class ApiContactControllerTest extends ApiTestCase
'id' => $contact->id,
]);
}
public function test_it_sets_me_contact()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account_id,
]);
$response = $this->json('PUT', '/api/contacts/'.$contact->id.'/setMe');
$response->assertStatus(200);
$this->assertDatabaseHas('users', [
'account_id' => $user->account_id,
'me_contact_id' => $contact->id,
]);
}
public function test_it_gets_me_contact()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account_id,
]);
$user->me_contact_id = $contact->id;
$user->save();
$response = $this->json('GET', '/api/contacts/'.$contact->id);
$response->assertOk();
$response->assertJsonStructure([
'data' => $this->jsonStructureContactShort,
]);
$response->assertJsonFragment([
'id' => $contact->id,
'is_me' => true,
]);
}
}
+76
View File
@@ -177,6 +177,82 @@ class CardDAVTest extends ApiTestCase
);
}
public function test_carddav_get_me_card()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$user->me_contact_id = $contact->id;
$user->save();
$response = $this->call('PROPFIND', "/dav/addressbooks/{$user->email}", [], [], [],
[
'HTTP_DEPTH' => '1',
'content-type' => 'application/xml; charset=utf-8',
],
"<propfind xmlns='DAV:' xmlns:cs='http://calendarserver.org/ns/'>
<prop>
<cs:me-card />
</prop>
</propfind>"
);
$response->assertStatus(207);
$response->assertHeader('X-Sabre-Version');
$response->assertSee('<d:response>'.
"<d:href>/dav/addressbooks/{$user->email}/contacts/</d:href>".
'<d:propstat>'.
'<d:prop>'.
"<cs:me-card>/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf</cs:me-card>".
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'
);
}
public function test_carddav_set_me_card()
{
$user = $this->signin();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$response = $this->call('PROPPATCH', "/dav/addressbooks/{$user->email}/contacts", [], [], [],
[
'content-type' => 'application/xml; charset=utf-8',
],
"<propertyupdate xmlns='DAV:' xmlns:cs='http://calendarserver.org/ns/'>
<set>
<prop>
<cs:me-card>
<href>/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf</href>
</cs:me-card>
</prop>
</set>
</propertyupdate>"
);
$response->assertSee('<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/">');
$response->assertSee('<d:response>'.
"<d:href>/dav/addressbooks/{$user->email}/contacts</d:href>".
'<d:propstat>'.
'<d:prop>'.
'<cs:me-card/>'.
'</d:prop>'.
'<d:status>HTTP/1.1 200 OK</d:status>'.
'</d:propstat>'.
'</d:response>'
);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'me_contact_id' => $contact->id,
]);
}
public function test_carddav_getctag_contacts()
{
$user = $this->signin();
@@ -0,0 +1,70 @@
<?php
namespace Tests\Unit\Services\Contact\Contact;
use Tests\TestCase;
use App\Models\User\User;
use App\Models\Contact\Contact;
use App\Services\Contact\Contact\SetMeContact;
use Illuminate\Validation\ValidationException;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class SetMeContactTest extends TestCase
{
use DatabaseTransactions;
public function test_it_set_me_as_a_a_contact()
{
$user = factory(User::class)->create();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$request = [
'account_id' => $user->account->id,
'user_id' => $user->id,
'contact_id' => $contact->id,
];
$user = app(SetMeContact::class)->execute($request);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'account_id' => $user->account->id,
'me_contact_id' => $contact->id,
]);
}
public function test_it_fails_if_wrong_parameters_are_given()
{
$user = factory(User::class)->create();
$contact = factory(Contact::class)->create([
'account_id' => $user->account->id,
]);
$request = [
'account_id' => $user->account->id,
'user_id' => $user->id,
'contact_id' => 0,
];
$this->expectException(ValidationException::class);
app(SetMeContact::class)->execute($request);
}
public function test_it_throws_an_exception_if_contact_not_found()
{
$user = factory(User::class)->create();
$contact = factory(Contact::class)->create();
$request = [
'account_id' => $user->account->id,
'user_id' => $user->id,
'contact_id' => $contact->id,
];
$this->expectException(ModelNotFoundException::class);
app(SetMeContact::class)->execute($request);
}
}