Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions app/Http/Controllers/CourseTrackController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,28 @@
use App\Models\Course;
use App\Models\CourseTrack;
use App\Models\Task;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;

class CourseTrackController extends Controller
{

public function index(Course $course) : View
{
$tracks = $course->tracks()->get();

$tracks->map(function(CourseTrack $track) {
$track->depth = $track->getDepthAttribute();
});

return view('courses.tracks.index', [
'course' => $course,
'tracks' => $tracks,
]);
}

public function show(Course $course, CourseTrack $track) : View
{
$tasks = $course->tasks()->where('track_id', $track->id)->where('is_visible', true)->get()->map(fn(Task $task) => [
Expand All @@ -32,4 +49,143 @@ public function show(Course $course, CourseTrack $track) : View
],
]);
}

public function create(Request $request, Course $course) : View
{
Log::info("Create course track");

$parent = $course->tracks()->where('id', $request->query('parent'))->first();

return view('courses.tracks.create', [
'course' => $course,
'parent' => $parent,
]);
}

public function store(Request $request, Course $course) : RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255|min:1',
'description' => 'nullable|string',
'parent' => 'nullable|exists:course_tracks,id',
]);

/** @var CourseTrack $track */

$track = $course->tracks()->make([
'name' => $validated['name'],
'description' => $validated['description'],
]);

if (array_key_exists('parent', $validated) && $validated['parent'] != null)
{
$parent = $course->tracks()->where('id', $validated['parent'])->first();
if ($parent == null)
{
flash()->addError("Parent track does not exist!");

return redirect()->route('courses.manage.tracks.index', $course);
}

$track->parent()->associate($validated['parent']);
}

$track->save();

flash()->addSuccess("Track created successfully");

return redirect()->route('courses.manage.tracks.index', [$course, $track]);
}

public function destroy(Course $course, CourseTrack $track) : RedirectResponse
{
if ($track->children()->count() > 0)
{
flash()->addError("Track has sub tracks, remove them first!");

return redirect()->route('courses.manage.tracks.index', [$course]);
}

Log::info("Deleting course track $track->id");

$track->tasks()->each(function(Task $task) {
$task->track()->dissociate();
$task->save();
});

$track->delete();

flash()->addSuccess("Track deleted successfully");

return redirect()->route('courses.manage.tracks.index', $course);
}

public function assign(Course $course, CourseTrack $track) : View
{
$tasks = $course->tasks()->where('track_id', null)->get();

return view('courses.tracks.assign-tasks', [
'course' => $course,
'track' => $track,
'tasks' => $tasks,
]);
}

public function doAssign(Request $request, Course $course, CourseTrack $track) : RedirectResponse
{
if ($course->id != $track->course_id)
{
abort(403);
}

$validated = $request->validate([
'tasks' => 'required|array',
'tasks.*' => 'exists:tasks,id',
]);

$selectedTasks = $course->tasks()->whereIn('id', $validated['tasks'])->getQuery();
if ($selectedTasks->count() != count($validated['tasks']))
{
flash()->addError("Some tasks do not exist");

return redirect()->route('courses.manage.tracks.assign', [$course, $track]);
}

if ($selectedTasks->clone()->where('track_id', '!=', null)->count() > 0)
{
flash()->addError("Some tasks are already assigned to a track");

return redirect()->route('courses.manage.tracks.assign', [$course, $track]);
}


$selectedTasks->get()->each(function (Task $task) use ($track) {
$task->track()->associate($track);
$task->save();
});

flash()->addSuccess("Tasks assigned to track successfully");

return redirect()->route('courses.manage.tracks.index', $course);
}

public function unassign(Course $course, CourseTrack $track, Task $task) : RedirectResponse
{
if ($course->id != $track->course_id)
{
abort(403);
}

if ($task->track_id != $track->id)
{
abort(403);
}

$task->track()->dissociate();
$task->save();

flash()->addSuccess("Task unassigned from track successfully");

return redirect()->route('courses.manage.tracks.assign', [$course, $track]);
}
}
10 changes: 10 additions & 0 deletions app/Models/CourseTrack.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Models;

use Database\Factories\CourseTrackFactory;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
Expand All @@ -10,8 +11,12 @@
use Illuminate\Support\Facades\DB;

/**
* @property-read int $id
* @property int $course_id
* @property-read CourseTrack|null $parent
* @property-read CourseTrack[]|Collection $immediateChildren
* @method static CourseTrackFactory factory()
* @mixin \Eloquent
*/
class CourseTrack extends Model
{
Expand Down Expand Up @@ -140,4 +145,9 @@ public function rootChildrenNotInPath(bool $withChildren = true): Collection

return $remaining->whereNotIn('id', $this->children()->pluck('id'));
}

public function getDepthAttribute(): int
{
return $this->path()->count();
}
}
1 change: 1 addition & 0 deletions app/Models/Task.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
* @property string $name
* @static Task findOrFail($id, $columns = []) {
* @property SubTaskCollection $sub_tasks
* @property-read int $track_id
* @property-read CourseTrack|null $track
* @property-read SurveyTask|null $pivot
* @property-read bool $hasEnded
Expand Down
3 changes: 2 additions & 1 deletion database/factories/CourseTrackFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ class CourseTrackFactory extends Factory
public function definition()
{
return [
//
'name' => $this->faker->sentence(3),
];
}

}
4 changes: 4 additions & 0 deletions resources/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,7 @@
@apply bg-red-700 hover:bg-red-800 text-white py-2 px-2 rounded transition-colors text-center
}
}

.max-h-fit {
max-height: fit-content;
}
43 changes: 32 additions & 11 deletions resources/views/courses/manage/partials/sidebar.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
</x-sidebar-item>
<x-sidebar-group name="Tasks">
<x-sidebar-item name="Tasks" route="courses.manage.exercises.index" :route-params="[$course]">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5 text-gray-400">
<path fill-rule="evenodd" d="M2.625 6.75a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0zm4.875 0A.75.75 0 018.25 6h12a.75.75 0 010 1.5h-12a.75.75 0 01-.75-.75zM2.625 12a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0zM7.5 12a.75.75 0 01.75-.75h12a.75.75 0 010 1.5h-12A.75.75 0 017.5 12zm-4.875 5.25a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0zm4.875 0a.75.75 0 01.75-.75h12a.75.75 0 010 1.5h-12a.75.75 0 01-.75-.75z" clip-rule="evenodd" />
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="h-5 w-5 text-gray-400">
<path fill-rule="evenodd"
d="M2.625 6.75a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0zm4.875 0A.75.75 0 018.25 6h12a.75.75 0 010 1.5h-12a.75.75 0 01-.75-.75zM2.625 12a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0zM7.5 12a.75.75 0 01.75-.75h12a.75.75 0 010 1.5h-12A.75.75 0 017.5 12zm-4.875 5.25a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0zm4.875 0a.75.75 0 01.75-.75h12a.75.75 0 010 1.5h-12a.75.75 0 01-.75-.75z"
clip-rule="evenodd"/>
</svg>
</x-sidebar-item>
<x-sidebar-item name="Grades" route="courses.manage.grading.index" :route-params="[$course]">
Expand All @@ -20,23 +23,41 @@ class="h-5 w-5 text-gray-400">
</svg>
</x-sidebar-item>
</x-sidebar-group>
<x-sidebar-group name="Tracks">
<x-sidebar-item name="Tracks" route="courses.manage.tracks.index" :route-params="[$course]">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor" class="h-5 w-5 text-gray-400">
<path
d="M440-80v-200q0-56-17-83t-45-53l57-57q12 11 23 23.5t22 26.5q14-19 28.5-33.5T538-485q38-35 69-81t33-161l-63 63-57-56 160-160 160 160-56 56-64-63q-2 143-44 203.5T592-425q-32 29-52 56.5T520-280v200h-80ZM248-633q-4-20-5.5-44t-2.5-50l-64 63-56-56 160-160 160 160-57 56-63-62q0 21 2 39.5t4 34.5l-78 19Zm86 176q-20-21-38.5-49T263-575l77-19q10 27 23 46t28 34l-57 57Z"/>
</svg>
</x-sidebar-item>
</x-sidebar-group>
<x-sidebar-group name="User Management">
<x-sidebar-item name="Enrolled" route="courses.manage.enrolment.index" :route-params="[$course]">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5 text-gray-400">
<path d="M11.7 2.805a.75.75 0 01.6 0A60.65 60.65 0 0122.83 8.72a.75.75 0 01-.231 1.337 49.949 49.949 0 00-9.902 3.912l-.003.002-.34.18a.75.75 0 01-.707 0A50.009 50.009 0 007.5 12.174v-.224c0-.131.067-.248.172-.311a54.614 54.614 0 014.653-2.52.75.75 0 00-.65-1.352 56.129 56.129 0 00-4.78 2.589 1.858 1.858 0 00-.859 1.228 49.803 49.803 0 00-4.634-1.527.75.75 0 01-.231-1.337A60.653 60.653 0 0111.7 2.805z" />
<path d="M13.06 15.473a48.45 48.45 0 017.666-3.282c.134 1.414.22 2.843.255 4.285a.75.75 0 01-.46.71 47.878 47.878 0 00-8.105 4.342.75.75 0 01-.832 0 47.877 47.877 0 00-8.104-4.342.75.75 0 01-.461-.71c.035-1.442.121-2.87.255-4.286A48.4 48.4 0 016 13.18v1.27a1.5 1.5 0 00-.14 2.508c-.09.38-.222.753-.397 1.11.452.213.901.434 1.346.661a6.729 6.729 0 00.551-1.608 1.5 1.5 0 00.14-2.67v-.645a48.549 48.549 0 013.44 1.668 2.25 2.25 0 002.12 0z" />
<path d="M4.462 19.462c.42-.419.753-.89 1-1.394.453.213.902.434 1.347.661a6.743 6.743 0 01-1.286 1.794.75.75 0 11-1.06-1.06z" />
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="h-5 w-5 text-gray-400">
<path
d="M11.7 2.805a.75.75 0 01.6 0A60.65 60.65 0 0122.83 8.72a.75.75 0 01-.231 1.337 49.949 49.949 0 00-9.902 3.912l-.003.002-.34.18a.75.75 0 01-.707 0A50.009 50.009 0 007.5 12.174v-.224c0-.131.067-.248.172-.311a54.614 54.614 0 014.653-2.52.75.75 0 00-.65-1.352 56.129 56.129 0 00-4.78 2.589 1.858 1.858 0 00-.859 1.228 49.803 49.803 0 00-4.634-1.527.75.75 0 01-.231-1.337A60.653 60.653 0 0111.7 2.805z"/>
<path
d="M13.06 15.473a48.45 48.45 0 017.666-3.282c.134 1.414.22 2.843.255 4.285a.75.75 0 01-.46.71 47.878 47.878 0 00-8.105 4.342.75.75 0 01-.832 0 47.877 47.877 0 00-8.104-4.342.75.75 0 01-.461-.71c.035-1.442.121-2.87.255-4.286A48.4 48.4 0 016 13.18v1.27a1.5 1.5 0 00-.14 2.508c-.09.38-.222.753-.397 1.11.452.213.901.434 1.346.661a6.729 6.729 0 00.551-1.608 1.5 1.5 0 00.14-2.67v-.645a48.549 48.549 0 013.44 1.668 2.25 2.25 0 002.12 0z"/>
<path
d="M4.462 19.462c.42-.419.753-.89 1-1.394.453.213.902.434 1.347.661a6.743 6.743 0 01-1.286 1.794.75.75 0 11-1.06-1.06z"/>
</svg>
</x-sidebar-item>
<x-sidebar-item name="Groups" route="courses.manage.groups.index" :route-params="[$course]">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5 text-gray-400">
<path d="M4.5 6.375a4.125 4.125 0 118.25 0 4.125 4.125 0 01-8.25 0zM14.25 8.625a3.375 3.375 0 116.75 0 3.375 3.375 0 01-6.75 0zM1.5 19.125a7.125 7.125 0 0114.25 0v.003l-.001.119a.75.75 0 01-.363.63 13.067 13.067 0 01-6.761 1.873c-2.472 0-4.786-.684-6.76-1.873a.75.75 0 01-.364-.63l-.001-.122zM17.25 19.128l-.001.144a2.25 2.25 0 01-.233.96 10.088 10.088 0 005.06-1.01.75.75 0 00.42-.643 4.875 4.875 0 00-6.957-4.611 8.586 8.586 0 011.71 5.157v.003z" />
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="h-5 w-5 text-gray-400">
<path
d="M4.5 6.375a4.125 4.125 0 118.25 0 4.125 4.125 0 01-8.25 0zM14.25 8.625a3.375 3.375 0 116.75 0 3.375 3.375 0 01-6.75 0zM1.5 19.125a7.125 7.125 0 0114.25 0v.003l-.001.119a.75.75 0 01-.363.63 13.067 13.067 0 01-6.761 1.873c-2.472 0-4.786-.684-6.76-1.873a.75.75 0 01-.364-.63l-.001-.122zM17.25 19.128l-.001.144a2.25 2.25 0 01-.233.96 10.088 10.088 0 005.06-1.01.75.75 0 00.42-.643 4.875 4.875 0 00-6.957-4.611 8.586 8.586 0 011.71 5.157v.003z"/>
</svg>
</x-sidebar-item>
<x-sidebar-item name="Activity" route="courses.manage.activity.index" :route-params="[$course]">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5 text-gray-400">
<path fill-rule="evenodd" d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0016.5 9h-1.875a1.875 1.875 0 01-1.875-1.875V5.25A3.75 3.75 0 009 1.5H5.625zM7.5 15a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5A.75.75 0 017.5 15zm.75 2.25a.75.75 0 000 1.5H12a.75.75 0 000-1.5H8.25z" clip-rule="evenodd" />
<path d="M12.971 1.816A5.23 5.23 0 0114.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 013.434 1.279 9.768 9.768 0 00-6.963-6.963z" />
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="h-5 w-5 text-gray-400">
<path fill-rule="evenodd"
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0016.5 9h-1.875a1.875 1.875 0 01-1.875-1.875V5.25A3.75 3.75 0 009 1.5H5.625zM7.5 15a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5A.75.75 0 017.5 15zm.75 2.25a.75.75 0 000 1.5H12a.75.75 0 000-1.5H8.25z"
clip-rule="evenodd"/>
<path
d="M12.971 1.816A5.23 5.23 0 0114.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 013.434 1.279 9.768 9.768 0 00-6.963-6.963z"/>
</svg>
</x-sidebar-item>
</x-sidebar-group>
Expand Down
59 changes: 59 additions & 0 deletions resources/views/courses/tracks/assign-tasks.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
@extends('master')

@section('content')
<div class="px-6 pt-6 container mx-auto">
<form action="{{ route('courses.manage.tracks.doAssign', [$course, $track]) }}" method="post">
@csrf
<div
class="py-4 px-6 lg:max-w-xl container mx-auto flex flex-col bg-white border dark:border-gray-700 shadow dark:bg-gray-800 rounded-md">
<h2 class="text-2xl font-bold mb-2 text-lime-green-700 dark:text-lime-green-400">Assigning tasks to track "{{$track->name}}"</h2>
<p class="text-gray-500">Hold <code>ctrl</code> or <code>cmd</code> to do a multi select.</p>
<div class="mb-4">
<label for="tasks" class="text-sm text-lime-green-700 dark:text-gray-300">
Tasks*
</label>
<select id="course-track-tasks" name="tasks[]" multiple required
class=" bg-gray-50 flex-grow border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:outline-none focus:ring-2 focus:ring-lime-green-400 block w-full p-2.5 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-200">
@foreach($tasks as $task)
<option value="{{ $task->id }}">{{ $task->name }}</option>
@endforeach
</select>
</div>
@if($errors->any())
<div class="text-red-800 dark:text-red-500 text-sm font-semibold">
<ul>
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<button
class="flex-shrink-0 mt-3 bg-white hover:bg-lime-green-500 text-lime-green-700 font-semi-bold hover:text-white py-2 px-4 border border-lime-green-500 hover:border-transparent rounded-lg"
type="submit">
Assign
</button>
</div>
</form>

<div class="mt-6 py-4 px-6 lg:max-w-xl container mx-auto flex flex-col bg-white border dark:border-gray-700 shadow dark:bg-gray-800 rounded-md">
<h2 class="text-xl font-bold mb-2 text-lime-green-700 dark:text-lime-green-400">Currently Assigned Tasks</h2>
@if($track->tasks->isEmpty())
<p class="text-gray-400 text-sm">No tasks are currently assigned to this track.</p>
@else
<ul class="list-disc pl-5">
@foreach($track->tasks as $task)
<li class="flex justify-between items-center mb-2">
<span class="text-gray-800 dark:text-gray-200">{{ $task->name }}</span>
<form action="{{ route('courses.manage.tracks.unassign', [$course, $track, $task]) }}" method="post">
@csrf
@method('delete')
<button type="submit" class="text-white bg-red-500 hover:bg-red-600 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-800">Unassign</button>
</form>
</li>
@endforeach
</ul>
@endif
</div>
</div>
@endsection
Loading