Skip to content

Commit

Permalink
Merge pull request #696 from biigle/annotation-session-invitation
Browse files Browse the repository at this point in the history
Annotation session invitation
  • Loading branch information
mzur authored Nov 6, 2023
2 parents 5b0646e + 01d55d7 commit 619ac62
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 8 deletions.
16 changes: 13 additions & 3 deletions app/Http/Controllers/Api/ProjectInvitationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Biigle\Http\Controllers\Api;

use Biigle\AnnotationSession;
use Biigle\Http\Requests\JoinProjectInvitation;
use Biigle\Http\Requests\StoreProjectInvitation;
use Biigle\ProjectInvitation;
Expand All @@ -26,9 +27,10 @@ class ProjectInvitationController extends Controller
* @apiParam {Number} id The project ID.
*
* @apiParam (Required attributes) {Date} expires_at The date on which the project invitation will expire.
* @apiParam (Required attributes) {Number} role_id ID of the user role the new project members should have. Invited usery may not become project admins. Default is "editor".
*
* @apiParam (Optional attributes) {Number} max_uses The number of times this project invitation can be used to adda user to the project.
* @apiParam (Required attributes) {Number} role_id ID of the user role the new project members should have. Invited usery may not become project admins. Default is "editor".
* @apiParam (Optional attributes) {Boolean} add_to_sessions If set to `true`, all users joining the project will automatically be added to all annotation sessions of all volumes that belong to the project.
*
* @param StoreProjectInvitation $request
* @return \Illuminate\Http\Response
Expand All @@ -41,6 +43,7 @@ public function store(StoreProjectInvitation $request)
'expires_at' => $request->input('expires_at'),
'role_id' => $request->input('role_id', Role::editorId()),
'max_uses' => $request->input('max_uses'),
'add_to_sessions' => $request->input('add_to_sessions', false),
]);
}

Expand Down Expand Up @@ -72,9 +75,16 @@ public function join(JoinProjectInvitation $request, $id)
->findOrFail($id);

$project = $request->invitation->project;
if (!$project->users()->where('user_id', $request->user()->id)->exists()) {
$project->addUserId($request->user()->id, $request->invitation->role_id);
$userId = $request->user()->id;
if (!$project->users()->where('user_id', $userId)->exists()) {
$project->addUserId($userId, $request->invitation->role_id);
$invitation->increment('current_uses');

if ($invitation->add_to_sessions) {
AnnotationSession::join('project_volume', 'annotation_sessions.volume_id', '=', 'project_volume.volume_id')
->where('project_volume.project_id', $project->id)
->eachById(fn ($session) => $session->users()->attach($userId));
}
}

return $invitation;
Expand Down
16 changes: 16 additions & 0 deletions app/Http/Requests/StoreProjectInvitation.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,25 @@ public function rules()
'expires_at' => "required|date|after:today",
'role_id' => "in:{$roles}",
'max_uses' => "integer|min:1",
'add_to_sessions' => "boolean",
];
}

/**
* Configure the validator instance.
*
* @param \Illuminate\Validation\Validator $validator
* @return void
*/
public function withValidator($validator)
{
$validator->after(function ($validator) {
if ($this->input('add_to_sessions') && intval($this->input('role_id')) === Role::guestId()) {
$validator->errors()->add('add_to_sessions', 'Project guests cannot be added to annotation sessions. Use a different role.');
}
});
}

/**
* Get the error messages for the defined validation rules.
*
Expand Down
2 changes: 2 additions & 0 deletions app/ProjectInvitation.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class ProjectInvitation extends Model
'max_uses',
'project_id',
'role_id',
'add_to_sessions',
];

/**
Expand All @@ -29,6 +30,7 @@ class ProjectInvitation extends Model
*/
protected $casts = [
'expires_at' => 'datetime',
'add_to_sessions' => 'bool',
];

/**
Expand Down
1 change: 1 addition & 0 deletions database/factories/ProjectInvitationFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public function definition()
'project_id' => Project::factory(),
'role_id' => Role::editorId(),
'current_uses' => 0,
'add_to_sessions' => false,
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('project_invitations', function (Blueprint $table) {
$table->boolean('add_to_sessions')->default(false);
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('project_invitations', function (Blueprint $table) {
$table->dropColumn('add_to_sessions');
});
}
};
11 changes: 11 additions & 0 deletions resources/assets/js/projects/components/createInvitationForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@
v-model="maxUses"
>
</div>
<div class="form-group">
<label>
<input
type="checkbox"
v-model="addToSessions"
>
Add to annotation sessions
</label>
</div>
<button
class="btn btn-success btn-block"
type="submit"
Expand Down Expand Up @@ -75,6 +84,7 @@ export default {
expiresAt: null,
roleId: null,
maxUses: null,
addToSessions: false,
};
},
computed: {
Expand Down Expand Up @@ -106,6 +116,7 @@ export default {
let payload = {
expires_at: isoExpiresAt,
role_id: this.roleId,
add_to_sessions: this.addToSessions,
};
if (this.maxUses > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<li class="list-group-item invitation-list-item">
<h4 class="list-group-item-heading">
<span :class="classObject">
Invitation
Invitation for role <span v-text="role.name"></span>
</span>
<small v-if="expired">
(expired)
Expand Down Expand Up @@ -37,7 +37,7 @@
</span>
</h4>
<p class="list-group-item-text text-muted">
Role: <span v-text="role.name"></span>.
<span v-if="invitation.add_to_sessions">Add to annotation sessions.</span>
<span>
Used: <span v-text="uses"></span><span v-if="invitation.max_uses"> of <span v-text="invitation.max_uses"></span></span> times.
</span>
Expand Down
2 changes: 1 addition & 1 deletion resources/views/manual/tutorials/projects/about.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
Each project is only visible to project members. Each member has one of four roles: admin, expert, editor or guest. To modify project members you must be admin in the project. Click on the <button class="btn btn-default btn-xs"><i class="fa fa-users"></i> Members</button> tab of the project overview to modify members. To add a new member, click the <button class="btn btn-default btn-xs"><i class="fa fa-user-plus"></i> Add member</button> button at the top right, then enter a username, choose a role and click <button class="btn btn-success btn-xs">Add</button> to add a new member to the project. Hover the mouse over a member in the list to choose a new user role using the dropdown list or click the <button class="btn btn-default btn-xs"><i class="fa fa-trash"></i></button> button to remove a member from the roject. You cannot modify or remove yourself as a member of the project. Instead, ask another project admin to do this.
</p>
<p>
You can also create links to invite users as new members to the project. To create a new invitation link, click the <button class="btn btn-default btn-xs"><i class="fa fa-envelope"></i> Create invitation</button> button at the top right of the members tab. Then choose an expiration date, new member role and optional number of maximum uses for the invitation link and click <button class="btn btn-success btn-xs">Create</button>. By visiting the invitation link, users can add themselves to the project as long as the expiration date is not past and the number of maximum uses for the link is not met. This link can also be sent to people without a BIIGLE account, as they will be automatically redirected to the sign up page and then asked to join the project after they have created a user account.
You can also create links to invite users as new members to the project. To create a new invitation link, click the <button class="btn btn-default btn-xs"><i class="fa fa-envelope"></i> Create invitation</button> button at the top right of the members tab. Then choose an expiration date, new member role and optional number of maximum uses for the invitation link and click <button class="btn btn-success btn-xs">Create</button>. By visiting the invitation link, users can add themselves to the project as long as the expiration date is not past and the number of maximum uses for the link is not met. This link can also be sent to people without a BIIGLE account, as they will be automatically redirected to the sign up page and then asked to join the project after they have created a user account. You can also select the option "add to annotation sessions" for a new invitation. If users join a project through an invitation that has this option active, they will be automatically added to all <a href="{{route('manual-tutorials', ['volumes', 'annotation-sessions'])}}">annotation sessions</a> of all volumes that belong to the project.
</p>

<h4>Member roles</h4>
Expand Down
56 changes: 54 additions & 2 deletions tests/php/Http/Controllers/Api/ProjectInvitationControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Biigle\Tests\Http\Controllers\Api;

use ApiTestCase;
use Biigle\AnnotationSession;
use Biigle\ProjectInvitation;
use Biigle\Role;

Expand Down Expand Up @@ -44,6 +45,7 @@ public function testStore()
$this->assertEquals(0, $invitation->current_uses);
$this->assertNotNull($invitation->uuid);
$this->assertNull($invitation->max_uses);
$this->assertFalse($invitation->add_to_sessions);
$this->assertEquals(Role::editorId(), $invitation->role_id);
}

Expand Down Expand Up @@ -78,15 +80,41 @@ public function testStoreOptionalAttributes()
$this
->postJson("/api/v1/projects/{$id}/invitations", [
'expires_at' => $timestamp,
'role_id' => Role::guestId(),
'add_to_sessions' => 123,
])
->assertStatus(422);

$this
->postJson("/api/v1/projects/{$id}/invitations", [
'expires_at' => $timestamp,
'role_id' => Role::editorId(),
'max_uses' => 10,
'add_to_sessions' => true,
])
->assertSuccessful();

$invitation = $this->project()->invitations()->first();
$this->assertNotNull($invitation);
$this->assertEquals(10, $invitation->max_uses);
$this->assertEquals(Role::guestId(), $invitation->role_id);
$this->assertEquals(Role::editorId(), $invitation->role_id);
$this->assertTrue($invitation->add_to_sessions);
}

public function testStoreAddToSessionsRoleConflict()
{
$id = $this->project()->id;
$this->beAdmin();

$timestamp = now()->addDay();

// It makes no sense to add guests to annotation sessions, as they can't annotate.
$this
->postJson("/api/v1/projects/{$id}/invitations", [
'expires_at' => $timestamp,
'role_id' => Role::guestId(),
'add_to_sessions' => true,
])
->assertStatus(422);
}

public function testDestroy()
Expand Down Expand Up @@ -207,6 +235,30 @@ public function testJoinAlreadyMember()
$this->assertEquals(0, $invitation->fresh()->current_uses);
}

public function testJoinAddToSessions()
{
$session = AnnotationSession::factory()->create([
'volume_id' => $this->volume()->id,
'starts_at' => '2023-11-04',
'ends_at' => '2023-11-05',
]);

$invitation = ProjectInvitation::factory()->create([
'project_id' => $this->project()->id,
'role_id' => Role::editorId(),
'add_to_sessions' => true,
]);

$this->beUser();
$this
->postJson("/api/v1/project-invitations/{$invitation->id}/join", [
'token' => $invitation->uuid,
])
->assertSuccessful();

$this->assertNotNull($session->users()->find($this->user()->id));
}

public function testShowQrCode()
{
$invitation = ProjectInvitation::factory()->create([
Expand Down
1 change: 1 addition & 0 deletions tests/php/ProjectInvitationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public function testAttributes()
$this->assertNotNull($this->model->role_id);
$this->assertNotNull($this->model->current_uses);
$this->assertNull($this->model->max_uses);
$this->assertFalse($this->model->add_to_sessions);
}

public function testMaxUsesConstraint()
Expand Down

0 comments on commit 619ac62

Please sign in to comment.