Skip to content

Commit

Permalink
feat: tasks (#643)
Browse files Browse the repository at this point in the history
Resolves #375 

---------

Co-authored-by: Thor K. Høgås <[email protected]>
  • Loading branch information
blt950 and thor authored Oct 9, 2023
1 parent 39b5068 commit 24daf8e
Show file tree
Hide file tree
Showing 34 changed files with 1,172 additions and 17 deletions.
79 changes: 79 additions & 0 deletions app/Console/Commands/SendTaskNotifications.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace App\Console\Commands;

use App\Helpers\TaskStatus;
use App\Models\Task;
use App\Models\User;
use App\Notifications\TaskNotification;
use Illuminate\Console\Command;

class SendTaskNotifications extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'send:task:notifications';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Send out the digest of task notifications';

/**
* Execute the console command.
*
* @return int
*/
public function handle()
{

// For recipients who have not yet been notified
$pendingTasks = Task::where('status', TaskStatus::PENDING)->where('assignee_notified', false)->get();

// For senders who have not yet been notified
$completedTasks = Task::where('status', TaskStatus::COMPLETED)->where('creator_notified', false)->get();
$declinedTasks = Task::where('status', TaskStatus::DECLINED)->where('creator_notified', false)->get();

// Put together the list of email recipients
$tasks = $pendingTasks->merge($completedTasks)->merge($declinedTasks);
$usersRecipients = $pendingTasks->pluck('assignee_user_id')->merge($completedTasks->pluck('creator_user_id'))->merge($declinedTasks->pluck('creator_user_id'))->unique();
$userModels = User::whereIn('id', $usersRecipients)->get();

foreach ($userModels as $user) {

// If no tasks for this user, skip
if (! $tasks->where('assignee_user_id', $user->id)->count() && ! $tasks->where('creator_user_id', $user->id)->count()) {
continue;
}

// If user has disabled task notifications, mark as notified and skip
if (! $user->setting_notify_tasks) {

$tasks->where('assignee_user_id', $user->id)->each(function ($task) {
$task->assignee_notified = true;
$task->save();
});

$tasks->where('creator_user_id', $user->id)->each(function ($task) {
$task->creator_notified = true;
$task->save();
});

continue;
}

$user->notify(new TaskNotification(
$user,
$tasks->where('assignee_user_id', $user->id),
$tasks->where('creator_user_id', $user->id)->whereIn('status', [TaskStatus::COMPLETED, TaskStatus::DECLINED]),
));
}

return Command::SUCCESS;
}
}
4 changes: 4 additions & 0 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ protected function schedule(Schedule $schedule)
$schedule->command('update:workmails')
->daily();

// Send task notifications
$schedule->command('send:task:notifications')
->hourly();

// Send telemetry data
if (Setting::get('telemetryEnabled')) {
$schedule->command('send:telemetry')
Expand Down
13 changes: 13 additions & 0 deletions app/Helpers/TaskStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace App\Helpers;

/**
* Constants for task status.
*/
enum TaskStatus: int
{
case PENDING = 0;
case DECLINED = -1;
case COMPLETED = 1;
}
159 changes: 159 additions & 0 deletions app/Http/Controllers/TaskController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?php

namespace App\Http\Controllers;

use App\Helpers\TaskStatus;
use App\Models\Task;
use App\Models\User;
use App\Rules\ValidTaskType;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\View\View;

class TaskController extends Controller
{
/**
* Show the application task dashboard.
*/
public function index(Authenticatable $user, string $activeFilter = null): View
{
$this->authorize('update', Task::class);

if ($activeFilter == 'sent') {
$tasks = Task::where('creator_user_id', $user->id)->get()->sortBy('created_at');
} elseif ($activeFilter == 'archived') {
$tasks = Task::where('assignee_user_id', $user->id)->whereIn('status', [TaskStatus::COMPLETED->value, TaskStatus::DECLINED->value])->get()->sortBy('created_at');
} else {
$tasks = Task::where('assignee_user_id', $user->id)->where('status', TaskStatus::PENDING->value)->with('creator', 'subject', 'assignee', 'subjectTraining')->get()->sortBy('created_at');
}

return view('tasks.index', compact('tasks', 'activeFilter'));
}

/**
* Store a newly created task in storage.
*/
public function store(Request $request, Authenticatable $user): RedirectResponse
{

$this->authorize('create', Task::class);

$data = $request->validate([
'type' => ['required', new ValidTaskType],
'message' => 'sometimes|min:3|max:256',
'subject_user_id' => 'required|exists:users,id',
'subject_training_id' => 'required|exists:trainings,id',
'assignee_user_id' => 'required|exists:users,id',
]);

$data['creator_user_id'] = $user->id;
$data['created_at'] = now();

// Check if recipient is mentor or above
$recipient = User::findOrFail($data['assignee_user_id']);

// Policy check if recpient can recieve a task
if ($recipient->can('receive', Task::class)) {
// Create the model
$task = Task::create($data);

// Run the create method on the task type to trigger type specific actions on creation
$task->type()->create($task);

return redirect()->back()->with('success', 'Task created successfully.');
}

return redirect()->back()->withErrors('Recipient is not allowed to receive tasks.');

}

/**
* Close the specified task with a given status.
*/
protected function close(Task|int $task, TaskStatus $status): Task
{
$this->authorize('update', Task::class);
$task = Task::findOrFail($task);
$task->status = $status;
$task->closed_at = now();
$task->save();

return $task;
}

/**
* Complete the specified task.
*/
public function complete(Request $request, Task|int $task): RedirectResponse
{
$completed = self::close($task, TaskStatus::COMPLETED);

// Run the complete method on the task type to trigger type specific actions on completion
$completed->type()->complete($completed);

return redirect()->back()->with('success', sprintf('Completed task regarding %s from %s.', $completed->subject->name, $completed->creator->name));
}

/**
* Decline the specified task.
*/
public function decline(Request $request, Task|int $task): RedirectResponse
{
$declined = self::close($task, TaskStatus::DECLINED);

// Run the decline method on the task type to trigger type specific actions on decline
$declined->type()->decline($declined);

return redirect()->back()->with('success', sprintf('Declined task regarding %s from %s.', $declined->subject->name, $declined->creator->name));
}

/**
* Return the task type classes
*
* @return array
*/
public static function getTypes()
{
// Specify the directory where your subclasses are located
$subclassesDirectory = app_path('Tasks/Types');

// Initialize an array to store the subclasses
$subclasses = [];

// Get all PHP files in the directory
$files = File::files($subclassesDirectory);

foreach ($files as $file) {
// Get the class name from the file path
$className = 'App\\Tasks\\Types\\' . pathinfo($file, PATHINFO_FILENAME);

// Check if the class exists and is a subclass of Types
if (class_exists($className) && is_subclass_of($className, 'App\\Tasks\\Types\\Types')) {
$subclasses[] = new $className();
}
}

return $subclasses;
}

/**
* Check if a task type is valid
*
* @param string $type
* @return bool
*/
public static function isValidType($type)
{
$types = self::getTypes();

foreach ($types as $taskType) {
if ($taskType::class == $type) {
return true;
}
}

return false;
}
}
4 changes: 3 additions & 1 deletion app/Http/Controllers/TrainingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,9 @@ public function show(Training $training)
$trainingInterests = TrainingInterest::where('training_id', $training->id)->orderBy('created_at', 'DESC')->get();
$activeTrainingInterest = TrainingInterest::where('training_id', $training->id)->where('expired', false)->get()->count();

return view('training.show', compact('training', 'reportsAndExams', 'trainingMentors', 'statuses', 'types', 'experiences', 'activities', 'trainingInterests', 'activeTrainingInterest'));
$requestTypes = TaskController::getTypes();

return view('training.show', compact('training', 'reportsAndExams', 'trainingMentors', 'statuses', 'types', 'experiences', 'activities', 'trainingInterests', 'activeTrainingInterest', 'requestTypes'));
}

/**
Expand Down
3 changes: 3 additions & 0 deletions app/Http/Controllers/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -286,18 +286,21 @@ public function settings_update(Request $request, User $user)
'setting_notify_newreq' => '',
'setting_notify_closedreq' => '',
'setting_notify_newexamreport' => '',
'setting_notify_tasks' => '',
'setting_workmail_address' => 'nullable|email|max:64|regex:/(.*)' . Setting::get('linkDomain') . '$/i',
]);

isset($data['setting_notify_newreport']) ? $setting_notify_newreport = true : $setting_notify_newreport = false;
isset($data['setting_notify_newreq']) ? $setting_notify_newreq = true : $setting_notify_newreq = false;
isset($data['setting_notify_closedreq']) ? $setting_notify_closedreq = true : $setting_notify_closedreq = false;
isset($data['setting_notify_newexamreport']) ? $setting_notify_newexamreport = true : $setting_notify_newexamreport = false;
isset($data['setting_notify_tasks']) ? $setting_notify_tasks = true : $setting_notify_tasks = false;

$user->setting_notify_newreport = $setting_notify_newreport;
$user->setting_notify_newreq = $setting_notify_newreq;
$user->setting_notify_closedreq = $setting_notify_closedreq;
$user->setting_notify_newexamreport = $setting_notify_newexamreport;
$user->setting_notify_tasks = $setting_notify_tasks;

if (! $user->setting_workmail_address && isset($data['setting_workmail_address'])) {
$user->setting_workmail_address = $data['setting_workmail_address'];
Expand Down
48 changes: 48 additions & 0 deletions app/Mail/TaskMail.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace App\Mail;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class TaskMail extends Mailable
{
use Queueable, SerializesModels;

private $mailSubject;

private $user;

private $textLines;

/**
* Create a new message instance.
*
* @param string $mailSubject the subject of the email
* @param User $user the user to send the email to
* @param array $textLines an array of markdown lines to add
* @return void
*/
public function __construct(string $mailSubject, User $user, array $textLines)
{
$this->mailSubject = $mailSubject;
$this->user = $user;
$this->textLines = $textLines;
}

/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->subject($this->mailSubject)->markdown('mail.tasks', [
'firstName' => $this->user->first_name,
'textLines' => $this->textLines,
'actionUrl' => route('tasks'),
]);
}
}
47 changes: 47 additions & 0 deletions app/Models/Task.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace App\Models;

use App\Helpers\TaskStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
use HasFactory;

protected $guarded = [];

protected $casts = [
'status' => TaskStatus::class,
];

public function creator()
{
return $this->belongsTo(User::class, 'creator_user_id');
}

public function subject()
{
return $this->belongsTo(User::class, 'subject_user_id');
}

public function subjectTraining()
{
return $this->belongsTo(Training::class, 'subject_training_id');
}

public function assignee()
{
return $this->belongsTo(User::class, 'assignee_user_id');
}

public function type()
{
if ($this->type) {
return app($this->type);
} else {
throw new \Exception('Invalid task type: ' . $this->type);
}
}
}
Loading

0 comments on commit 24daf8e

Please sign in to comment.