diff --git a/.env.example b/.env.example index 5dd89dc..e6ff2eb 100644 --- a/.env.example +++ b/.env.example @@ -7,12 +7,15 @@ APP_URL=http://localhost SEED_ADMIN_EMAIL=admin@example.com SEED_ADMIN_PASSWORD=admin_user -SEED_NEWS_EDITOR_EMAIL="news.editor@example.com" +SEED_NEWS_EDITOR_EMAIL="user+news.editor@example.com" SEED_NEWS_EDITOR_PASSWORD="news_editor" -SEED_EVENT_EDITOR_EMAIL="events.editor@example.com" +SEED_EVENT_EDITOR_EMAIL="user+events.editor@example.com" SEED_EVENT_EDITOR_PASSWORD="events_editor" +SEED_COURSE_MANAGER_EMAIL="course_manager@portal.ce.pdn.ac.lk" +SEED_COURSE_MANAGER_PASSWORD="course_manager" + SEED_USER_EMAIL=user@user.com SEED_USER_PASSWORD=regular_user @@ -22,6 +25,7 @@ APP_READ_ONLY_LOGIN=true DEBUGBAR_ENABLED=false LOG_CHANNEL=daily LOG_LEVEL=debug +LOG_DISCORD_WEBHOOK_URL= # Drivers DB_CONNECTION=mysql diff --git a/.github/workflows/laravel.yml b/.github/workflows/laravel_pull_request.yml similarity index 100% rename from .github/workflows/laravel.yml rename to .github/workflows/laravel_pull_request.yml diff --git a/.github/workflows/laravel_push.yml b/.github/workflows/laravel_push.yml new file mode 100644 index 0000000..d6f4a21 --- /dev/null +++ b/.github/workflows/laravel_push.yml @@ -0,0 +1,31 @@ +name: Laravel Push Test + +on: [push] + +jobs: + laravel-tests: + runs-on: ubuntu-latest + + steps: + - uses: shivammathur/setup-php@15c43e89cdef867065b0213be354c2841860869e + with: + php-version: '8.0' + - uses: actions/checkout@v2 + - name: Copy .env + run: php -r "file_exists('.env') || copy('.env.example', '.env');" + - name: Install Dependencies + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + - name: Generate key + run: php artisan key:generate + - name: Directory Permissions + run: chmod -R 777 storage bootstrap/cache + - name: Create Database + run: | + mkdir -p database + touch database/database.sqlite + - name: Execute tests (Unit and Feature tests) via PHPUnit + env: + DB_CONNECTION: sqlite + DB_DATABASE: database/database.sqlite + run: | + php artisan test -p --colors --debug \ No newline at end of file diff --git a/README.md b/README.md index 191ace7..9b47e4e 100644 --- a/README.md +++ b/README.md @@ -14,18 +14,18 @@ Please make sure you already created a Database and a Database User Account. #### Install Dependencies -``` +```bash // Install PHP dependencies composer install -// Install Node dependencies (development mode) -npm install -npm run dev +// Install Node dependencies (development mode, can use `npm` as well, but recommended to use `pnpm` here) +pnpm install +pnpm run dev ``` ##### Additional useful commands -``` +```bash // If you received mmap() error, use this command php -d memory_limit=-1 /usr/local/bin/composer install @@ -42,7 +42,7 @@ First you need to copy `.env.example` and save as `.env` in the root folder, and Next follow the below commands -``` +```bash // Prepare the public link for storage php artisan storage:link @@ -56,20 +56,20 @@ git config --local core.hooksPath .githooks #### Serve in the Local environment -``` +```bash // Serve PHP web server php artisan serve // Serve PHP web server, in a specific IP & port php artisan serve --host=0.0.0.0 --port=8000 -// To work with Vue components, you need to run this in parallel -npm run watch +// To work with Vue components, you need to run this in parallel (can use `npm` as well, but recommended to use `pnpm` here) +pnpm run watch ``` #### Cache and optimization -``` +```bash // Remove dev dependencies composer install --optimize-autoloader --no-dev @@ -84,14 +84,14 @@ php artisan view:clear #### Maintenance related commands -``` +```bash php artisan down --message="{Message}" --retry=60 php artisan up ``` #### Other useful instructions -``` +```bash // Create Model, Controller and Database Seeder php artisan make:model {name} --migration --controller --seed @@ -106,6 +106,15 @@ php artisan test ``` +#### Maintenance Scripts + +Can be found under `./scripts.` folder. In the production environment, scripts need to be run with `sudo` from the base directory to work correctly. + +Ex: +```bash +sudo sh ./scripts/deploy-prod.sh +``` + #### Resource Routes - Standardard Pattern | Verb | URI | Action | Route Name | diff --git a/app/Domains/AcademicProgram/AcademicProgram.php b/app/Domains/AcademicProgram/AcademicProgram.php new file mode 100644 index 0000000..248b4ce --- /dev/null +++ b/app/Domains/AcademicProgram/AcademicProgram.php @@ -0,0 +1,38 @@ + 'Undergraduate', + 'postgraduate' => 'Postgraduate' + ]; + } + + public static function getVersions(): array + { + // TODO integrate with Taxonomies + return [ + 1 => 'Current Curriculum', + 2 => 'Curriculum - Effective from E22' + ]; + } + + public static function getTypes(): array + { + return [ + 'Found' => 'Foundation', + 'Core' => 'Core', + 'GE' => 'General Elective', + 'TE' => 'Technical Elective' + ]; + } +} \ No newline at end of file diff --git a/app/Domains/AcademicProgram/Course/Models/Course.php b/app/Domains/AcademicProgram/Course/Models/Course.php new file mode 100644 index 0000000..d55c46d --- /dev/null +++ b/app/Domains/AcademicProgram/Course/Models/Course.php @@ -0,0 +1,136 @@ + 'string', + 'type' => 'string', + 'objectives' => 'json', + 'time_allocation' => 'json', + 'marks_allocation' => 'json', + 'ilos' => 'json', + 'references' => 'json', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + public static function getILOTemplate(): array + { + // TODO Get the list from Taxonomies + return [ + 'general' => [], + 'knowledge' => [], + 'skills' => [], + 'attitudes' => [], + ]; + } + public static function getMarksAllocation(): array + { + // TODO Get the list from Taxonomies + return [ + 'practicals' => null, + 'tutorials' => null, + 'quizzes' => null, + 'projects' => null, + 'participation' => null, + 'mid_exam' => null, + 'end_exam' => null, + ]; + } + + public static function getTimeAllocation(): array + { + // TODO Get the list from Taxonomies + return [ + 'lecture' => null, + 'tutorial' => null, + 'practical' => null, + 'design' => null, + 'assignment' => null, + 'independent_learning' => null + ]; + } + + public function academicProgram() + { + return $this->getAcademicPrograms()[$this->academic_program]; + } + + public function createdUser() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function updatedUser() + { + return $this->belongsTo(User::class, 'updated_by'); + } + + public function semester() + { + return $this->belongsTo(Semester::class, 'semester_id'); + } + + public function version() + { + return $this->getVersions()[$this->version]; + } + + public function modules() + { + return $this->hasMany(CourseModule::class); + } + + protected static function newFactory() + { + return CourseFactory::new(); + } +} \ No newline at end of file diff --git a/app/Domains/AcademicProgram/Course/Models/CourseModule.php b/app/Domains/AcademicProgram/Course/Models/CourseModule.php new file mode 100644 index 0000000..84d36d1 --- /dev/null +++ b/app/Domains/AcademicProgram/Course/Models/CourseModule.php @@ -0,0 +1,56 @@ + 'integer', + 'topic' => 'string', + 'description' => 'string', + 'time_allocation' => 'json', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'created_by' => 'integer', + 'updated_by' => 'integer', + ]; + + public function course() + { + return $this->belongsTo(Course::class); + } +} \ No newline at end of file diff --git a/app/Domains/AcademicProgram/Course/Models/Traits/Scope/CourseScope.php b/app/Domains/AcademicProgram/Course/Models/Traits/Scope/CourseScope.php new file mode 100644 index 0000000..cc11480 --- /dev/null +++ b/app/Domains/AcademicProgram/Course/Models/Traits/Scope/CourseScope.php @@ -0,0 +1,57 @@ +where('academic_program', $program); + } + + /** + * Scope a query to only include courses for a specific semester. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $semesterId + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeForSemester($query, $semesterId) + { + return $query->where('semester_id', $semesterId); + } + + /** + * Scope a query to only include courses of a specific type (Core, General Elective, Technical Elective). + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $type + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeOfType($query, $type) + { + return $query->where('type', $type); + } + + /** + * Scope a query to only include courses of a specific version. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $version + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeOfVersion($query, $version) + { + return $query->where('version', $version); + } +} \ No newline at end of file diff --git a/app/Domains/AcademicProgram/Semester/Models/Semester.php b/app/Domains/AcademicProgram/Semester/Models/Semester.php new file mode 100644 index 0000000..47a8d60 --- /dev/null +++ b/app/Domains/AcademicProgram/Semester/Models/Semester.php @@ -0,0 +1,61 @@ + 'string', + 'version' => 'integer', + 'academic_program' => 'string', + 'description' => 'string', + 'url' => 'string', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'created_by' => 'integer', + 'updated_by' => 'integer', + ]; + + + public function getLatestCurriculumAttribute() + { + $maxVersion = self::where('title', $this->title)->max('version'); + return $this->version === $maxVersion; + } + + public function createdUser() + { + return $this->belongsTo(User::class, 'created_by', 'id'); + } + + public function updatedUser() + { + return $this->belongsTo(User::class, 'updated_by', 'id'); + } + + public function academicProgram() + { + return $this->getAcademicPrograms()[$this->academic_program]; + } + + protected static function newFactory() + { + return SemesterFactory::new(); + } +} \ No newline at end of file diff --git a/app/Domains/AcademicProgram/Semester/Models/Traits/Scope/SemesterScope.php b/app/Domains/AcademicProgram/Semester/Models/Traits/Scope/SemesterScope.php new file mode 100644 index 0000000..9fcde35 --- /dev/null +++ b/app/Domains/AcademicProgram/Semester/Models/Traits/Scope/SemesterScope.php @@ -0,0 +1,33 @@ +where('version', $version); + } + + /** + * Scope a query to only include semesters for a specific academic program. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $program + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeForProgram($query, $program) + { + return $query->where('academic_program', $program); + } +} \ No newline at end of file diff --git a/app/Domains/Auth/Http/Controllers/Frontend/Auth/RegisterController.php b/app/Domains/Auth/Http/Controllers/Frontend/Auth/RegisterController.php index 3fadeb9..81739ad 100644 --- a/app/Domains/Auth/Http/Controllers/Frontend/Auth/RegisterController.php +++ b/app/Domains/Auth/Http/Controllers/Frontend/Auth/RegisterController.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; use LangleyFoxall\LaravelNISTPasswordRules\PasswordRules; +use App\Rules\ValidateAsInternalEmail; /** * Class RegisterController. @@ -74,7 +75,7 @@ protected function validator(array $data) { return Validator::make($data, [ 'name' => ['required', 'string', 'max:100'], - 'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')], + 'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users'), new ValidateAsInternalEmail()], 'password' => array_merge(['max:100'], PasswordRules::register($data['email'] ?? null)), 'terms' => ['required', 'in:1'], 'g-recaptcha-response' => ['required_if:captcha_status,true', new Captcha], diff --git a/app/Domains/Auth/Http/Controllers/Frontend/Auth/SocialController.php b/app/Domains/Auth/Http/Controllers/Frontend/Auth/SocialController.php index de0a032..46c7c82 100644 --- a/app/Domains/Auth/Http/Controllers/Frontend/Auth/SocialController.php +++ b/app/Domains/Auth/Http/Controllers/Frontend/Auth/SocialController.php @@ -5,6 +5,8 @@ use App\Domains\Auth\Events\User\UserLoggedIn; use App\Domains\Auth\Services\UserService; use Laravel\Socialite\Facades\Socialite; +use Illuminate\Support\Facades\Validator; +use App\Rules\ValidateAsInternalEmail; /** * Class SocialController. @@ -30,7 +32,30 @@ public function redirect($provider) */ public function callback($provider, UserService $userService) { - $user = $userService->registerProvider(Socialite::driver($provider)->user(), $provider); + // Validate for internal user + $info = Socialite::driver($provider)->user(); + $validator = Validator::make( + ['email' => $info->email, 'name' => $info->name], + ['email' => ['required', 'email', new ValidateAsInternalEmail()], 'name' => ['required']] + ); + + if ($validator->fails()) { + $errorMessage = ""; + $errors = $validator->errors(); + + foreach ($errors->messages() as $key => $messages) { + if (is_array($messages)) { + foreach ($messages as $message) { + $errorMessage .= $message . ' '; + } + } else { + $errorMessage .= $messages . ' '; + } + } + return redirect()->route('frontend.auth.login')->withFlashDanger(trim($errorMessage)); + } + + $user = $userService->registerProvider($info, $provider); if (!$user->isActive()) { auth()->logout(); @@ -43,4 +68,4 @@ public function callback($provider, UserService $userService) return redirect()->route(homeRoute()); } -} +} \ No newline at end of file diff --git a/app/Domains/Event/Models/Event.php b/app/Domains/Event/Models/Event.php index 2f78a16..5077a10 100644 --- a/app/Domains/Event/Models/Event.php +++ b/app/Domains/Event/Models/Event.php @@ -2,11 +2,12 @@ namespace App\Domains\Event\Models; -use App\Domains\Event\Models\Traits\Scope\EventScope; +use App\Domains\Auth\Models\User; use Database\Factories\EventFactory; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Spatie\Activitylog\Traits\LogsActivity; +use App\Domains\Event\Models\Traits\Scope\EventScope; +use Illuminate\Database\Eloquent\Factories\HasFactory; /** * Class News. @@ -53,6 +54,11 @@ public function thumbURL() else return config('constants.frontend.dummy_thumb'); } + public function user() + { + return $this->belongsTo(User::class, 'created_by'); + } + /** * Create a new factory instance for the model. * diff --git a/app/Domains/News/Models/News.php b/app/Domains/News/Models/News.php index 589c87c..3fb86c8 100644 --- a/app/Domains/News/Models/News.php +++ b/app/Domains/News/Models/News.php @@ -2,11 +2,13 @@ namespace App\Domains\News\Models; -use App\Domains\News\Models\Traits\Scope\NewsScope; +use App\Domains\Auth\Models\User; use Database\Factories\NewsFactory; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Spatie\Activitylog\Traits\LogsActivity; +use App\Domains\News\Models\Traits\Scope\NewsScope; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Factories\HasFactory; /** * Class News. @@ -47,6 +49,11 @@ public function thumbURL() else return config('constants.frontend.dummy_thumb'); } + public function user() + { + return $this->belongsTo(User::class, 'created_by'); + } + /** * Create a new factory instance for the model. * diff --git a/app/Http/Controllers/API/CourseApiController.php b/app/Http/Controllers/API/CourseApiController.php new file mode 100644 index 0000000..4e164e5 --- /dev/null +++ b/app/Http/Controllers/API/CourseApiController.php @@ -0,0 +1,38 @@ +has('curriculum')) { + $query->where('version', $request->curriculum); + } + + if ($request->has('semester')) { + $query->where('semester_id', $request->semester); + } + + if ($request->has('type')) { + $query->where('type', $request->type); + } + + $courses = $query->paginate(20); + + return CourseResource::collection($courses); + } catch (\Exception $e) { + Log::error('Error in CourseApiController@index', ['error' => $e->getMessage()]); + return response()->json(['message' => 'An error occurred while fetching courses'], 500); + } + } +} diff --git a/app/Http/Controllers/API/EventApiController.php b/app/Http/Controllers/API/EventApiController.php index 909108d..80e9c10 100644 --- a/app/Http/Controllers/API/EventApiController.php +++ b/app/Http/Controllers/API/EventApiController.php @@ -5,59 +5,74 @@ use App\Domains\Event\Models\Event; use App\Http\Controllers\Controller; use App\Http\Resources\EventResource; +use Illuminate\Support\Facades\Log; class EventApiController extends Controller { public function index() { - $perPage = 20; - $event = Event::where('enabled', 1)->orderBy('start_at', 'desc') - ->paginate($perPage); + try { + $perPage = 20; + $event = Event::where('enabled', 1)->orderBy('start_at', 'desc')->paginate($perPage); - if ($event->count() > 0) { return EventResource::collection($event); - } else { - return response()->json(['message' => 'Events not found'], 404); + } catch (\Exception $e) { + Log::error('Error in EventApiController@index', ['error' => $e->getMessage()]); + return response()->json(['message' => 'An error occurred while fetching events'], 500); } } public function upcoming() { - $perPage = 20; - $event = Event::getUpcomingEvents() - ->orderBy('start_at', 'asc') - ->paginate($perPage); + try { + $perPage = 20; + $event = Event::getUpcomingEvents() + ->orderBy('start_at', 'asc') + ->paginate($perPage); - if ($event->count() > 0) { - return EventResource::collection($event); - } else { - return response()->json(['message' => 'Events not found'], 404); + if ($event->count() > 0) { + return EventResource::collection($event); + } else { + return response()->json(['message' => 'Events not found'], 404); + } + } catch (\Exception $e) { + Log::error('Error in EventApiController@upcoming', ['error' => $e->getMessage()]); + return response()->json(['message' => 'An error occurred while fetching upcoming events'], 500); } } public function past() { - $perPage = 20; - $event = Event::getPastEvents() - ->orderBy('start_at', 'desc') - ->paginate($perPage); + try { + $perPage = 20; + $event = Event::getPastEvents() + ->orderBy('start_at', 'desc') + ->paginate($perPage); - if ($event->count() > 0) { - return EventResource::collection($event); - } else { - return response()->json(['message' => 'Events not found'], 404); + if ($event->count() > 0) { + return EventResource::collection($event); + } else { + return response()->json(['message' => 'Events not found'], 404); + } + } catch (\Exception $e) { + Log::error('Error in EventApiController@past', ['error' => $e->getMessage()]); + return response()->json(['message' => 'An error occurred while fetching past events'], 500); } } - public function show($id) { - $event = Event::find($id); + try { + $event = Event::find($id); - if ($event) { - return new EventResource($event); - } else { - return response()->json(['message' => 'Event not found'], 404); + if ($event) { + return new EventResource($event); + } else { + return response()->json(['message' => 'Event not found'], 404); + } + } catch (\Exception $e) { + Log::error('Error in EventApiController@show', ['error' => $e->getMessage(), 'id' => $id]); + return response()->json(['message' => 'An error occurred while fetching the event'], 500); } } } \ No newline at end of file diff --git a/app/Http/Controllers/API/NewsApiController.php b/app/Http/Controllers/API/NewsApiController.php index d69038d..993ed24 100644 --- a/app/Http/Controllers/API/NewsApiController.php +++ b/app/Http/Controllers/API/NewsApiController.php @@ -5,30 +5,35 @@ use App\Http\Resources\NewsResource; use App\Http\Controllers\Controller; use App\Domains\News\Models\News; +use Illuminate\Support\Facades\Log; class NewsApiController extends Controller { public function index() { - $perPage = 20; - $news = News::latest()->where('enabled', 1)->paginate($perPage); + try { + $perPage = 20; + $news = News::latest()->where('enabled', 1)->paginate($perPage); - if ($news->count() > 0) { return NewsResource::collection($news); - } else { - return response()->json(['message' => 'News not found'], 404); + } catch (\Exception $e) { + Log::error('Error in NewsApiController@index', ['error' => $e->getMessage()]); + return response()->json(['message' => 'An error occurred while fetching news'], 500); } } - public function show($id) { - $news = News::find($id); - - if ($news) { - return new NewsResource($news); - } else { - return response()->json(['message' => 'News not found'], 404); + try { + $news = News::find($id); + if ($news) { + return new NewsResource($news); + } else { + return response()->json(['message' => 'News not found'], 404); + } + } catch (\Exception $e) { + Log::error('Error in NewsApiController@show', ['error' => $e->getMessage()]); + return response()->json(['message' => 'An error occurred while fetching news'], 500); } } } \ No newline at end of file diff --git a/app/Http/Controllers/API/SemesterApiController.php b/app/Http/Controllers/API/SemesterApiController.php new file mode 100644 index 0000000..a493183 --- /dev/null +++ b/app/Http/Controllers/API/SemesterApiController.php @@ -0,0 +1,34 @@ +has('curriculum')) { + $query->where('version', $request->curriculum); + } + + if ($request->has('semester')) { + $query->where('id', $request->semester); + } + + $semesters = $query->paginate(20); + + return SemesterResource::collection($semesters); + } catch (\Exception $e) { + Log::error('Error in SemesterApiController@index', ['error' => $e->getMessage()]); + return response()->json(['message' => 'An error occurred while fetching semesters'], 500); + } + } +} diff --git a/app/Http/Controllers/Backend/AnnouncementController.php b/app/Http/Controllers/Backend/AnnouncementController.php index d17b2ae..62eb25b 100644 --- a/app/Http/Controllers/Backend/AnnouncementController.php +++ b/app/Http/Controllers/Backend/AnnouncementController.php @@ -6,6 +6,7 @@ use Illuminate\Http\Request; use App\Domains\Announcement\Models\Announcement; use Illuminate\Validation\Rule; +use Illuminate\Support\Facades\Log; class AnnouncementController extends Controller { @@ -17,9 +18,14 @@ class AnnouncementController extends Controller */ public function create() { - $areas = Announcement::areas(); - $types = Announcement::types(); - return view('backend.announcements.create', compact('areas', 'types')); + try { + $areas = Announcement::areas(); + $types = Announcement::types(); + return view('backend.announcements.create', compact('areas', 'types')); + } catch (\Exception $ex) { + Log::error('Failed to load announcement creation page', ['error' => $ex->getMessage()]); + return abort(500); + } } /** @@ -46,6 +52,7 @@ public function store(Request $request) return redirect()->route('dashboard.announcements.index', $announcement)->with('Success', 'Announcement was created !'); } catch (\Exception $ex) { + Log::error('Failed to create announcement', ['error' => $ex->getMessage()]); return abort(500); } } @@ -58,9 +65,14 @@ public function store(Request $request) */ public function edit(Announcement $announcement) { - $areas = Announcement::areas(); - $types = Announcement::types(); - return view('backend.announcements.edit', compact('announcement', 'areas', 'types')); + try { + $areas = Announcement::areas(); + $types = Announcement::types(); + return view('backend.announcements.edit', compact('announcement', 'areas', 'types')); + } catch (\Exception $ex) { + Log::error('Failed to load announcement edit page', ['announcement_id' => $announcement->id, 'error' => $ex->getMessage()]); + return abort(500); + } } /** @@ -71,7 +83,7 @@ public function edit(Announcement $announcement) * @return \Illuminate\Http\RedirectResponse */ public function update(Request $request, Announcement $announcement) - { + { $data = request()->validate([ 'area' => ['required', Rule::in(array_keys(Announcement::areas()))], 'type' => ['required', Rule::in(array_keys(Announcement::types()))], @@ -86,6 +98,7 @@ public function update(Request $request, Announcement $announcement) $announcement->update($data); return redirect()->route('dashboard.announcements.index')->with('Success', 'Announcement was updated !'); } catch (\Exception $ex) { + Log::error('Failed to update announcement', ['announcement_id' => $announcement->id, 'error' => $ex->getMessage()]); return abort(500); } } @@ -114,6 +127,7 @@ public function destroy(Announcement $announcement) $announcement->delete(); return redirect()->route('dashboard.announcements.index')->with('Success', 'Announcement was deleted !'); } catch (\Exception $ex) { + Log::error('Failed to delete announcement', ['announcement_id' => $announcement->id, 'error' => $ex->getMessage()]); return abort(500); } } diff --git a/app/Http/Controllers/Backend/CourseController.php b/app/Http/Controllers/Backend/CourseController.php new file mode 100644 index 0000000..c06c69d --- /dev/null +++ b/app/Http/Controllers/Backend/CourseController.php @@ -0,0 +1,149 @@ +getMessage()); + return abort(500); + } + } + + /** + * Show the form for creating a new course. + * + * @return \Illuminate\Http\Response + */ + public function create() + { + try { + return view('backend.courses.create'); + } catch (\Exception $e) { + Log::error('Error loading course creation page: ' . $e->getMessage()); + return abort(500); + } + } + + /** + * Store a newly created course in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + */ + public function store(Request $request) + { + $validatedData = $request->validate([ + 'code' => 'required|string|max:16|unique:courses,code', + 'semester_id' => 'required|integer|exists:semesters,id', + 'academic_program' => ['required', Rule::in(array_values(Course::getAcademicPrograms()))], + 'version' => ['required', 'integer', Rule::in(array_keys(Course::getVersions()))], + 'name' => 'required|string|max:255', + 'credits' => 'required|integer', + 'type' => ['required', Rule::in(array_keys(Course::getTypes()))], + 'content' => 'nullable|string', + 'objectives' => 'nullable|json', + 'time_allocation' => 'nullable|json', + 'marks_allocation' => 'nullable|json', + 'ilos' => 'nullable|json', + 'urls' => 'nullable|json', + 'references' => 'nullable|json', + ]); + + try { + $course = Course::create($validatedData); + return redirect()->route('dashboard.courses.index')->with('success', 'Course created successfully.'); + } catch (\Exception $e) { + Log::error('Error creating course: ' . $e->getMessage()); + return abort(500); + } + } + + /** + * Show the form for editing the specified course. + * + * @param \App\Models\Course $course + * @return \Illuminate\Http\Response + */ + public function edit(Course $course) + { + try { + return view('backend.courses.edit', compact('course')); + } catch (\Exception $e) { + Log::error('Error loading course edit page: ' . $e->getMessage()); + return abort(500); + } + } + /** + * Update the specified course in storage. + * + * @param \Illuminate\Http\Request $request + * @param \App\Models\Course $course + * @return \Illuminate\Http\Response + */ + public function update(Request $request, Course $course) + { + $validatedData = $request->validate([ + 'code' => 'required|string|max:16|unique:courses,code,' . $course->id, + 'semester_id' => 'required|integer|exists:semesters,id', + 'academic_program' => ['required', Rule::in(array_values(Course::getAcademicPrograms()))], + 'version' => ['required', 'integer', Rule::in(array_keys(Course::getVersions()))], + 'name' => 'required|string|max:255', + 'credits' => 'required|integer', + 'type' => ['required', Rule::in(array_values(Course::getTypes()))], + 'type' => ['required', Rule::in(array_values(Course::getTypes()))], + 'content' => 'nullable|string', + 'objectives' => 'nullable|json', + 'time_allocation' => 'nullable|json', + 'marks_allocation' => 'nullable|json', + 'ilos' => 'nullable|json', + 'urls' => 'nullable|json', + 'references' => 'nullable|json', + ]); + try { + $course->update($validatedData); + return redirect()->route('dashboard.courses.index')->with('success', 'Course updated successfully.'); + } catch (\Exception $e) { + Log::error('Error updating course: ' . $e->getMessage()); + return abort(500); + } + } + + /** + * Remove the specified course from storage. + * + * @param \App\Models\Course $course + * @return \Illuminate\Http\Response + */ + public function delete(Course $course) + { + return view('backend.courses.delete', compact('course')); + } + + public function destroy(Course $course) + { + try { + $course->delete(); + return redirect()->route('dashboard.courses.index')->with('success', 'Course deleted successfully.'); + } catch (\Exception $e) { + Log::error('Error in deleting course: ' . $e->getMessage()); + return abort(500); + } + } +} diff --git a/app/Http/Controllers/Backend/DashboardController.php b/app/Http/Controllers/Backend/DashboardController.php index 3d00896..ad63d4f 100644 --- a/app/Http/Controllers/Backend/DashboardController.php +++ b/app/Http/Controllers/Backend/DashboardController.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers\Backend; +use Illuminate\Support\Facades\Log; + /** * Class DashboardController. */ @@ -12,6 +14,11 @@ class DashboardController */ public function index() { - return view('backend.dashboard'); + try{ + return view('backend.dashboard'); + }catch (\Exception $ex) { + Log::error('Failed to load dashboard', ['error' => $ex->getMessage()]); + return abort(500); + } } } diff --git a/app/Http/Controllers/Backend/EventController.php b/app/Http/Controllers/Backend/EventController.php index 54f2e0d..314ec10 100644 --- a/app/Http/Controllers/Backend/EventController.php +++ b/app/Http/Controllers/Backend/EventController.php @@ -20,7 +20,12 @@ class EventController extends Controller */ public function create() { - return view('backend.event.create'); + try { + return view('backend.event.create'); + } catch (\Exception $ex) { + Log::error('Failed to load event creation page', ['error' => $ex->getMessage()]); + return abort(500); + } } /** @@ -31,6 +36,7 @@ public function create() */ public function store(Request $request) { + $data = request()->validate([ 'title' => 'string|required', 'url' => ['required', 'unique:events'], @@ -43,6 +49,7 @@ public function store(Request $request) 'end_at' => 'nullable|date_format:Y-m-d\\TH:i', 'location' => 'string|required', ]); + if ($request->hasFile('image')) { $data['image'] = $this->uploadThumb(null, $request->image, "events"); } @@ -56,11 +63,10 @@ public function store(Request $request) return redirect()->route('dashboard.event.index', $event)->with('Success', 'Event was created !'); } catch (\Exception $ex) { - Log::error($ex->getMessage()); + Log::error('Failed to create event', ['error' => $ex->getMessage()]); return abort(500); } } - /** * Show the form for editing the specified resource. * @@ -69,7 +75,12 @@ public function store(Request $request) */ public function edit(Event $event) { - return view('backend.event.edit', compact('event')); + try { + return view('backend.event.edit', compact('event')); + } catch (\Exception $ex) { + Log::error('Failed to edit event', ['error' => $ex->getMessage()]); + return abort(500); + }; } /** @@ -81,12 +92,12 @@ public function edit(Event $event) */ public function update(Request $request, Event $event) { + $data = request()->validate([ 'title' => ['required'], - 'url' => - ['required', Rule::unique('news')->ignore($event->id)], + 'url' => ['required', Rule::unique('events')->ignore($event->id)], 'published_at' => 'required|date_format:Y-m-d', - 'description' => 'string', + 'description' => 'string|required', 'enabled' => 'nullable', 'link_url' => 'nullable|url', 'link_caption' => 'nullable|string', @@ -94,6 +105,8 @@ public function update(Request $request, Event $event) 'end_at' => 'nullable|date_format:Y-m-d\\TH:i', 'location' => 'string|required', ]); + + if ($request->hasFile('image')) { $data['image'] = $this->uploadThumb($event->image, $request->image, "events"); } else { @@ -109,10 +122,12 @@ public function update(Request $request, Event $event) return redirect()->route('dashboard.event.index')->with('Success', 'Event was updated !'); } catch (\Exception $ex) { + Log::error('Failed to update event', ['event_id' => $event->id, 'error' => $ex->getMessage()]); return abort(500); } } + /** * Confirm to delete the specified resource from storage. * @@ -136,23 +151,21 @@ public function destroy(Event $event) $event->delete(); return redirect()->route('dashboard.event.index')->with('Success', 'Event was deleted !'); } catch (\Exception $ex) { + Log::error('Failed to delete event', ['event_id' => $event->id, 'error' => $ex->getMessage()]); return abort(500); } } - // Private function to handle deleting images private function deleteThumb($currentURL) { if ($currentURL != null) { $oldImage = public_path($currentURL); - if (File::exists($oldImage)) unlink($oldImage); + if (File::exists($oldImage)) unlink($oldImage); } } - // Private function to handle uploading images private function uploadThumb($currentURL, $newImage, $folder) { - // Delete the existing image $this->deleteThumb($currentURL); $imageName = time() . '.' . $newImage->extension(); @@ -163,4 +176,4 @@ private function uploadThumb($currentURL, $newImage, $folder) return $imageName; } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Backend/NewsController.php b/app/Http/Controllers/Backend/NewsController.php index 76f4501..6b6db16 100644 --- a/app/Http/Controllers/Backend/NewsController.php +++ b/app/Http/Controllers/Backend/NewsController.php @@ -9,10 +9,10 @@ use Intervention\Image\Facades\Image; use Illuminate\Support\Facades\File; use Illuminate\Validation\Rule; +use Illuminate\Support\Facades\Log; class NewsController extends Controller { - /** * Show the form for creating a new resource. * @@ -20,9 +20,13 @@ class NewsController extends Controller */ public function create() { - return view('backend.news.create'); + try{ + return view('backend.news.create'); + }catch (\Exception $ex) { + Log::error('Failed to load news creation page', ['error' => $ex->getMessage()]); + return abort(500); + } } - /** * Store a newly created resource in storage. * @@ -41,6 +45,7 @@ public function store(Request $request) 'link_url' => 'nullable|url', 'link_caption' => 'nullable|string', ]); + if ($request->hasFile('image')) { $data['image'] = $this->uploadThumb(null, $request->image, "news"); } @@ -54,10 +59,10 @@ public function store(Request $request) return redirect()->route('dashboard.news.index', $news)->with('Success', 'News was created !'); } catch (\Exception $ex) { + Log::error('Failed to create news', ['error' => $ex->getMessage()]); return abort(500); } } - /** * Show the form for editing the specified resource. * @@ -66,9 +71,13 @@ public function store(Request $request) */ public function edit(News $news) { - return view('backend.news.edit', compact('news')); + try{ + return view('backend.news.edit', ['news' => $news]); + }catch (\Exception $ex) { + Log::error('Failed to load news edit page', ['error' => $ex->getMessage()]); + return abort(500); + } } - /** * Update the specified resource in storage. * @@ -78,6 +87,7 @@ public function edit(News $news) */ public function update(Request $request, News $news) { + $data = request()->validate([ 'title' => ['required'], 'url' => ['required', Rule::unique('news')->ignore($news->id)], @@ -87,6 +97,7 @@ public function update(Request $request, News $news) 'link_url' => 'nullable|url', 'link_caption' => 'nullable|string', ]); + if ($request->hasFile('image')) { $data['image'] = $this->uploadThumb($news->image, $request->image, "news"); } else { @@ -102,11 +113,11 @@ public function update(Request $request, News $news) return redirect()->route('dashboard.news.index')->with('Success', 'News was updated !'); } catch (\Exception $ex) { + Log::error('Failed to update news', ['news_id' => $news->id, 'error' => $ex->getMessage()]); return abort(500); } } - - /** + /** * Confirm to delete the specified resource from storage. * * @param \App\Models\News $news @@ -132,31 +143,34 @@ public function destroy(News $news) $news->delete(); return redirect()->route('dashboard.news.index')->with('Success', 'News was deleted !'); } catch (\Exception $ex) { + Log::error('Failed to delete news', ['news_id' => $news->id, 'error' => $ex->getMessage()]); return abort(500); } } + // Private function to handle deleting images + private function deleteThumb($currentURL) + { + if ($currentURL != null && $currentURL != config('constants.frontend.dummy_thumb')) { + $oldImage = public_path($currentURL); + if (File::exists($oldImage)) { + unlink($oldImage); + } + } + } + + // Private function to handle uploading images + private function uploadThumb($currentURL, $newImage, $folder) + { + // Delete the existing image + $this->deleteThumb($currentURL); + + $imageName = time() . '.' . $newImage->extension(); + $newImage->move(public_path('img/' . $folder), $imageName); + $imagePath = "/img/$folder/" . $imageName; + $image = Image::make(public_path($imagePath)); + $image->save(); + + return $imageName; + } +} - // Private function to handle deleting images - private function deleteThumb($currentURL) - { - if ($currentURL != null && $currentURL != config('constants.frontend.dummy_thumb')) { - $oldImage = public_path($currentURL); - if (File::exists($oldImage)) unlink($oldImage); - } - } - - // Private function to handle uploading images - private function uploadThumb($currentURL, $newImage, $folder) - { - // Delete the existing image - $this->deleteThumb($currentURL); - - $imageName = time() . '.' . $newImage->extension(); - $newImage->move(public_path('img/' . $folder), $imageName); - $imagePath = "/img/$folder/" . $imageName; - $image = Image::make(public_path($imagePath)); - $image->save(); - - return $imageName; - } -} \ No newline at end of file diff --git a/app/Http/Controllers/Backend/SemesterController.php b/app/Http/Controllers/Backend/SemesterController.php new file mode 100644 index 0000000..9b76503 --- /dev/null +++ b/app/Http/Controllers/Backend/SemesterController.php @@ -0,0 +1,160 @@ +getMessage()); + return abort(500); + } + } + + /** + * Show the form for creating a new semester. + * + * @return \Illuminate\Http\Response + */ + public function create() + { + try { + return view('backend.semesters.create'); + } catch (\Exception $e) { + Log::error('Error loading semester creation page: ' . $e->getMessage()); + return abort(500); + } + } + + /** + * Store a newly created semester in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + */ + public function store(Request $request) + { + $validatedData = $request->validate([ + 'title' => 'required|string|max:255', + 'version' => ['required', 'integer', Rule::in(array_keys(Semester::getVersions()))], + 'academic_program' => ['required', Rule::in(array_keys(Semester::getAcademicPrograms()))], + 'description' => 'nullable|string', + 'url' => [ + 'required', + 'string', + 'unique:semesters', + ], + ]); + + try { + $semester = new Semester($validatedData); + $semester->created_by = Auth::user()->id; + $semester->updated_by = Auth::user()->id; + $semester->url = urlencode(str_replace(" ", "-", $request->url)); + $semester->save(); + + return redirect()->route('dashboard.semesters.index')->with('success', 'Semester created successfully.'); + } catch (\Exception $e) { + Log::error('Error in storing semester: ' . $e->getMessage()); + return abort(500); + } + } + + /** + * Show the form for editing the specified semester. + * + * @param \App\Domains\AcademicProgram\Semester\Models\Semester $semester + * @return \Illuminate\Http\Response + */ + public function edit(Semester $semester) + { + try { + return view('backend.semesters.edit', compact('semester')); + } catch (\Exception $e) { + Log::error('Error loading semester edit page: ' . $e->getMessage()); + return abort(500); + } + } + + /** + * Update the specified semester in storage. + * + * @param \Illuminate\Http\Request $request + * @param \App\Domains\AcademicProgram\Semester\Models\Semester $semester + * @return \Illuminate\Http\Response + */ + public function update(Request $request, Semester $semester) + { + $validatedData = $request->validate([ + 'title' => 'required|string|max:255', + 'version' => ['required', 'integer', Rule::in(array_keys(Semester::getVersions()))], + 'academic_program' => ['required', Rule::in(array_keys(Semester::getAcademicPrograms()))], + 'description' => 'nullable|string', + 'url' => [ + 'required', + 'string', + Rule::unique('semesters', 'url')->ignore($semester->id), + ], + ]); + + try { + $semester->update($validatedData); + $semester->updated_by = Auth::user()->id; + $semester->url = urlencode(str_replace(" ", "-", $request->url)); + $semester->save(); + + return redirect()->route('dashboard.semesters.index')->with('success', 'Semester updated successfully.'); + } catch (\Exception $e) { + Log::error('Error in updating semester: ' . $e->getMessage()); + return abort(500); + } + } + + /** + * Remove the specified semester from storage. + * + * @param \App\Domains\AcademicProgram\Semester\Models\Semester $semester + * @return \Illuminate\Http\Response + */ + public function delete(Semester $semester) + { + $courses = Course::where('semester_id', $semester->id)->get(); + return view('backend.semesters.delete', compact('semester', 'courses')); + } + + + public function destroy(Semester $semester) + { + $courses = Course::where('semester_id', $semester->id)->get(); + + if ($courses->count() > 0) { + return redirect()->route('dashboard.semesters.index') + ->withErrors('Can not delete the semester as it already has associated courses. Please reassign or delete those courses first.'); + } + + try { + $semester->delete(); + return redirect()->route('dashboard.semesters.index')->with('success', 'Semester deleted successfully.'); + } catch (\Exception $e) { + Log::error('Error in deleting semester: ' . $e->getMessage()); + return abort(500); + } + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 43b5aff..081a7e1 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -79,7 +79,7 @@ class Kernel extends HttpKernel 'is_admin' => \App\Domains\Auth\Http\Middleware\AdminCheck::class, 'is_super_admin' => \App\Domains\Auth\Http\Middleware\SuperAdminCheck::class, 'is_user' => \App\Domains\Auth\Http\Middleware\UserCheck::class, - 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, + 'password.confirm' => \App\Http\Middleware\CustomRequirePassword::class, // \Illuminate\Auth\Middleware\RequirePassword::class, 'password.expires' => \App\Domains\Auth\Http\Middleware\PasswordExpires::class, 'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class, 'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class, @@ -105,4 +105,4 @@ class Kernel extends HttpKernel \Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Auth\Middleware\Authorize::class, ]; -} +} \ No newline at end of file diff --git a/app/Http/Livewire/Backend/CourseTable.php b/app/Http/Livewire/Backend/CourseTable.php new file mode 100644 index 0000000..2188eff --- /dev/null +++ b/app/Http/Livewire/Backend/CourseTable.php @@ -0,0 +1,83 @@ +searchable()->sortable(), + Column::make("Name", "name") + ->searchable()->sortable(), + Column::make("Semester", "semester") + ->searchable(), + Column::make("Academic Program", "academic_program") + ->sortable(), + Column::make("Type", "type") + ->sortable(), + Column::make("Curriculum", "version") + ->sortable(), + Column::make("Credits", "credits") + ->searchable(), + Column::make("Updated by", "created_by") + ->sortable(), + Column::make("Updated At", "updated_at") + ->sortable(), + Column::make("Actions") + ]; + } + + public function query(): Builder + { + return Course::query() + ->when($this->getFilter('academic_program'), fn($query, $type) => $query->where('academic_program', $type)) + ->when($this->getFilter('semester_id'), fn($query, $type) => $query->where('semester_id', $type)) + ->when($this->getFilter('version'), fn($query, $version) => $query->where('version', $version));; + } + + public function filters(): array + { + $academicProgramOptions = ["" => "Any"]; + foreach (Course::getAcademicPrograms() as $key => $value) { + $academicProgramOptions[$key] = $value; + } + + $typeOptions = ["" => "Any"]; + foreach (Course::getTypes() as $key => $value) { + $typeOptions[$key] = $value; + } + + $versionOptions = ["" => "Any"]; + foreach (Course::getVersions() as $key => $value) { + $versionOptions[$key] = $value; + } + + return [ + 'academic_program' => Filter::make('Academic Program') + ->select($academicProgramOptions), + 'type' => Filter::make('Type') + ->select($typeOptions), + 'version' => Filter::make('Curriculum') + ->select($versionOptions), + ]; + } + + public function rowView(): string + { + return 'backend.courses.index-table-row'; + } +} \ No newline at end of file diff --git a/app/Http/Livewire/Backend/CreateCourses.php b/app/Http/Livewire/Backend/CreateCourses.php new file mode 100644 index 0000000..a2c18f7 --- /dev/null +++ b/app/Http/Livewire/Backend/CreateCourses.php @@ -0,0 +1,304 @@ + 'required|string', + 'semester' => 'required|string', + 'version' => ['required', 'string', Rule::in(array_keys(Course::getVersions()))], + 'type' => ['required', 'string', Rule::in(array_keys(Course::getTypes()))], + 'code' => 'required|string|unique:courses,code', + 'name' => 'required|string|max:255', + 'credits' => 'required|integer|min:1|max:18', + 'faq_page' => 'nullable|url', + 'content' => 'nullable|string', + 'time_allocation.lecture' => 'nullable|integer|min:0', + 'time_allocation.tutorial' => 'nullable|integer|min:0', + 'time_allocation.practical' => 'nullable|integer|min:0', + 'time_allocation.assignment' => 'nullable|integer|min:0', + 'marks_allocation.practicals' => 'nullable|integer|min:0|max:100', + 'marks_allocation.project' => 'nullable|integer|min:0|max:100', + 'marks_allocation.mid_exam' => 'nullable|integer|min:0|max:100', + 'marks_allocation.end_exam' => 'nullable|integer|min:0|max:100', + 'modules' => 'nullable|array', + 'modules.*.name' => 'required|string|max:255', + 'modules.*.description' => 'nullable|string', + 'modules.*.time_allocation.lectures' => 'nullable|integer|min:0', + 'modules.*.time_allocation.tutorials' => 'nullable|integer|min:0', + 'modules.*.time_allocation.practicals' => 'nullable|integer|min:0', + 'modules.*.time_allocation.assignments' => 'nullable|integer|min:0', + ]; + } + + public function messages() + { + return [ + 'academicProgram.required' => 'Please select an academic program.', + 'semester.required' => 'Please select a semester.', + 'version.required' => 'Please provide a curriculum.', + 'type.required' => 'Please select a course type.', + 'type.in' => 'The course type must be Core, GE, or TE.', + 'code.required' => 'Please provide a course code.', + 'code.unique' => 'This course code is already in use.', + 'name.required' => 'Please provide a course name.', + 'credits.required' => 'Please specify the number of credits.', + 'credits.min' => 'The course must have at least 1 credit.', + 'credits.max' => 'The course cannot have more than 18 credits.', + 'modules.*.name.required' => 'Each module must have a name.', + 'modules.*.description.required' => 'Each module must have a description.', + 'modules.*.description.min' => 'Module descriptions should be at least 10 characters long.', + ]; + } + + protected function validateCurrentStep() + { + switch ($this->formStep) { + case 1: + $validationRules = [ + 'academicProgram' => 'required|string', + 'semester' => 'required|string', + 'version' => ['required', 'string', Rule::in(array_keys(Course::getVersions()))], + 'type' => ['required', 'string', Rule::in(array_keys(Course::getTypes()))], + 'code' => 'required|string|unique:courses,code', + 'name' => 'required|string|max:255', + 'credits' => 'required|integer|min:1|max:18', + 'faq_page' => 'nullable|url', + 'content' => 'nullable|string', + ]; + + foreach (Course::getTimeAllocation() as $key => $value) { + $validationRules["time_allocation.$key"] = 'nullable|integer|min:0'; + } + foreach (Course::getMarksAllocation() as $key => $value) { + $validationRules["marks_allocation.$key"] = 'nullable|integer|min:0|max:100'; + } + + $this->validate($validationRules); + $this->validateMarksAllocation(); + if ($this->getErrorBag()->has('marks_allocation.total')) { + return; + } + break; + + case 3: + $validationRules = [ + 'modules' => 'nullable|array', + 'modules.*.name' => 'required|string|min:3|max:255', + 'modules.*.description' => 'required|string', + ]; + + foreach (Course::getTimeAllocation() as $key => $value) { + $validationRules["modules.*.time_allocation.$key"] = 'nullable|integer|min:0'; + } + + $this->validate($validationRules); + break; + } + } + + protected function validateMarksAllocation() + { + $totalMarks = 0; + $hasValue = false; + + foreach ($this->marks_allocation as $key => $value) { + if (!empty($value)) { + $hasValue = true; + $totalMarks += (int) $value; + } + } + + if ($hasValue && $totalMarks != 100) { + $this->addError('marks_allocation.total', 'The total of marks allocation must be 100.'); + } + } + + public function updated($propertyName) + { + $this->validateOnly($propertyName); + } + + protected $listeners = ['itemsUpdated' => 'updateItems']; + + public function mount() + { + $this->academicProgramsList = Course::getAcademicPrograms(); + $this->time_allocation = Course::getTimeAllocation(); + $this->marks_allocation = Course::getMarksAllocation(); + $this->module_time_allocation = Course::getTimeAllocation(); + $this->ilos = Course::getILOTemplate(); + } + + public function updateItems($type, $newItems) + { + if ($type == 'references') { + $this->$type = $newItems; + } else { + $this->ilos[$type] = $newItems; + } + } + + public function next() + { + $this->validateCurrentStep(); + if ($this->getErrorBag()->has('marks_allocation.total')) { + return; // Do not proceed to the next step if the marks total is invalid + } + $this->formStep++; + } + + public function previous() + { + $this->formStep--; + } + + public function submit() + { + \Log::info("Submit method called"); + try { + $this->validate(); + $this->storeCourse(); + return redirect()->route('dashboard.courses.index')->with('Success', 'Course created successfully.'); + } catch (\Exception $e) { + \Log::error("Error in submit method: " . $e->getMessage()); + session()->flash('error', 'There was an error creating the course: ' . $e->getMessage()); + } + $this->resetForm(); + } + + public function updatedAcademicProgram() + { + $this->updateSemestersList(); + } + + public function updatedVersion() + { + $this->updateSemestersList(); + } + + public function updateSemestersList() + { + if ($this->academicProgram && $this->version) { + $this->semestersList = Semester::where('academic_program', $this->academicProgram) + ->where('version', $this->version) + ->pluck('title', 'id') + ->toArray(); + } else { + $this->semestersList = []; + } + } + + + protected function storeCourse() + { + try { + \DB::beginTransaction(); + $course = Course::create([ + 'academic_program' => $this->academicProgram, + 'semester_id' => (int)$this->semester, + 'version' => (int)$this->version, + 'type' => $this->type, + 'code' => $this->code, + 'name' => $this->name, + 'credits' => (int)$this->credits, + 'faq_page' => $this->faq_page, + 'content' => $this->content, + 'time_allocation' => json_encode($this->time_allocation), + 'marks_allocation' => json_encode($this->marks_allocation), + 'objectives' => $this->objectives, + 'ilos' => json_encode($this->ilos), + 'references' => json_encode($this->references), + 'created_by' => auth()->id(), + 'updated_by' => auth()->id() + ]); + + if (empty($this->modules)) { + \Log::warning("No modules to create"); + } else { + foreach ($this->modules as $module) { + CourseModule::create([ + 'course_id' => $course->id, + 'topic' => $module['name'], + 'description' => $module['description'], + 'time_allocation' => json_encode($module['time_allocation']), + 'created_by' => auth()->id(), + 'updated_by' => auth()->id(), + ]); + } + } + + \DB::commit(); + } catch (\Exception $e) { + \DB::rollBack(); + \Log::error("Error in storeCourse method: " . $e->getMessage()); + throw $e; + } + } + + + protected function resetForm() + { + $this->formStep = 1; + $this->academicProgram = ''; + $this->semester = ''; + $this->version = ''; + $this->type = ''; + $this->code = ''; + $this->name = ''; + $this->credits = 0; + $this->faq_page = ''; + $this->content = ''; + $this->time_allocation = Course::getTimeAllocation(); + $this->marks_allocation = Course::getMarksAllocation(); + $this->module_time_allocation = Course::getTimeAllocation(); + $this->objectives = ''; + $this->ilos = Course::getILOTemplate(); + $this->references = []; + $this->modules = []; + } + + public function render() + { + return view('livewire.backend.create-courses'); + } +} \ No newline at end of file diff --git a/app/Http/Livewire/Backend/EditCourses.php b/app/Http/Livewire/Backend/EditCourses.php new file mode 100644 index 0000000..ab54e3e --- /dev/null +++ b/app/Http/Livewire/Backend/EditCourses.php @@ -0,0 +1,328 @@ + [], + 'skills' => [], + 'attitudes' => [], + ]; + + // 3rd form step + public $references = []; + public $modules = []; + + public function rules() + { + + $validationRules = [ + 'academicProgram' => 'required|string', + 'semester' => 'required|int', + 'version' => ['required', 'string', Rule::in(array_keys(Course::getVersions()))], + 'type' => ['required', 'string', Rule::in(array_keys(Course::getTypes()))], + 'code' => 'required|string', + 'name' => 'required|string|max:255', + 'credits' => 'required|integer|min:1|max:18', + 'faq_page' => 'nullable|url', + 'content' => 'nullable|string', + 'modules' => 'nullable|array', + 'modules.*.name' => 'required|string|max:255', + 'modules.*.description' => 'nullable|string', + 'modules.*.time_allocation.lectures' => 'nullable|integer|min:0', + 'modules.*.time_allocation.tutorials' => 'nullable|integer|min:0', + 'modules.*.time_allocation.practicals' => 'nullable|integer|min:0', + 'modules.*.time_allocation.assignments' => 'nullable|integer|min:0', + ]; + + foreach (Course::getTimeAllocation() as $key => $value) { + $validationRules["time_allocation.$key"] = 'nullable|integer|min:0'; + $validationRules["modules.*.time_allocation.$key"] = 'nullable|integer|min:0'; + } + foreach (Course::getMarksAllocation() as $key => $value) { + $validationRules["marks_allocation.$key"] = 'nullable|integer|min:0|max:100'; + } + + return $validationRules; + } + + public function messages() + { + return [ + 'academicProgram.required' => 'Please select an academic program.', + 'semester.required' => 'Please select a semester.', + 'version.required' => 'Please provide a curriculum.', + 'type.required' => 'Please select a course type.', + 'type.in' => 'The course type must be Core, GE, or TE.', + 'code.required' => 'Please provide a course code.', + 'name.required' => 'Please provide a course name.', + 'credits.required' => 'Please specify the number of credits.', + 'credits.min' => 'The course must have at least 1 credit.', + 'credits.max' => 'The course cannot have more than 18 credits.', + 'modules.*.name.required' => 'Each module must have a name.', + 'modules.*.description.required' => 'Each module must have a description.', + 'modules.*.description.min' => 'Module descriptions should be at least 10 characters long.', + ]; + } + + protected function validateCurrentStep() + { + switch ($this->formStep) { + case 1: + $this->validate($this->rules()); + $this->validateMarksAllocation(); + break; + + case 3: + $this->validate([ + 'modules' => 'nullable|array', + 'modules.*.name' => 'nullable|string|max:255', + 'modules.*.description' => 'nullable|string', + 'modules.*.time_allocation.lectures' => 'nullable|integer|min:0', + 'modules.*.time_allocation.tutorials' => 'nullable|integer|min:0', + 'modules.*.time_allocation.practicals' => 'nullable|integer|min:0', + 'modules.*.time_allocation.assignments' => 'nullable|integer|min:0', + ]); + break; + } + } + + protected function validateMarksAllocation() + { + $totalMarks = 0; + $hasValue = false; + + foreach ($this->marks_allocation as $key => $value) { + if (!empty($value)) { + $hasValue = true; + $totalMarks += (int) $value; + } + } + + if ($hasValue && $totalMarks != 100) { + $this->addError('marks_allocation.total', 'The total of marks allocation must be 100.'); + } + } + + public function updated($propertyName) + { + $this->canUpdate = false; + $this->validateCurrentStep(); + if ($this->getErrorBag()->has('marks_allocation.total')) { + return; + } + $this->canUpdate = true; + } + + protected $listeners = ['itemsUpdated' => 'updateItems']; + + public function mount(Course $course) + { + $this->academicProgramsList = Course::getAcademicPrograms(); + $this->time_allocation = Course::getTimeAllocation(); + $this->module_time_allocation = Course::getTimeAllocation(); + $this->marks_allocation = Course::getMarksAllocation(); + $this->course = $course; + + // Populate form fields with existing course data + $this->academicProgram = $course->academic_program; + $this->semester = $course->semester_id; + $this->version = $course->version; + $this->type = $course->type; + $this->code = $course->code; + $this->name = $course->name; + $this->credits = $course->credits; + $this->faq_page = $course->faq_page; + $this->content = $course->content; + $this->time_allocation = array_merge(Course::getTimeAllocation(), json_decode($course->time_allocation, true)); + $this->marks_allocation = array_merge(Course::getMarksAllocation(), json_decode($course->marks_allocation, true)); + $this->objectives = $course->objectives; + $this->ilos = array_merge(Course::getILOTemplate(), json_decode($course->ilos, true) ?? []); + $this->references = json_decode($course->references, true) ?? []; + + // Load modules + $this->modules = $course->modules()->get()->map(function ($module, $index) { + return [ + 'id' => $index + 1, // or use $module->id if available + 'name' => $module->topic, + 'description' => $module->description, + 'time_allocation' => array_merge(Course::getTimeAllocation(), json_decode($module->time_allocation, true)) + ]; + })->toArray(); + // Update semesters list based on academic program and version + $this->updateSemestersList(); + } + + public function updateItems($type, $newItems) + { + if ($type == 'references') { + $this->$type = $newItems; + } else { + $this->ilos[$type] = $newItems; + } + + $this->emit('refreshItems' . ucfirst($type), $newItems); + } + + + public function next() + { + $this->validateCurrentStep(); + if ($this->getErrorBag()->has('marks_allocation.total')) { + return; // Do not proceed to the next step if the marks total is invalid + } + $this->formStep++; + } + + public function previous() + { + $this->formStep--; + } + + public function update() + { + \Log::info("update method called"); + try { + + $this->validateCurrentStep(); + $this->updateCourse(); + return redirect()->route('dashboard.courses.index')->with('Success', 'Course updated successfully.'); + } catch (\Exception $e) { + \Log::error("Error in update method: " . $e->getMessage()); + session()->flash('error', 'There was an error updating the course: ' . $e->getMessage()); + } + $this->resetForm(); + } + + public function updatedAcademicProgram() + { + $this->updateSemestersList(); + } + + public function updatedVersion() + { + $this->updateSemestersList(); + } + + public function updateSemestersList() + { + if ($this->academicProgram && $this->version) { + $this->semestersList = Semester::where('academic_program', $this->academicProgram) + ->where('version', $this->version) + ->pluck('title', 'id') + ->toArray(); + } else { + $this->semestersList = []; + } + } + + protected function updateCourse() + { + \Log::info("updateCourse method called"); + + try { + \DB::beginTransaction(); + + $course = Course::where('id', $this->course->id)->firstOrFail(); + + $course->update([ + 'academic_program' => $this->academicProgram, + 'semester_id' => (int)$this->semester, + 'version' => (int)$this->version, + 'type' => $this->type, + 'code' => $this->code, + 'name' => $this->name, + 'credits' => (int)$this->credits, + 'faq_page' => $this->faq_page, + 'content' => $this->content, + 'time_allocation' => json_encode($this->time_allocation), + 'marks_allocation' => json_encode($this->marks_allocation), + 'objectives' => $this->objectives, + 'ilos' => json_encode($this->ilos), + 'references' => json_encode($this->references), + 'updated_by' => auth()->id() + ]); + + \Log::info("Course updated with ID: " . $course->id); + + $course->modules()->delete(); // Delete existing modules before adding new ones + + if (!empty($this->modules)) { + foreach ($this->modules as $module) { + $createdModule = CourseModule::create([ + 'course_id' => $course->id, + 'topic' => $module['name'], + 'description' => $module['description'], + 'time_allocation' => json_encode($module['time_allocation']), + 'created_by' => auth()->id(), + 'updated_by' => auth()->id(), + ]); + \Log::info("Created module with ID: " . $createdModule->id); + } + } + + \DB::commit(); + \Log::info("updateCourse method completed successfully"); + } catch (\Exception $e) { + \DB::rollBack(); + \Log::error("Error in updateCourse method: " . $e->getMessage()); + throw $e; + } + } + + protected function resetForm() + { + $this->formStep = 1; + $this->academicProgram = ''; + $this->semester = ''; + $this->version = ''; + $this->type = ''; + $this->code = ''; + $this->name = ''; + $this->credits = null; + $this->faq_page = ''; + $this->content = ''; + $this->time_allocation = Course::getTimeAllocation(); + $this->marks_allocation = Course::getMarksAllocation(); + $this->module_time_allocation = Course::getTimeAllocation(); + $this->objectives = ''; + $this->ilos = Course::getILOTemplate(); + $this->references = []; + $this->modules = []; + } + + public function render() + { + return view('livewire.backend.edit-courses'); + } +} \ No newline at end of file diff --git a/app/Http/Livewire/Backend/EventsTable.php b/app/Http/Livewire/Backend/EventsTable.php index 70da63d..de81950 100644 --- a/app/Http/Livewire/Backend/EventsTable.php +++ b/app/Http/Livewire/Backend/EventsTable.php @@ -54,7 +54,7 @@ public function query(): Builder } elseif ($enabled === 0) { $query->where('enabled', false); } - }); + })->orderBy('published_at', 'desc'); } public function toggleEnable($eventId) { diff --git a/app/Http/Livewire/Backend/ItemAdder.php b/app/Http/Livewire/Backend/ItemAdder.php new file mode 100644 index 0000000..bea59d7 --- /dev/null +++ b/app/Http/Livewire/Backend/ItemAdder.php @@ -0,0 +1,22 @@ +items = $items; + $this->type = $type; + } + + public function render() + { + return view('livewire.backend.item-adder'); + } +} \ No newline at end of file diff --git a/app/Http/Livewire/Backend/NewsTable.php b/app/Http/Livewire/Backend/NewsTable.php index 26774fd..08a30e0 100644 --- a/app/Http/Livewire/Backend/NewsTable.php +++ b/app/Http/Livewire/Backend/NewsTable.php @@ -31,7 +31,7 @@ public function columns(): array ->format(function (News $news) { return view('backend.news.enabled-toggle', ['news' => $news]); }), - Column::make("Author") + Column::make("Author", "user.name") ->sortable() ->searchable(), Column::make("Published at", "published_at") @@ -52,7 +52,7 @@ public function query(): Builder } elseif ($enabled === 0) { $query->where('enabled', false); } - }); + })->orderBy('published_at', 'desc');; } diff --git a/app/Http/Livewire/Backend/SemesterTable.php b/app/Http/Livewire/Backend/SemesterTable.php new file mode 100644 index 0000000..4422099 --- /dev/null +++ b/app/Http/Livewire/Backend/SemesterTable.php @@ -0,0 +1,71 @@ +searchable()->sortable(), + Column::make("Curriculum", "version") + ->sortable(), + Column::make("Academic Program", "academic_program") + ->sortable(), + Column::make("Description", "description") + ->searchable(), + Column::make("URL", "url") + ->searchable(), + Column::make("Updated by", "created_by") + ->sortable(), + Column::make("Updated At", "updated_at") + ->sortable(), + Column::make("Actions") + ]; + } + + public function query(): Builder + { + return Semester::query() + ->when($this->getFilter('academic_program'), fn($query, $type) => $query->where('academic_program', $type)) + ->when($this->getFilter('version'), fn($query, $version) => $query->where('version', $version)); + } + + public function filters(): array + { + $academicProgramOptions = ["" => "Any"]; + foreach (Semester::getAcademicPrograms() as $key => $value) { + $academicProgramOptions[$key] = $value; + } + $versionOptions = ["" => "Any"]; + foreach (Semester::getVersions() as $key => $value) { + $versionOptions[$key] = $value; + } + + + return [ + 'academic_program' => Filter::make('Academic Program') + ->select($academicProgramOptions), + 'version' => Filter::make('Curriculum') + ->select($versionOptions), + ]; + } + + public function rowView(): string + { + return 'backend.semesters.index-table-row'; + } +} \ No newline at end of file diff --git a/app/Http/Middleware/CustomRequirePassword.php b/app/Http/Middleware/CustomRequirePassword.php new file mode 100644 index 0000000..05daee0 --- /dev/null +++ b/app/Http/Middleware/CustomRequirePassword.php @@ -0,0 +1,39 @@ +environment('testing')) { + return $next($request); + } + + // Should ask only if user has password = not signed in with providers + $hasPassword = $this->hasPassword($request); + if ($hasPassword) { + return parent::handle($request, $next, $redirectToRoute); + } + return $next($request); + } + + protected function hasPassword($request) + { + return (!in_array($request->user()->provider, ['google'])); + } +} \ No newline at end of file diff --git a/app/Http/Resources/CourseResource.php b/app/Http/Resources/CourseResource.php new file mode 100644 index 0000000..d29ef12 --- /dev/null +++ b/app/Http/Resources/CourseResource.php @@ -0,0 +1,39 @@ + $this->id, + 'code' => $this->code, + 'name' => $this->name, + 'description' => $this->content, + 'credits' => $this->credits, + 'type' => $this->type, + 'semester_id' => $this->semester_id, + 'academic_program' => $this->academic_program, + 'version' => $this->version, + 'objectives' => $this->objectives, + 'time_allocation' => $this->time_allocation, + 'marks_allocation' => $this->marks_allocation, + 'ilos' => $this->ilos, + 'references' => $this->references, + 'created_by' => User::find($this->created_by)?->name, + 'updated_by' => User::find($this->updated_by)?->name, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} \ No newline at end of file diff --git a/app/Http/Resources/SemesterResource.php b/app/Http/Resources/SemesterResource.php new file mode 100644 index 0000000..a5c7360 --- /dev/null +++ b/app/Http/Resources/SemesterResource.php @@ -0,0 +1,31 @@ + $this->id, + 'title' => $this->title, + 'version' => $this->version, + 'academic_program' => $this->academic_program, + 'description' => $this->description, + 'url' => $this->url, + 'created_by' => User::find($this->created_by)?->name, + 'updated_by' => User::find($this->updated_by)?->name, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 02922ef..1c87405 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -30,5 +30,6 @@ public function boot() { Paginator::useBootstrap(); Schema::defaultStringLength(191); + ini_set('max_execution_time', 120); } } diff --git a/app/Rules/ValidateAsInternalEmail.php b/app/Rules/ValidateAsInternalEmail.php new file mode 100644 index 0000000..a8a8aad --- /dev/null +++ b/app/Rules/ValidateAsInternalEmail.php @@ -0,0 +1,44 @@ +environment() == 'testing') return true; + $api = new DepartmentDataService(); + return $api->isInternalEmail($value); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return "Only Department of Computer Engineering students/staff are allowed to register by themselves."; + } +} \ No newline at end of file diff --git a/app/Services/DepartmentDataService.php b/app/Services/DepartmentDataService.php new file mode 100644 index 0000000..70c91a5 --- /dev/null +++ b/app/Services/DepartmentDataService.php @@ -0,0 +1,64 @@ +getData('/people/v1/students/all/'); + $student_emails = collect($students)->map(function ($user) { + $faculty_name = $user['emails']['faculty']['name']; + $faculty_domain = $user['emails']['faculty']['domain']; + + $personal_name = $user['emails']['faculty']['name']; + $personal_domain = $user['emails']['faculty']['domain']; + + if ($faculty_domain == 'eng.pdn.ac.lk' && $faculty_name != '' && $faculty_domain != '') { + // Faculty Email + return "$faculty_name@$faculty_domain"; + } else if ($personal_domain == 'eng.pdn.ac.lk') { + // Personal Email + return "$personal_name@$personal_domain"; + } + return null; + }); + + // Staff + $staff = $this->getData('/people/v1/staff/all/'); + $staff_emails = collect($staff)->map(function ($user) { + return $user['email']; + }); + + return $student_emails->union($staff_emails)->filter()->values()->toArray(); + } + ); + return in_array($userEmail, $emails); + } + + private function getData($endpoint) + { + $url = config('constants.department_data.base_url') . $endpoint; + $response = Http::get($url); + + if ($response->successful()) { + return $response->json(); + } else { + $statusCode = $response->status(); + $errorMessage = $response->body(); + + Log::error('Error in getData: ' . $errorMessage); + return []; + } + } +} \ No newline at end of file diff --git a/bootstrap/cache/.gitignore b/bootstrap/cache/.gitignore old mode 100755 new mode 100644 diff --git a/composer.json b/composer.json index c994433..03347d7 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "laravel/ui": "^3.0", "laravelcollective/html": "^6.4", "livewire/livewire": "^2.0", + "marvinlabs/laravel-discord-logger": "^1.4", "rappasoft/laravel-livewire-tables": "^1.0", "rappasoft/lockout": "^3.0", "spatie/laravel-activitylog": "^3.14", diff --git a/config/app.php b/config/app.php index d572e37..1c12734 100644 --- a/config/app.php +++ b/config/app.php @@ -179,7 +179,8 @@ App\Providers\LocaleServiceProvider::class, App\Providers\ObserverServiceProvider::class, App\Providers\RouteServiceProvider::class, - + + MarvinLabs\DiscordLogger\ServiceProvider::class ], /* diff --git a/config/boilerplate.php b/config/boilerplate.php index baff4ef..18ac83c 100644 --- a/config/boilerplate.php +++ b/config/boilerplate.php @@ -162,4 +162,4 @@ | */ 'testing' => env('APP_TESTING', false), -]; +]; \ No newline at end of file diff --git a/config/constants.php b/config/constants.php index c362453..067dc95 100644 --- a/config/constants.php +++ b/config/constants.php @@ -4,5 +4,9 @@ 'frontend' => [ 'dummy_thumb' => '/dummy/item_thumbnail.jpg', ], - 'backend' => [] -]; + 'backend' => [], + 'department_data' => [ + 'base_url' => 'https://api.ce.pdn.ac.lk', + 'cache_duration' => 43200 // 6 hours + ] +]; \ No newline at end of file diff --git a/config/discord-logger.php b/config/discord-logger.php new file mode 100644 index 0000000..da2cef5 --- /dev/null +++ b/config/discord-logger.php @@ -0,0 +1,61 @@ + [ + 'name' => env('APP_NAME', 'Discord Logger'), + 'avatar_url' => null, + ], + + /** + * The converter to use to turn a log record into a discord message + * + * Bundled converters: + * - \MarvinLabs\DiscordLogger\Converters\SimpleRecordConverter::class + * - \MarvinLabs\DiscordLogger\Converters\RichRecordConverter::class + */ + 'converter' => \MarvinLabs\DiscordLogger\Converters\RichRecordConverter::class, + + /** + * If enabled, stacktraces will be attached as files. If not, stacktraces will be directly printed out in the + * message. + * + * Valid values are: + * + * - 'smart': when stacktrace is less than 2000 characters, it is inlined with the message, else attached as file + * - 'file': stacktrace is always attached as file + * - 'inline': stacktrace is always inlined with the message, truncated if necessary + */ + 'stacktrace' => 'smart', + + /* + * A set of colors to associate to the different log levels when using the `RichRecordConverter` + */ + 'colors' => [ + 'DEBUG' => 0x607d8b, + 'INFO' => 0x4caf50, + 'NOTICE' => 0x2196f3, + 'WARNING' => 0xff9800, + 'ERROR' => 0xf44336, + 'CRITICAL' => 0xe91e63, + 'ALERT' => 0x673ab7, + 'EMERGENCY' => 0x9c27b0, + ], + + /* + * A set of emojis to associate to the different log levels. Set to null to disable an emoji for a given level + */ + 'emojis' => [ + 'DEBUG' => ':beetle:', + 'INFO' => ':bulb:', + 'NOTICE' => ':wink:', + 'WARNING' => ':flushed:', + 'ERROR' => ':poop:', + 'CRITICAL' => ':imp:', + 'ALERT' => ':japanese_ogre:', + 'EMERGENCY' => ':skull:', + ], +]; \ No newline at end of file diff --git a/config/logging.php b/config/logging.php index 1aa06aa..1e68192 100644 --- a/config/logging.php +++ b/config/logging.php @@ -62,6 +62,13 @@ 'level' => env('LOG_LEVEL', 'critical'), ], + 'discord' => [ + 'driver' => 'custom', + 'via' => MarvinLabs\DiscordLogger\Logger::class, + 'level' => 'debug', + 'url' => env('LOG_DISCORD_WEBHOOK_URL'), + ], + 'papertrail' => [ 'driver' => 'monolog', 'level' => env('LOG_LEVEL', 'debug'), diff --git a/database/factories/CourseFactory.php b/database/factories/CourseFactory.php new file mode 100644 index 0000000..1d223f8 --- /dev/null +++ b/database/factories/CourseFactory.php @@ -0,0 +1,48 @@ + $this->faker->unique()->regexify('[A-Z]{4}[0-9]{4}'), + 'semester_id' => $this->faker->numberBetween(1, 8), + 'academic_program' => $this->faker->randomElement(array_keys(Course::getAcademicPrograms())), + 'version' => $this->faker->randomElement([1, 2]), + 'name' => $this->faker->sentence(3), + 'credits' => $this->faker->numberBetween(1, 6), + 'type' => $this->faker->randomElement(array_keys(Course::getTypes())), + 'content' => $this->faker->paragraph(), + 'objectives' => json_encode([$this->faker->sentence(), $this->faker->sentence()]), + 'time_allocation' => json_encode(['lectures' => $this->faker->numberBetween(10, 50), 'practicals' => $this->faker->numberBetween(5, 20)]), + 'marks_allocation' => json_encode(['assignments' => $this->faker->numberBetween(10, 30), 'exams' => $this->faker->numberBetween(40, 60)]), + 'ilos' => json_encode([$this->faker->sentence(), $this->faker->sentence()]), + 'references' => json_encode([$this->faker->sentence(), $this->faker->sentence()]), + 'created_by' => User::inRandomOrder()->first()->id, + 'updated_by' => User::inRandomOrder()->first()->id, + 'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'), + 'updated_at' => now(), + ]; + } +} diff --git a/database/factories/SemesterFactory.php b/database/factories/SemesterFactory.php new file mode 100644 index 0000000..026310b --- /dev/null +++ b/database/factories/SemesterFactory.php @@ -0,0 +1,40 @@ + $this->faker->sentence(3), + 'version' => $this->faker->randomElement(array_keys(Semester::getVersions())), + 'academic_program' => $this->faker->randomElement(array_keys(Semester::getAcademicPrograms())), + 'description' => $this->faker->paragraph, + 'url' => $this->faker->url, + 'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'), + 'updated_at' => now(), + 'created_by' => User::inRandomOrder()->first()->id, + 'updated_by' => User::inRandomOrder()->first()->id, + ]; + } +} diff --git a/database/migrations/2024_08_23_165504_create_semesters_table.php b/database/migrations/2024_08_23_165504_create_semesters_table.php new file mode 100644 index 0000000..c19095e --- /dev/null +++ b/database/migrations/2024_08_23_165504_create_semesters_table.php @@ -0,0 +1,39 @@ +id(); + $table->string('title', 255); + $table->enum('version', array_keys(Semester::getVersions())); + $table->enum('academic_program', array_keys(Semester::getAcademicPrograms())); + $table->text('description')->nullable(); + $table->string('url', 200)->unique(); + $table->timestamps(); + $table->foreignId('created_by')->constrained('users'); + $table->foreignId('updated_by')->constrained('users'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('semesters'); + } +} diff --git a/database/migrations/2024_09_03_102546_create_courses_table.php b/database/migrations/2024_09_03_102546_create_courses_table.php new file mode 100644 index 0000000..4af42a2 --- /dev/null +++ b/database/migrations/2024_09_03_102546_create_courses_table.php @@ -0,0 +1,49 @@ +id(); // Primary key, auto-incrementing + $table->string('code', 16)->unique(); // Course code with a unique constraint + $table->foreignId('semester_id')->constrained('semesters')->on('semesters') + ->onDelete('cascade');; // Foreign key to semesters.id + $table->enum('academic_program', array_keys(Course::getAcademicPrograms())); // Enum for academic program + $table->enum('version', array_keys(Course::getVersions())); // Enum for version as numeric keys + $table->string('name', 255); // Course name + $table->integer('credits')->max(18)->min(0); // Credit hours + $table->enum('type', array_keys(Course::getTypes())); // Enum for course type + $table->text('content')->nullable(); // Course content, nullable + $table->json('objectives')->nullable(); // JSON for course objectives, nullable + $table->json('time_allocation')->nullable(); // JSON for time allocation, nullable + $table->json('marks_allocation')->nullable(); // JSON for marks allocation, nullable + $table->json('ilos')->nullable(); // JSON for intended learning outcomes, nullable + $table->json('references')->nullable(); // JSON for references, nullable + $table->string('faq_page', 191)->nullable(); + $table->timestamps(); + $table->foreignId('created_by')->constrained('users'); // Foreign key to users.id for created_by + $table->foreignId('updated_by')->constrained('users'); // Foreign key to users.id for updated_by + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('courses'); + } +} diff --git a/database/migrations/2024_09_03_102824_create_course_modules_table.php b/database/migrations/2024_09_03_102824_create_course_modules_table.php new file mode 100644 index 0000000..05ac4b8 --- /dev/null +++ b/database/migrations/2024_09_03_102824_create_course_modules_table.php @@ -0,0 +1,37 @@ +id(); // Primary key, auto-incrementing + $table->foreignId('course_id')->constrained('courses'); // Foreign key to courses.id + $table->string('topic', 255); // module topic + $table->text('description')->nullable(); // Course content, nullable + $table->json('time_allocation')->nullable(); // JSON for time allocation, nullable + $table->timestamps(); // Created_at and updated_at + $table->foreignId('created_by')->constrained('users'); // Foreign key to users.id for created_by + $table->foreignId('updated_by')->constrained('users'); // Foreign key to users.id for updated_by + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('course_modules'); + } +} \ No newline at end of file diff --git a/database/seeders/Auth/PermissionRoleSeeder.php b/database/seeders/Auth/PermissionRoleSeeder.php index 1ff5f98..f077b58 100644 --- a/database/seeders/Auth/PermissionRoleSeeder.php +++ b/database/seeders/Auth/PermissionRoleSeeder.php @@ -26,17 +26,20 @@ public function run() // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // Create Roles Role::create([ - 'id' => 1, 'type' => User::TYPE_ADMIN, 'name' => 'Administrator', ]); Role::create([ - 'id' => 2, 'type' => User::TYPE_USER, 'name' => 'Editor', ]); + Role::create([ + 'type' => User::TYPE_USER, + 'name' => 'Course Manager', + ]); + // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // Non Grouped Permissions @@ -107,14 +110,37 @@ public function run() ]) ]); + // Role: CourseManager + $courseManager = Permission::create([ + 'type' => User::TYPE_USER, + 'name' => 'user.access.academic', + 'description' => 'All Course Manager Permissions', + ]); + + $courseManager->children()->saveMany([ + new Permission([ + 'type' => User::TYPE_USER, + 'name' => 'user.access.academic.semesters', + 'description' => 'Semesters', + ]), + new Permission([ + 'type' => User::TYPE_USER, + 'name' => 'user.access.academic.courses', + 'description' => 'Courses', + ]), + ]); // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // Assign permissions to Roles Role::findByName('Administrator')->givePermissionTo([ - 'admin.access.user', 'user.access.editor' + 'admin.access.user', + 'user.access.editor', + 'user.access.academic' ]); + Role::findByName('Editor')->givePermissionTo(['user.access.editor']); + Role::findByName('Course Manager')->givePermissionTo(['user.access.academic']); // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // Assign Permissions to users @@ -127,4 +153,4 @@ public function run() $this->enableForeignKeys(); } -} \ No newline at end of file +} diff --git a/database/seeders/Auth/UserRoleSeeder.php b/database/seeders/Auth/UserRoleSeeder.php index e2710dd..ada2a11 100644 --- a/database/seeders/Auth/UserRoleSeeder.php +++ b/database/seeders/Auth/UserRoleSeeder.php @@ -20,12 +20,16 @@ public function run() { $this->disableForeignKeys(); User::find(1)->assignRole(config('boilerplate.access.role.admin'), 'Editor'); + User::find(1)->assignRole(config('boilerplate.access.role.admin'), 'Course Manager'); // Only for the local testings if (app()->environment(['local', 'testing'])) { User::find(2)->assignRole('Editor'); + User::find(2)->assignRole('Course Manager'); + + User::find(5)->assignRole('Course Manager'); } $this->enableForeignKeys(); } -} \ No newline at end of file +} diff --git a/database/seeders/Auth/UserSeeder.php b/database/seeders/Auth/UserSeeder.php index 57f02e7..1505d99 100644 --- a/database/seeders/Auth/UserSeeder.php +++ b/database/seeders/Auth/UserSeeder.php @@ -34,7 +34,7 @@ public function run() if (app()->environment(['local', 'testing'])) { User::create([ 'type' => User::TYPE_USER, - 'name' => 'Test User', + 'name' => 'User', 'email' => env('SEED_USER_EMAIL', 'user@portal.ce.pdn.ac.lk'), 'password' => env('SEED_USER_PASSWORD', 'regular_user'), 'email_verified_at' => now(), @@ -43,8 +43,8 @@ public function run() User::create([ 'type' => User::TYPE_USER, - 'name' => 'NewsEditor1', - 'email' => env('SEED_NEWS_EDITOR_EMAIL', 'news.editor@portal.ce.pdn.ac.lk'), + 'name' => 'News Editor', + 'email' => env('SEED_NEWS_EDITOR_EMAIL', 'user+news.editor@portal.ce.pdn.ac.lk'), 'password' => env('SEED_NEWS_EDITOR_PASSWORD', 'news'), 'email_verified_at' => now(), 'active' => true, @@ -52,14 +52,23 @@ public function run() User::create([ 'type' => User::TYPE_USER, - 'name' => 'EventEditor1', - 'email' => env('SEED_EVENT_EDITOR_EMAIL', 'events.editor@portal.ce.pdn.ac.lk'), + 'name' => 'Event Editor', + 'email' => env('SEED_EVENT_EDITOR_EMAIL', 'user+events.editor@portal.ce.pdn.ac.lk'), 'password' => env('SEED_EVENT_EDITOR_PASSWORD', 'events'), 'email_verified_at' => now(), 'active' => true, ]); + + User::create([ + 'type' => User::TYPE_USER, + 'name' => 'CourseManager', + 'email' => env('SEED_COURSE_MANAGER_EMAIL', 'course_manager@portal.ce.pdn.ac.lk'), + 'password' => env('SEED_COURSE_MANAGER_PASSWORD', 'course_manager'), + 'email_verified_at' => now(), + 'active' => true, + ]); } $this->enableForeignKeys(); } -} \ No newline at end of file +} diff --git a/database/seeders/CourseSeeder.php b/database/seeders/CourseSeeder.php new file mode 100644 index 0000000..8e59a42 --- /dev/null +++ b/database/seeders/CourseSeeder.php @@ -0,0 +1,231 @@ + 'GP101', + 'name' => 'English I', + 'credits' => 3, + 'type' => 'Core', + 'semester_id' => 1, + 'academic_program' => 'undergraduate', + 'version' => 1, + 'content' => 'Language development, Communication through reading, Communication through listening, Communication through writing, Communication through speech', + 'objectives' => '', + 'ilos' => json_encode(['knowledge' => [], 'skills' => [], 'attitudes' => []]), + 'time_allocation' => json_encode(['lecture' => '20', 'assignment' => '50', 'tutorial' => '', 'practical' => '1']), + 'marks_allocation' => json_encode([ + 'practicals' => '10', + 'project' => '', + 'mid_exam' => '30', + 'end_exam' => '60' + ]), + 'references' => json_encode([]), + 'created_by' => '1', + 'updated_by' => '1', + + ]); + + Course::create([ + 'code' => 'GP109', + 'name' => 'Materials Science', + 'credits' => 3, + 'type' => 'Core', + 'semester_id' => 1, + 'academic_program' => 'undergraduate', + 'version' => 1, + 'content' => 'Introduction to the structure and properties of engineering materials, Principles underlying structure-property relationships, Phase equilibrium, Structure and properties of cement and timber...', + 'objectives' => 'Introduce the structure and properties of Engineering Materials', + 'ilos' => json_encode([ + 'knowledge' => ['Describe materials in major classes of engineering materials'], + 'skills' => ['Use Equilibrium Phase diagrams...'], + 'attitudes' => ['Appreciate structure-property relationships...'] + ]), + 'time_allocation' => json_encode(['lecture' => '38', 'assignment' => '1', 'tutorial' => '10', 'practical' => '1']), + 'marks_allocation' => json_encode([ + 'practicals' => '10', + 'project' => '10', + 'mid_exam' => '30', + 'end_exam' => '50' + ]), + 'references' => json_encode([ + 'Engineering Materials 1...', + 'The Science and Engineering of Materials...' + ]), + 'created_by' => '1', + 'updated_by' => '1', + + ]); + + Course::create([ + 'code' => 'GP110', + 'name' => 'Engineering Mechanics', + 'credits' => 3, + 'type' => 'Core', + 'semester_id' => 1, + 'academic_program' => 'undergraduate', + 'version' => 1, + 'content' => 'Force systems, Analysis of simple structures, Work and energy methods, Inertial properties of plane and three-dimensional objects...', + 'objectives' => 'To introduce the state of rest or motion of bodies subjected to forces. Emphasis on applications to Engineering Designs.', + 'ilos' => json_encode([ + 'knowledge' => ['Use scalar and vector methods for analyzing forces in structures.'], + 'skills' => ['Apply fundamental concepts of motion and identify parameters that define motion.'], + 'attitudes' => ['Use engineering mechanics for solving problems systematically.'] + ]), + 'time_allocation' => json_encode(['lecture' => '28', 'assignment' => '11', 'tutorial' => '', 'practical' => '12']), + 'marks_allocation' => json_encode([ + 'practicals' => '10', + 'project' => '10', + 'mid_exam' => '20', + 'end_exam' => '60' + ]), + 'references' => json_encode([ + 'Hibbeler, R.C., Engineering Mechanics Statics and Dynamics...', + 'Douglas, J. F., Fluid Mechanics...' + ]), + 'created_by' => '1', + 'updated_by' => '1', + ]); + + Course::create([ + 'code' => 'GP115', + 'name' => 'Calculus I', + 'credits' => 3, + 'type' => 'Core', + 'semester_id' => 1, + 'academic_program' => 'undergraduate', + 'version' => 1, + 'content' => 'Real number system, Functions of a single variable, 2-D coordinate geometry, 3-D Euclidean geometry, Complex numbers...', + 'objectives' => '', + 'ilos' => json_encode([ + 'knowledge' => ['Analyze problems in limits, continuity, differentiability, and integration.'], + 'skills' => ['Compute derivatives of complex functions, identify conic sections, and solve problems.'], + 'attitudes' => ['Determine the convergence of sequences and series, and find power series expansions.'] + ]), + 'time_allocation' => json_encode(['lecture' => '36', 'assignment' => '18', 'tutorial' => '', 'practical' => '12']), + 'marks_allocation' => json_encode([ + 'practicals' => '20', + 'project' => '', + 'mid_exam' => '30', + 'end_exam' => '50' + ]), + 'references' => json_encode([ + 'James Stewart, Calculus...', + 'Watson Fulks, Advanced Calculus...' + ]), + 'created_by' => '1', + 'updated_by' => '1', + ]); + + Course::create([ + 'code' => 'GP112', + 'name' => 'Engineering Measurements', + 'credits' => 3, + 'type' => 'Core', + 'semester_id' => 1, + 'academic_program' => 'undergraduate', + 'version' => 1, + 'content' => 'Units and standards, Approximation errors and calibration, Measurement of physical parameters...', + 'objectives' => json_encode(['Understand different aspects of instrumentation and solve engineering problems through measurement and experimentation.']), + 'ilos' => json_encode([ + 'knowledge' => ['Measure basic engineering quantities and present results using charts and tables.'], + 'skills' => ['Identify and minimize measurement errors, analyze time-dependent output of instruments.'], + 'attitudes' => ['Construct experiments to test hypotheses using statistical techniques.'] + ]), + 'time_allocation' => json_encode(['lecture' => '21', 'assignment' => '', 'tutorial' => '4', 'practical' => '40']), + 'marks_allocation' => json_encode([ + 'practicals' => '40', + 'project' => '20', + 'mid_exam' => '', + 'end_exam' => '40' + ]), + 'references' => json_encode([ + 'Schofield, W., Engineering Surveying...', + 'Ghilani, Charles D., Elementary Surveying...' + ]), + 'created_by' => '1', + 'updated_by' => '1', + ]); + + Course::create([ + 'code' => 'GP113', + 'name' => 'Fundamentals of Manufacture', + 'credits' => 3, + 'type' => 'Core', + 'semester_id' => 2, + 'academic_program' => 'undergraduate', + 'version' => 1, + 'content' => 'Introduction to manufacturing industry, Machining, Casting, Welding, Metal forming, Manufacturing systems...', + 'objectives' => + 'Provide fundamental knowledge of manufacturing engineering and design.Enable students to evaluate and manufacture products while satisfying consumer requirements.', + 'ilos' => json_encode([ + 'knowledge' => ['Understand the core principles of manufacturing processes.'], + 'skills' => ['Evaluate manufacturing systems for optimizing efficiency.'], + 'attitudes' => ['Apply safety measures in engineering manufacturing processes.'] + ]), + 'time_allocation' => json_encode(['lecture' => '20', 'assignment' => '36', 'tutorial' => '7', 'practical' => '40']), + 'marks_allocation' => json_encode([ + 'practicals' => '30', + 'project' => '10', + 'mid_exam' => '20', + 'end_exam' => '40' + ]), + 'references' => json_encode([ + 'Shop Theory by Anderson and Tatro...', + 'Workshop Technology by W.A.J. Chapman...' + ]), + 'created_by' => '1', + 'updated_by' => '1', + ]); + + Course::create([ + 'code' => 'CO221', + 'name' => 'Digital Design', + 'credits' => 3, + 'type' => 'Core', + 'semester_id' => 3, + 'academic_program' => 'undergraduate', + 'version' => 1, + 'content' => 'Introduction to digital logic, Number systems, Combinational logic circuits, Sequential logic circuits, Digital circuit design and implementation...', + 'objectives' => + 'Introduce digital electronics with emphasis on practical design techniques for digital circuits.Teach how to design combinational and sequential circuits.', + 'ilos' => json_encode([ + 'knowledge' => ['Perform Boolean manipulation and design digital circuits.'], + 'skills' => ['Design and implement basic combinational and sequential circuits.'], + 'attitudes' => ['Develop confidence in digital circuit design.'] + ]), + 'time_allocation' => json_encode(['lecture' => '30', 'assignment' => '14', 'tutorial' => '10']), + 'marks_allocation' => json_encode([ + + 'practicals' => '10', + 'project' => '', + 'mid_exam' => '30', + 'end_exam' => '60' + ]), + 'references' => json_encode([ + 'Digital Design by Morris Mano...', + 'Digital Design: A Systems Approach by William James Dally...' + ]), + 'created_by' => '1', + 'updated_by' => '1', + ]); + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 49ed96a..eeeabdc 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -5,6 +5,7 @@ use Database\Seeders\Traits\TruncateTable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\App; /** @@ -26,10 +27,15 @@ public function run() 'failed_jobs', ]); - $this->call(AuthSeeder::class); - $this->call(AnnouncementSeeder::class); - $this->call(NewsSeeder::class); - $this->call(EventSeeder::class); + + if (App::environment('local', 'testing')) { + $this->call(AuthSeeder::class); + $this->call(AnnouncementSeeder::class); + $this->call(NewsSeeder::class); + $this->call(EventSeeder::class); + $this->call(SemesterSeeder::class); + $this->call(CourseSeeder::class); + } Model::reguard(); } diff --git a/database/seeders/EventSeeder.php b/database/seeders/EventSeeder.php index 0798fb8..8f947f1 100644 --- a/database/seeders/EventSeeder.php +++ b/database/seeders/EventSeeder.php @@ -24,7 +24,7 @@ public function run() [ "id" => 3, "title" => "ESCAPE - 2020", - "description" => "

EscaPe is the annual project symposium of the Department of Computer Engineering, University of Peradeniya. It will present the research projects of the undergraduates of the Department of Computer Engineering. ESCaPe 2020 is the 5th symposium that is organized by the department and this time the symposium is open for a broader audience and aims to build a platform for the undergraduates to present their research ideas to the industry and academic community.<\/span><\/p>", + "description" => '

EscaPe is the annual project symposium of the Department of Computer Engineering, University of Peradeniya. It will present the research projects of the undergraduates of the Department of Computer Engineering. ESCaPe 2020 is the 5th symposium that is organized by the department and this time the symposium is open for a broader audience and aims to build a platform for the undergraduates to present their research ideas to the industry and academic community.

Further details of the event are at: http://aces.ce.pdn.ac.lk/escape20/

', "url" => "escape-2020", "published_at" => "2024-08-27", "image" => "1724778446.jpg", @@ -41,7 +41,7 @@ public function run() [ "id" => 4, "title" => "VIVACES 2020", - "description" => "

Online social gathering of the Department of Computer Engineering, University of Peradeniya was held on Friday 12th of June, 2020 with the participation of the Students and Staff.<\/span><\/p>", + "description" => '

Online social gathering of Department of Computer Engineering, University of Peradeniya was held on Friday 12th of June, 2020 with the participation of the Students and Staff successfully.

', "url" => "VIVACES-2020", "published_at" => "2024-08-27", "image" => "1724778453.jpg", @@ -58,7 +58,7 @@ public function run() [ "id" => 5, "title" => "Hackers’ Club Developer Series", - "description" => "

An Online Webinar series organized by Hackers’ Club to introduce some of the tools that you must have up on your sleeve to be a successful Developer\/Engineer in the world of Computing. And also a chance to master some of them with the Developer Resources & Materials shared by Hackers’ Club.<\/p>

This Developer Series mainly focuses on front-end web development, and back-end development, for implementing a multi-platform solution for the real world problems. The Developer Series will be an invaluable chance for you to start the journey of mastering the Web Development world.<\/p>


<\/p>

Series Timeline=><\/p>

+ + + -
- @include('backend.includes.partials.breadcrumbs') +
+ @include('backend.includes.partials.breadcrumbs') -
- @yield('breadcrumb-links') -
-
+
+ @yield('breadcrumb-links') +
+
diff --git a/resources/views/backend/includes/sidebar.blade.php b/resources/views/backend/includes/sidebar.blade.php index 3dd15df..22c166e 100644 --- a/resources/views/backend/includes/sidebar.blade.php +++ b/resources/views/backend/includes/sidebar.blade.php @@ -1,12 +1,4 @@ @stack('before-scripts') + - @stack('after-scripts') - - diff --git a/resources/views/backend/news/create.blade.php b/resources/views/backend/news/create.blade.php index 8b69780..ca25c62 100644 --- a/resources/views/backend/news/create.blade.php +++ b/resources/views/backend/news/create.blade.php @@ -8,6 +8,10 @@ @endpush +@push('before-scripts') + +@endpush + @section('content')
{!! Form::open([ @@ -28,9 +32,9 @@
{!! Form::label('title', 'Title*', ['class' => 'col-md-2 col-form-label']) !!}
- {!! Form::text('title', '', ['class' => 'form-control', 'required' => true]) !!} + {!! Form::text('title', '', ['class' => 'form-control']) !!} @error('title') - {{ $message }} + {{ $message }} @enderror
@@ -39,9 +43,9 @@
{!! Form::label('published_at', 'Publish at*', ['class' => 'col-md-2 col-form-label']) !!}
- {!! Form::date('published_at', date('Y-m-d'), ['class' => 'form-control', 'required' => true]) !!} + {!! Form::date('published_at', date('Y-m-d'), ['class' => 'form-control']) !!} @error('published_at') - {{ $message }} + {{ $message }} @enderror
@@ -51,11 +55,11 @@ {!! Form::label('url', 'URL*', ['class' => 'col-md-2 col-form-label']) !!}
- https://ce.pdn.ac.lk/news/{yyyy-mm-dd}-  - {!! Form::text('url', '', ['class' => 'form-control', 'required' => true]) !!} + https://www.ce.pdn.ac.lk/news/{yyyy-mm-dd}- + {!! Form::text('url', '', ['class' => 'form-control']) !!}
@error('url') - {{ $message }} + {{ $message }} @enderror
@@ -64,22 +68,63 @@
{!! Form::label('description', 'Description*', ['class' => 'col-md-2 col-form-label']) !!}
-
- +
+
+ + +
+ @error('description') + {{ $message }} + @enderror +
+
- @error('description') - {{ $message }} - @enderror
+ -
+
{!! Form::label('image', 'Image', ['class' => 'col-md-2 col-form-label']) !!}
- {!! Form::file('image', ['accept' => 'image/*']) !!} + {!! Form::file('image', ['accept' => 'image/*', 'x-on:change' => 'updatePreview($event)']) !!} @error('image') - {{ $message }} + {{ $message }} @enderror + +
+ Image Preview +
@@ -90,9 +135,9 @@
- + @error('enabled') - {{ $message }} + {{ $message }} @enderror
@@ -103,7 +148,7 @@ class="form-check-input checkbox-lg" checked />
{!! Form::text('link_url', '', ['class' => 'form-control']) !!} @error('link_url') - {{ $message }} + {{ $message }} @enderror
@@ -114,15 +159,14 @@ class="form-check-input checkbox-lg" checked />
{!! Form::text('link_caption', '', ['class' => 'form-control']) !!} @error('link_caption') - {{ $message }} + {{ $message }} @enderror
- - {!! Form::submit('Create', ['class' => 'btn btn-primary float-right', 'id' => 'submit-button']) !!} + {!! Form::submit('Create', ['class' => 'btn btn-primary btn-w-150 float-right']) !!} @@ -130,4 +174,10 @@ class="form-check-input checkbox-lg" checked /> {!! Form::close() !!} + @endsection diff --git a/resources/views/backend/news/edit.blade.php b/resources/views/backend/news/edit.blade.php index 4c233cd..d616540 100644 --- a/resources/views/backend/news/edit.blade.php +++ b/resources/views/backend/news/edit.blade.php @@ -8,6 +8,10 @@ @endpush +@push('before-scripts') + +@endpush + @section('content')
{!! Form::open([ @@ -26,15 +30,11 @@
- {!! Form::label('message', 'Title*', ['class' => 'col-md-2 col-form-label']) !!} - + {!! Form::label('title', 'Title*', ['class' => 'col-md-2 col-form-label']) !!}
- {!! Form::text('title', $news->title, [ - 'class' => 'form-control', - 'required' => true, - ]) !!} + {!! Form::text('title', $news->title, ['class' => 'form-control', 'required' => true]) !!} @error('title') - {{ $message }} + {{ $message }} @enderror
@@ -45,7 +45,7 @@
{!! Form::date('published_at', $news->published_at, ['class' => 'form-control', 'required' => true]) !!} @error('published_at') - {{ $message }} + {{ $message }} @enderror
@@ -55,11 +55,12 @@ {!! Form::label('url', 'URL*', ['class' => 'col-md-2 col-form-label']) !!}
- https://ce.pdn.ac.lk/news/{{ $news->published_at }}-  - {!! Form::text('url', $news->url, ['class' => 'form-control', 'required' => true]) !!} + https://www.ce.pdn.ac.lk/news/{{ $news->published_at }}-  + {!! Form::text('url', $news->url, ['class' => 'form-control', 'required' => true]) !!}
@error('url') - {{ $message }} + {{ $message }} @enderror
@@ -67,81 +68,114 @@
{!! Form::label('description', 'Description*', ['class' => 'col-md-2 col-form-label']) !!} -
-
{!! $news->description !!}
- - @error('description') - {{ $message }} - @enderror +
+
+ + +
+ @error('description') + {{ $message }} + @enderror +
+
-
+
{!! Form::label('image', 'Image', ['class' => 'col-md-2 col-form-label']) !!} -
-
{!! Form::file('image', ['accept' => 'image/*']) !!} +
+ {!! Form::file('image', ['accept' => 'image/*', 'x-on:change' => 'updatePreview($event)']) !!} @error('image') - {{ $message }} + {{ $message }} @enderror
- - - Image preview + Image preview
+ -
+
{!! Form::label('enabled', 'Enabled*', ['class' => 'col-md-2 form-check-label']) !!} -
enable ? 'checked' : '""' }} class="form-check-input checkbox-lg" {{ $news->enabled == 1 ? 'checked' : '' }} /> - + @error('enabled') - {{ $message }} + {{ $message }} @enderror
- +
{!! Form::label('link_url', 'Link URL', ['class' => 'col-md-2 col-form-label']) !!} -
- {!! Form::text('link_url', $news->link_url, [ - 'class' => 'form-control', - ]) !!} + {!! Form::text('link_url', $news->link_url, ['class' => 'form-control']) !!} @error('link_url') - {{ $message }} + {{ $message }} @enderror
- +
{!! Form::label('link_caption', 'Link Caption', ['class' => 'col-md-2 col-form-label']) !!} -
- {!! Form::text('link_caption', $news->link_caption, [ - 'class' => 'form-control', - ]) !!} + {!! Form::text('link_caption', $news->link_caption, ['class' => 'form-control']) !!} @error('link_caption') - {{ $message }} + {{ $message }} @enderror
- + - {!! Form::submit('Update', ['class' => 'btn btn-primary float-right', 'id' => 'submit-button']) !!} + {!! Form::submit('Update', ['class' => 'btn btn-primary btn-w-150 float-right', 'id' => 'submit-button']) !!} {!! Form::close() !!}
+ + @endsection diff --git a/resources/views/backend/news/index-table-row.blade.php b/resources/views/backend/news/index-table-row.blade.php index d3425a7..747adc5 100644 --- a/resources/views/backend/news/index-table-row.blade.php +++ b/resources/views/backend/news/index-table-row.blade.php @@ -31,12 +31,16 @@
- + + + - +
diff --git a/resources/views/backend/news/index.blade.php b/resources/views/backend/news/index.blade.php index 53a2917..14ca52d 100644 --- a/resources/views/backend/news/index.blade.php +++ b/resources/views/backend/news/index.blade.php @@ -15,7 +15,6 @@ - @if (session('Success'))
{{ session('Success') }} diff --git a/resources/views/backend/news/preview.blade.php b/resources/views/backend/news/preview.blade.php new file mode 100644 index 0000000..82ee6b1 --- /dev/null +++ b/resources/views/backend/news/preview.blade.php @@ -0,0 +1,59 @@ + + + + {{ $news->title }} + + + + + @include('includes.partials.news-preview') +
+
+ {{ $news->title }}
+ @if($news->published_at) + {{ $news->published_at }} · + @endif + + @if($news->user) + + {{ $news->user->name }} · + + @endif + + @if($news->description) + @php + $words = str_word_count(strip_tags($news->description)); + $read_time = ceil($words / 200); + @endphp + + {{ $read_time }} mins read + + @endif + +
+ + @if($news->image) +
+
+ {{ $news->title }} +
+
+ @endif +
+ {!! $news->description !!} +
+ + @if($news->link_url) +
+

Resources / Links

+ +
+ @endif + +
+
+ \ No newline at end of file diff --git a/resources/views/backend/semesters/create.blade.php b/resources/views/backend/semesters/create.blade.php new file mode 100644 index 0000000..08b14f4 --- /dev/null +++ b/resources/views/backend/semesters/create.blade.php @@ -0,0 +1,118 @@ +@extends('backend.layouts.app') + +@section('title', __('Create Semester')) + +@section('content') +
+ {!! Form::open([ + 'url' => route('dashboard.semesters.store'), + 'method' => 'post', + 'class' => 'container', + 'files' => true, + 'enctype' => 'multipart/form-data', + ]) !!} + + + + Semester : Create + + + + +
+ {!! Form::label('title', 'Title*', ['class' => 'col-md-2 col-form-label']) !!} +
+ {!! Form::text('title', old('title'), [ + 'class' => 'form-control', + 'required' => true, + ]) !!} + @error('title') + {{ $message }} + @enderror +
+
+ + +
+ {!! Form::label('academic_program', 'Academic Program*', ['class' => 'col-md-2 col-form-label']) !!} +
+ {!! Form::select( + 'academic_program', + \App\Domains\AcademicProgram\Semester\Models\Semester::getAcademicPrograms(), + null, + [ + 'class' => 'form-select', + 'placeholder' => 'Select Academic Program', + 'required' => true, + 'id' => 'academic_program', + ], + ) !!} + @error('academic_program') + {{ $message }} + @enderror +
+
+ + +
+ {!! Form::label('version', 'Version*', ['class' => 'col-md-2 col-form-label']) !!} +
+ {!! Form::select('version', \App\Domains\AcademicProgram\Semester\Models\Semester::getVersions(), null, [ + 'class' => 'form-select', + 'placeholder' => 'Select Version', + 'required' => true, + ]) !!} + @error('version') + {{ $message }} + @enderror +
+
+ + +
+ {!! Form::label('description', 'Description*', ['class' => 'col-md-2 col-form-label']) !!} +
+ {!! Form::textarea('description', old('description'), ['class' => 'form-control']) !!} + @error('description') + {{ $message }} + @enderror +
+
+ + +
+ {!! Form::label('url', 'URL*', ['class' => 'col-md-2 col-form-label']) !!} +
+
+ + https://www.ce.pdn.ac.lk/academics/{academic_program}/semesters/ + + {!! Form::text('url', old('url', ''), ['class' => 'form-control', 'required' => true]) !!} + +
+ @error('url') + {{ $message }} + @enderror +
+ + +
+
+ + + {!! Form::submit('Create', ['class' => 'btn btn-primary float-right btn-w-150']) !!} + + +
+ {!! Form::close() !!} +
+ + +@endsection diff --git a/resources/views/backend/semesters/delete.blade.php b/resources/views/backend/semesters/delete.blade.php new file mode 100644 index 0000000..cd28ca0 --- /dev/null +++ b/resources/views/backend/semesters/delete.blade.php @@ -0,0 +1,44 @@ +@extends('backend.layouts.app') + +@section('title', __('Delete Semester')) + +@section('content') +
+ + + Semester : Delete | {{ $semester->title }} + + + +

Are you sure you want to delete + "{{ $semester->title }}" ? +

+ + @if ($courses->count() > 0) +

The following courses are linked to this semester. Deletion is not permitted until these courses are + reassigned or deleted.

+ + Back + @else +
+ {!! Form::open([ + 'url' => route('dashboard.semesters.destroy', $semester), + 'method' => 'delete', + 'class' => 'container', + ]) !!} + Back + {!! Form::submit('Delete', ['class' => 'btn btn-danger']) !!} + {!! Form::close() !!} +
+ @endif +
+
+
+@endsection diff --git a/resources/views/backend/semesters/edit.blade.php b/resources/views/backend/semesters/edit.blade.php new file mode 100644 index 0000000..2fb08e9 --- /dev/null +++ b/resources/views/backend/semesters/edit.blade.php @@ -0,0 +1,114 @@ +@extends('backend.layouts.app') + +@section('title', __('Edit Semester')) + +@section('content') +
+ {!! Form::model($semester, [ + 'url' => route('dashboard.semesters.update', $semester->id), + 'method' => 'put', + 'class' => 'container', + 'files' => true, + 'enctype' => 'multipart/form-data', + ]) !!} + + + + Semester : Edit | {{ $semester->title }} + + + + +
+ {!! Form::label('title', 'Title*', ['class' => 'col-md-2 col-form-label']) !!} +
+ {!! Form::text('title', null, [ + 'class' => 'form-control', + 'required' => true, + ]) !!} + @error('title') + {{ $message }} + @enderror +
+
+ + +
+ {!! Form::label('academic_program', 'Academic Program*', ['class' => 'col-md-2 col-form-label']) !!} +
+ {!! Form::select( + 'academic_program', + \App\Domains\AcademicProgram\Semester\Models\Semester::getAcademicPrograms(), + null, + [ + 'class' => 'form-control', + 'required' => true, + ], + ) !!} + @error('academic_program') + {{ $message }} + @enderror +
+
+ + +
+ {!! Form::label('version', 'Version*', ['class' => 'col-md-2 col-form-label']) !!} +
+ {!! Form::select('version', \App\Domains\AcademicProgram\Semester\Models\Semester::getVersions(), null, [ + 'class' => 'form-control', + 'required' => true, + ]) !!} + @error('version') + {{ $message }} + @enderror +
+
+ + +
+ {!! Form::label('description', 'Description*', ['class' => 'col-md-2 col-form-label']) !!} +
+ {!! Form::textarea('description', null, ['class' => 'form-control']) !!} + @error('description') + {{ $message }} + @enderror +
+
+ + +
+ {!! Form::label('url', 'URL*', ['class' => 'col-md-2 col-form-label']) !!} +
+
+ + https://www.ce.pdn.ac.lk/academics/{{ strtolower($semester->academic_program ?? 'academic_program') }}/semesters/  + + + {!! Form::text('url', old('url', $semester->url), ['class' => 'form-control', 'required' => true]) !!} + +
+ @error('url') + {{ $message }} + @enderror +
+
+
+ + + {!! Form::submit('Update', ['class' => 'btn btn-primary float-right btn-w-150']) !!} + + +
+ {!! Form::close() !!} +
+ + +@endsection diff --git a/resources/views/backend/semesters/index-table-row.blade.php b/resources/views/backend/semesters/index-table-row.blade.php new file mode 100644 index 0000000..fd77c22 --- /dev/null +++ b/resources/views/backend/semesters/index-table-row.blade.php @@ -0,0 +1,41 @@ + + {{ $row->title }} + + + + {{ \App\Domains\AcademicProgram\Semester\Models\Semester::getVersions()[$row->version] ?? 'Unknown Version' }} + + + + {{ $row->academicProgram() }} + + + + {{ $row->description }} + + + + /{{ $row->url }} + + + + {{ $row->updatedUser->name }} + + + + {{ $row->updated_at }} + + + +
+
+ + + + +
+
+
diff --git a/resources/views/backend/semesters/index.blade.php b/resources/views/backend/semesters/index.blade.php new file mode 100644 index 0000000..80a6b6a --- /dev/null +++ b/resources/views/backend/semesters/index.blade.php @@ -0,0 +1,31 @@ +@extends('backend.layouts.app') + +@section('title', __('Semesters')) + +@section('content') +
+ + + Semesters + + + + + + + + + @if (session('Success')) +
+ {{ session('Success') }} + +
+ @endif + + @livewire('backend.semester-table') +
+
+
+@endsection diff --git a/resources/views/components/backend/course_module.blade.php b/resources/views/components/backend/course_module.blade.php new file mode 100644 index 0000000..d53f0a5 --- /dev/null +++ b/resources/views/components/backend/course_module.blade.php @@ -0,0 +1,231 @@ +
+ +
Modules
+
+ + +
+ + {{-- Accordion --}} +
+ +
+ + + + +
+ + diff --git a/resources/views/components/backend/marks_allocation.blade.php b/resources/views/components/backend/marks_allocation.blade.php new file mode 100644 index 0000000..b720724 --- /dev/null +++ b/resources/views/components/backend/marks_allocation.blade.php @@ -0,0 +1,79 @@ +
+
+
+ +
+
+
+ + + +
+
+ +
+
+ + % +
+
+
+
+
+
+
diff --git a/resources/views/components/backend/time_allocation.blade.php b/resources/views/components/backend/time_allocation.blade.php new file mode 100644 index 0000000..1c08cd9 --- /dev/null +++ b/resources/views/components/backend/time_allocation.blade.php @@ -0,0 +1,50 @@ +
+
+
+ +
+
+
+ + + +
+
+ +
+
+ + hours +
+
+
+
+
diff --git a/resources/views/errors/503.blade.php b/resources/views/errors/503.blade.php index acd3810..00a54af 100644 --- a/resources/views/errors/503.blade.php +++ b/resources/views/errors/503.blade.php @@ -2,4 +2,4 @@ @section('title', __('Service Unavailable')) @section('code', '503') -@section('message', __($exception->getMessage() ?: 'Service Unavailable')) +@section('message', __('Site Under Maintenance. Please wait for a few minutes and retry')) diff --git a/resources/views/frontend/auth/includes/social.blade.php b/resources/views/frontend/auth/includes/social.blade.php index ab4b5b8..69cd279 100644 --- a/resources/views/frontend/auth/includes/social.blade.php +++ b/resources/views/frontend/auth/includes/social.blade.php @@ -1,41 +1,19 @@ - - - +
- - - - + + + + diff --git a/resources/views/includes/partials/event-preview.blade.php b/resources/views/includes/partials/event-preview.blade.php new file mode 100644 index 0000000..fb5b495 --- /dev/null +++ b/resources/views/includes/partials/event-preview.blade.php @@ -0,0 +1,5 @@ +
+ @lang('This is a preview of the event, :title.', ['title' => $event->title]) + @lang('Back') | + @lang('Edit') +
\ No newline at end of file diff --git a/resources/views/includes/partials/news-preview.blade.php b/resources/views/includes/partials/news-preview.blade.php new file mode 100644 index 0000000..2463586 --- /dev/null +++ b/resources/views/includes/partials/news-preview.blade.php @@ -0,0 +1,5 @@ +
+ @lang('This is a preview of the news, :title.', ['title' => $news->title]) + @lang('Back') | + @lang('Edit') +
\ No newline at end of file diff --git a/resources/views/livewire/backend/create-courses.blade.php b/resources/views/livewire/backend/create-courses.blade.php new file mode 100644 index 0000000..068ec2d --- /dev/null +++ b/resources/views/livewire/backend/create-courses.blade.php @@ -0,0 +1,241 @@ + + + Course : Create + + + +
+
+
+ 1 +
+
+ 2 +
+
+ 3 +
+
+ @if ($formStep == 1) +
+
+
+
Basics
+
+
+
+
+ +
+ + @error('academicProgram') +
{{ $message }}
+ @enderror +
+
+
+ +
+ + @error('version') +
{{ $message }}
+ @enderror +
+
+
+ +
+ + @error('semester') +
{{ $message }}
+ @enderror +
+
+
+ +
+
+ +
+ @error('code') + {{ $message }} + @enderror +
+
+
+ +
+
+ +
+ @error('name') +
{{ $message }}
+ @enderror +
+
+
+
+
+ +
+ + @error('type') +
{{ $message }}
+ @enderror +
+
+
+ +
+
+ +
+ @error('credits') + {{ $message }} + @enderror +
+
+
+ +
+
+ +
+ @error('faq_page') +
{{ $message }}
+ @enderror +
+
+ + + @error('content') +
{{ $message }}
+ @enderror +
+
+ +
+
+ + +
+
+
+
+
+
+
+ @elseif ($formStep == 2) +
+
+
+ {{-- Objectives --}} +
+ Aims/Objectives: +
+
+ + {{-- objectives --}} +
+
+ {!! Form::textarea('objectives', '', [ + 'class' => 'form-control' . ($errors->has('objectives') ? ' is-invalid' : ''), + 'id' => 'floatingTextarea', + 'placeholder' => '', + 'rows' => 8, + 'style' => 'height: 200px;', + 'wire:model.lazy' => 'objectives', + ]) !!} + + @error('objectives') +
{{ $message }}
+ @enderror +
+
+ +
+ ILOs: +
+
+ + {{-- ILO --}} + @foreach ($ilos as $key => $value) +
+ @livewire('backend.item-adder', ['type' => $key, 'items' => $ilos[$key]], key("ilos-$key-adder")) +
+ @endforeach + +
+
+
+ @elseif ($formStep == 3) +
+
+
+
Modules & References
+ +
+ +
+ +
+ @livewire('backend.item-adder', ['type' => 'references', 'items' => $references], key('references-adder')) +
+ +
+
+
+ @endif +
+ + + + +
diff --git a/resources/views/livewire/backend/edit-courses.blade.php b/resources/views/livewire/backend/edit-courses.blade.php new file mode 100644 index 0000000..e0368ee --- /dev/null +++ b/resources/views/livewire/backend/edit-courses.blade.php @@ -0,0 +1,244 @@ + + + Course : Edit + + + +
+
+
+ 1 +
+
+ 2 +
+
+ 3 +
+
+ @if ($formStep == 1) +
+
+
+
Basics
+
+
+
+
+ +
+ + @error('academicProgram') +
{{ $message }}
+ @enderror +
+
+
+ +
+ + @error('version') +
{{ $message }}
+ @enderror +
+
+
+ +
+ + @error('semester') +
{{ $message }}
+ @enderror +
+
+
+ +
+
+ +
+ @error('code') + {{ $message }} + @enderror +
+
+
+ +
+
+ +
+ @error('name') +
{{ $message }}
+ @enderror +
+
+ +
+
+
+ +
+ + @error('type') +
{{ $message }}
+ @enderror +
+
+
+ +
+
+ +
+ @error('credits') + {{ $message }} + @enderror +
+
+
+ +
+
+ +
+ @error('faq_page') +
{{ $message }}
+ @enderror +
+
+ + + @error('content') +
{{ $message }}
+ @enderror +
+
+ +
+
+ + +
+
+
+
+
+
+ @elseif ($formStep == 2) +
+
+
+ {{-- Objectives --}} +
+ Aims/Objectives: +
+
+ + {{-- objectives --}} +
+
+ {!! Form::textarea('objectives', '', [ + 'class' => 'form-control' . ($errors->has('objectives') ? ' is-invalid' : ''), + 'id' => 'floatingTextarea', + 'placeholder' => '', + 'rows' => 8, + 'style' => 'height: 200px;', + 'wire:model.lazy' => 'objectives', + ]) !!} + + @error('objectives') +
{{ $message }}
+ @enderror +
+
+ +
+ ILOs: +
+
+ + {{-- ILO --}} + @foreach ($ilos as $key => $value) +
+ @livewire('backend.item-adder', ['type' => $key, 'items' => $ilos[$key]], key("ilos-$key-adder")) +
+ @endforeach + +
+
+
+ @elseif ($formStep == 3) +
+
+
+
Modules & References
+ + +
+ @livewire('backend.item-adder', ['type' => 'references', 'items' => $references], key('references-adder')) +
+
+
+ @endif +
+
+ + + + +
diff --git a/resources/views/livewire/backend/item-adder.blade.php b/resources/views/livewire/backend/item-adder.blade.php new file mode 100644 index 0000000..5ed3071 --- /dev/null +++ b/resources/views/livewire/backend/item-adder.blade.php @@ -0,0 +1,136 @@ +
+ +
{{ ucfirst($type) }}
+
+ + +
+ + + + +
    + +
+ +
diff --git a/routes/api.php b/routes/api.php index d63fff6..4b15fea 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,16 +2,24 @@ use App\Http\Controllers\API\NewsApiController; use App\Http\Controllers\API\EventApiController; +use App\Http\Controllers\API\CourseApiController; +use App\Http\Controllers\API\SemesterApiController; - -Route::group(['prefix' => 'news', 'as' => 'api.news.'], function () { +Route::group(['prefix' => 'news/v1', 'as' => 'api.news.'], function () { Route::get('/', [NewsApiController::class, 'index']); Route::get('/{id}', [NewsApiController::class, 'show']); }); -Route::group(['prefix' => 'events', 'as' => 'api.events.'], function () { +Route::group(['prefix' => 'events/v1', 'as' => 'api.events.'], function () { Route::get('', [EventApiController::class, 'index']); Route::get('/upcoming', [EventApiController::class, 'upcoming']); Route::get('/past', [EventApiController::class, 'past']); Route::get('/{id}', [EventApiController::class, 'show']); -}); \ No newline at end of file +}); + +Route::group(['prefix' => 'academic/v1/undergraduate', 'as' => 'api.academic.undergraduate.'], function () { + Route::get('/courses', [CourseApiController::class, 'index']); + Route::get('/semesters', [SemesterApiController::class, 'index']); +}); + +// TODO: Implement postgraduate courses API \ No newline at end of file diff --git a/routes/backend/academic_program.php b/routes/backend/academic_program.php new file mode 100644 index 0000000..82955b2 --- /dev/null +++ b/routes/backend/academic_program.php @@ -0,0 +1,12 @@ +name('academic_program.index') + ->middleware(['auth', 'permission:user.access.academic']) + ->breadcrumbs(function (Trail $trail) { + $trail->push(__('Home'), route('dashboard.home')) + ->push(__('Academic Program'), route('dashboard.academic_program.index')); + }); \ No newline at end of file diff --git a/routes/backend/courses.php b/routes/backend/courses.php new file mode 100644 index 0000000..9ef12ec --- /dev/null +++ b/routes/backend/courses.php @@ -0,0 +1,60 @@ + ['permission:user.access.academic.course']], function () { + + // Index + Route::get('/courses', function () { + return view('backend.courses.index'); + })->name('courses.index') + ->breadcrumbs(function (Trail $trail) { + $trail->push(__('Home'), route('dashboard.home')) + ->push(__('Academic Program'), route('dashboard.academic_program.index')) + ->push(__('Courses'), route('dashboard.courses.index')); + }); + + // Create + Route::get('courses/create', function () { + return view('backend.courses.create'); + })->name('courses.create') + ->breadcrumbs(function (Trail $trail) { + $trail->push(__('Home'), route('dashboard.home')) + ->push(__('Academic Program'), route('dashboard.academic_program.index')) + ->push(__('Courses'), route('dashboard.courses.index')) + ->push(__('Create')); + }); + + // Store + Route::post('courses', [CourseController::class, 'store']) + ->name('courses.store'); + + // Edit + Route::get('courses/edit/{course}', [CourseController::class, 'edit']) + ->name('courses.edit') + ->breadcrumbs(function (Trail $trail) { + $trail->push(__('Home'), route('dashboard.home')) + ->push(__('Academic Program'), route('dashboard.academic_program.index')) + ->push(__('Courses'), route('dashboard.courses.index')) + ->push(__('Edit')); + }); + + // Update + Route::put('courses/{course}', [CreateCourses::class, 'update']) + ->name('courses.update'); + + // Delete + Route::get('courses/delete/{course}', [CourseController::class, 'delete']) + ->name('courses.delete') + ->breadcrumbs(function (Trail $trail) { + $trail->push(__('Home'), route('dashboard.home')) + ->push(__('Courses'), route('dashboard.courses.index')) + ->push(__('Delete')); + }); + + // Destroy + Route::delete('courses/{course}', [CourseController::class, 'destroy']) + ->name('courses.destroy'); +}); diff --git a/routes/backend/event.php b/routes/backend/event.php index c206893..9aa776b 100644 --- a/routes/backend/event.php +++ b/routes/backend/event.php @@ -1,6 +1,7 @@ name('event.destroy'); + + //Preview + Route::get('events/preview/{event}', function (Event $event){ + return view('backend.event.preview', compact('event')); + })->name('event.preview'); }); \ No newline at end of file diff --git a/routes/backend/news.php b/routes/backend/news.php index b52253e..d887c9b 100644 --- a/routes/backend/news.php +++ b/routes/backend/news.php @@ -1,8 +1,10 @@ ['permission:user.access.editor.news']], function () { @@ -52,4 +54,9 @@ // Destroy Route::delete('news/{news}', [NewsController::class, 'destroy']) ->name('news.destroy'); -}); + + //Preview + Route::get('news/preview/{news}', function (News $news) { + return view('backend.news.preview', compact('news')); + })->name('news.preview'); +}); \ No newline at end of file diff --git a/routes/backend/semesters.php b/routes/backend/semesters.php new file mode 100644 index 0000000..4c0f985 --- /dev/null +++ b/routes/backend/semesters.php @@ -0,0 +1,60 @@ + ['permission:user.access.academic.semester']], function () { + + // Index + Route::get('/semesters', function () { + return view('backend.semesters.index'); + })->name('semesters.index') + ->breadcrumbs(function (Trail $trail) { + $trail->push(__('Home'), route('dashboard.home')) + ->push(__('Academic Program'), route('dashboard.academic_program.index')) + ->push(__('Semesters'), route('dashboard.semesters.index')); + }); + + // Create + Route::get('semesters/create', [SemesterController::class, 'create']) + ->name('semesters.create') + ->breadcrumbs(function (Trail $trail) { + $trail->push(__('Home'), route('dashboard.home')) + ->push(__('Academic Program'), route('dashboard.academic_program.index')) + ->push(__('Semesters'), route('dashboard.semesters.index')) + ->push(__('Create')); + }); + + // Store + Route::post('semesters', [SemesterController::class, 'store']) + ->name('semesters.store'); + + // Edit + Route::get('semesters/edit/{semester}', [SemesterController::class, 'edit']) + ->name('semesters.edit') + ->breadcrumbs(function (Trail $trail) { + $trail->push(__('Home'), route('dashboard.home')) + ->push(__('Academic Program'), route('dashboard.academic_program.index')) + ->push(__('Semesters'), route('dashboard.semesters.index')) + ->push(__('Edit')); + }); + + // Update + Route::put('semesters/{semester}', [SemesterController::class, 'update']) + ->name('semesters.update'); + + // Delete + Route::get('semesters/delete/{semester}', [SemesterController::class, 'delete']) + ->name('semesters.delete') + ->breadcrumbs(function (Trail $trail) { + $trail->push(__('Home'), route('dashboard.home')) + ->push(__('Academic Program'), route('dashboard.academic_program.index')) + ->push(__('Semesters'), route('dashboard.semesters.index')) + ->push(__('Delete')); + }); + + // Destroy + Route::delete('semesters/{semester}', [SemesterController::class, 'destroy']) + ->name('semesters.destroy'); + +}); diff --git a/routes/web.php b/routes/web.php index dbd90a0..07b3f25 100644 --- a/routes/web.php +++ b/routes/web.php @@ -31,4 +31,4 @@ */ Route::group(['prefix' => 'dashboard', 'as' => 'dashboard.', 'middleware' => 'auth'], function () { includeRouteFiles(__DIR__ . '/backend/'); -}); +}); \ No newline at end of file diff --git a/scripts/deploy-dev.sh b/scripts/deploy-dev.sh new file mode 100644 index 0000000..db1009d --- /dev/null +++ b/scripts/deploy-dev.sh @@ -0,0 +1,41 @@ +#! /bin/bash + +echo "Running: Down the site for maintenance" +php artisan down --refresh=30 --render='errors::503' + +echo "Running: Update the branch with latest" +git pull + +echo "Running: composer update in dev-mode" +composer update + +echo "Running: composer install in dev-mode" +composer install + +echo "Running: pnpm install" +pnpm install + +echo "Running: pnpm run prod" +pnpm run dev + +# Not running. Should check on CI/CD level +# echo "Running: Unit test" +# touch database/database.sqlite +# php artisan test + +echo "Running: Optimizing the app with caching" +php artisan config:cache +php artisan route:cache +php artisan view:cache + +# Not run in dev mode. Must manually done +# echo "Running: Setting permissions" +# sudo chown -R www-data:www-data ./ +# sudo find ./ -type f -exec chmod 644 {} \; +# sudo find ./ -type d -exec chmod 755 {} \; + +echo "Running: Restarting the queue" +php artisan queue:restart + +echo "Running: Disable the maintenance mode" +php artisan up \ No newline at end of file diff --git a/scripts/deploy-prod.sh b/scripts/deploy-prod.sh new file mode 100644 index 0000000..76dd01c --- /dev/null +++ b/scripts/deploy-prod.sh @@ -0,0 +1,49 @@ +#! /bin/bash + +echo "Running: Down the site for maintenance" +php artisan down --refresh=30 --render='errors::503' + +echo "Running: Update the branch with latest" +sudo git pull + +echo "Running: composer install in prod-mode" +composer install --optimize-autoloader --no-dev +composer dump-autoload + +echo "Running: pnpm install" +pnpm install + +echo "Running: pnpm run prod" +pnpm run prod + +echo "Running: migrate the database (no seed)" +php artisan migrate + +# Not running. Should check on CI/CD level +# echo "Running: Unit test" +# touch database/database.sqlite +# php artisan test + +echo "Running: Optimizing the app with caching" +php artisan config:cache +php artisan route:cache +php artisan view:cache + +echo "Running: Setting permissions" +sudo chown -R www-data:www-data ./ + +# Set directory permissions to 755 (rwxr-xr-x) +sudo find ./ -type d -exec chmod 755 {} \; + +# Set file permissions to 644 (rw-r--r--) +sudo find ./ -type f -exec chmod 644 {} \; + +# Ensure storage and cache directories are writable +sudo chmod -R 775 ./storage +sudo chmod -R 775 ./bootstrap/cache + +echo "Running: Restarting the queue" +php artisan queue:restart + +echo "Running: Disable the maintenance mode" +php artisan up \ No newline at end of file diff --git a/scripts/serve.bat b/scripts/serve.bat deleted file mode 100644 index 09a69f9..0000000 --- a/scripts/serve.bat +++ /dev/null @@ -1 +0,0 @@ -php artisan serve \ No newline at end of file diff --git a/scripts/serve.sh b/scripts/serve.sh index 8b0d6da..1cd34a6 100644 --- a/scripts/serve.sh +++ b/scripts/serve.sh @@ -1,3 +1,3 @@ #!/bin/sh -php artisan serve \ No newline at end of file +php artisan serve --host=0.0.0.0 --port=8000 \ No newline at end of file diff --git a/scripts/setup.bat b/scripts/setup.bat deleted file mode 100644 index 1fef5e4..0000000 --- a/scripts/setup.bat +++ /dev/null @@ -1,29 +0,0 @@ -@echo off -cd .. -copy .env.example .env -@echo on - -echo "Running : composer install" -composer install - -echo "Running : composer update" -composer update - -echo "Running : npm install" -npm install - -echo "Running : npm run dev" -npm run dev - -echo "Running : php artisan storage:link" -php artisan storage:link - -echo "Running : php artisan migrate" -php artisan migrate - -echo "Running : php artisan migrate:fresh --seed" -php artisan migrate:fresh --seed - -php artisan storage:link - -php artisan key:generate \ No newline at end of file diff --git a/scripts/setup.sh b/scripts/setup.sh index 5b331e9..6316da1 100644 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -6,17 +6,14 @@ composer install echo "Running : composer update" composer update -echo "Running : npm install" -npm install +echo "Running : pnpm install" +pnpm install -echo "Running : npm run dev" -npm run dev +echo "Running : pnpm run dev" +pnpm run dev echo "Running : php artisan storage:link" php artisan storage:link -echo "Running : php artisan migrate" -php artisan migrate - echo "Running : php artisan migrate:fresh --seed" php artisan migrate:fresh --seed diff --git a/scripts/up.sh b/scripts/up.sh new file mode 100644 index 0000000..3458605 --- /dev/null +++ b/scripts/up.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +echo "Running: Disable the maintenance mode" +php artisan up \ No newline at end of file diff --git a/scripts/update.bat b/scripts/update.bat deleted file mode 100644 index b922255..0000000 --- a/scripts/update.bat +++ /dev/null @@ -1,9 +0,0 @@ -REM Update PHP dependencies -composer update - -REM Update NodeJS dependencies and compile -npm install -npm run dev - -REM Migrate the DB into latest status -php artisan migrate \ No newline at end of file diff --git a/scripts/update.sh b/scripts/update.sh index 870a857..fdeadb9 100644 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -4,8 +4,8 @@ composer update # Update NodeJS dependencies and compile -npm install -npm run dev +pnpm install +pnpm run dev # Migrate the DB into latest status php artisan migrate \ No newline at end of file diff --git a/tests/Feature/Backend/Courses/CourseTest.php b/tests/Feature/Backend/Courses/CourseTest.php new file mode 100644 index 0000000..e5d1c53 --- /dev/null +++ b/tests/Feature/Backend/Courses/CourseTest.php @@ -0,0 +1,149 @@ +loginAsCourseManager(); + $this->get('/dashboard/courses/')->assertOk(); + } + + /** @test */ + public function a_course_manager_can_access_the_create_course_page() + { + $this->loginAsCourseManager(); + $this->get('/dashboard/courses/create')->assertOk(); + } + + /** @test */ + public function a_course_can_be_created_via_livewire() + { + $this->loginAsCourseManager(); + + $semester = Semester::factory()->create(); + + Livewire::test(\App\Http\Livewire\Backend\CreateCourses::class) + ->set('academicProgram', 'undergraduate') + ->set('semester', (string) $semester->id) + ->set('version', '1') + ->set('type', 'Core') + ->set('code', 'CS101') + ->set('name', 'Introduction to Computer Science') + ->set('credits', 3) + ->set('content', 'Basic concepts of computer science.') + ->set('objectives', 'Learn the basics of computer science') + ->set('time_allocation', ['lecture' => 3, 'tutorial' => 1, 'practical' => 1]) + ->set('marks_allocation', ['practicals' => 20, 'mid_exam' => 30, 'end_exam' => 50]) + ->set('ilos', ['knowledge' => ['Understand basic algorithms'], 'skills' => ['Implement basic programs']]) + ->set('references', ['Introduction to Algorithms']) + ->call('submit') + ->assertHasNoErrors(); + + $this->assertDatabaseHas('courses', [ + 'code' => 'CS101', + 'name' => 'Introduction to Computer Science', + ]); + } + + /** @test */ + public function a_course_can_be_updated_via_livewire() + { + $this->loginAsCourseManager(); + $course = Course::factory()->create(); + + Livewire::test(\App\Http\Livewire\Backend\CreateCourses::class) + ->set('academicProgram', $course->academic_program) + ->set('semester', (string) $course->semester_id) + ->set('version', (string) $course->version) + ->set('type', $course->type) + ->set('code', 'CS102') + ->set('name', 'Advanced Computer Science') + ->set('credits', 3) + ->set('content', 'Advanced topics in computer science.') + ->set('objectives', 'Learn advanced topics') + ->set('time_allocation', ['lecture' => 3, 'tutorial' => 1, 'practical' => 2]) + ->set('marks_allocation', ['practicals' => 30, 'mid_exam' => 20, 'end_exam' => 50]) + ->set('ilos', ['knowledge' => ['Understand advanced algorithms']]) + ->set('references', ['Advanced Algorithms']) + ->call('submit') + ->assertHasNoErrors(); + + $this->assertDatabaseHas('courses', [ + 'code' => 'CS102', + 'name' => 'Advanced Computer Science', + ]); + } + + /** @test */ + public function a_course_can_be_deleted() + { + $this->loginAsCourseManager(); + $course = Course::factory()->create(); + + $response = $this->delete('/dashboard/courses/' . $course->id); + + $response->assertRedirect('/dashboard/courses'); + $this->assertDatabaseMissing('courses', ['id' => $course->id]); + } + + /** @test */ + public function unauthorized_user_cannot_access_course_pages() + { + $course = Course::factory()->create(); + + $this->get('/dashboard/courses/')->assertRedirect('/login'); + $this->get('/dashboard/courses/create')->assertRedirect('/login'); + $this->post('/dashboard/courses')->assertRedirect('/login'); + $this->put("/dashboard/courses/{$course->id}")->assertRedirect('/login'); + $this->delete('/dashboard/courses/' . $course->id)->assertRedirect('/login'); + } + + /** @test */ + public function store_course_requires_valid_data() + { + $this->loginAsCourseManager(); + + Livewire::test(\App\Http\Livewire\Backend\CreateCourses::class) + ->set('academicProgram', '') + ->set('semester', '') + ->set('version', '') + ->set('type', '') + ->set('code', '') + ->set('name', '') + ->set('credits', '') + ->call('submit') + ->assertHasErrors(['academicProgram', 'semester', 'version', 'type', 'code', 'name', 'credits']); + } + + /** @test */ + public function update_course_requires_valid_data() + { + $this->loginAsCourseManager(); + $course = Course::factory()->create(); + + Livewire::test(\App\Http\Livewire\Backend\CreateCourses::class) + ->set('academicProgram', '') + ->set('semester', '') + ->set('version', '') + ->set('type', '') + ->set('code', '') + ->set('name', '') + ->set('credits', '') + ->call('submit') + ->assertHasErrors(['academicProgram', 'semester', 'version', 'type', 'code', 'name', 'credits']); + } +} \ No newline at end of file diff --git a/tests/Feature/Backend/DashboardTest.php b/tests/Feature/Backend/DashboardTest.php index 4a2e773..5e7835e 100644 --- a/tests/Feature/Backend/DashboardTest.php +++ b/tests/Feature/Backend/DashboardTest.php @@ -25,18 +25,13 @@ public function all_users_can_access_admin_dashboard() $this->actingAs(User::factory()->user()->create()); $response = $this->get('/dashboard/home'); - - // $response->assertRedirect('/'); - $response->assertStatus(200); // As all auth users can access the dashboard - - // $response->assertSessionHas('flash_danger', __('You do not have access to do that.')); + $response->assertStatus(200); } /** @test */ public function admin_can_access_admin_dashboard() { $this->loginAsAdmin(); - $this->get('/dashboard/home')->assertOk(); } } diff --git a/tests/Feature/Backend/Event/EventTest.php b/tests/Feature/Backend/Event/EventTest.php index 96ac5b6..a6da325 100644 --- a/tests/Feature/Backend/Event/EventTest.php +++ b/tests/Feature/Backend/Event/EventTest.php @@ -44,7 +44,7 @@ public function event_can_be_created() $response = $this->post('/dashboard/events/', [ 'title' => 'test event', 'description' => 'Nostrum qui qui ut deserunt dolores quaerat. Est quos sed ea quo placeat maxime. Sequi temporibus alias atque assumenda facere modi deleniti. Recusandae autem quia officia iste laudantium veritatis aut.', - 'url' => "test-event", + 'image' => 'sample-image.jpg', 'created_by' => $user->id, 'link_url' => 'http://runolfsdottir.biz/quia-provident-ut-ipsa-atque-et', @@ -72,7 +72,7 @@ public function event_can_be_updated() $updateData = [ 'title' => 'Updated Event', 'description' => 'This is an updated event description.', - 'url' => "test-event", + 'image' => 'sample-image.jpg', 'created_by' => $user->id, 'link_url' => 'http://example.com', diff --git a/tests/Feature/Backend/Semesters/SemesterTest.php b/tests/Feature/Backend/Semesters/SemesterTest.php new file mode 100644 index 0000000..4ad7e8b --- /dev/null +++ b/tests/Feature/Backend/Semesters/SemesterTest.php @@ -0,0 +1,169 @@ +loginAsCourseManager(); + $this->get('/dashboard/semesters/')->assertOk(); + } + + /** @test */ + public function a_course_manager_can_access_the_create_semester_page() + { + $this->loginAsCourseManager(); + $this->get('/dashboard/semesters/create')->assertOk(); + } + + /** @test */ + public function a_course_manager_can_access_the_delete_semester_page() + { + $this->loginAsCourseManager(); + $semester = Semester::factory()->create(); + $this->get('/dashboard/semesters/delete/' . $semester->id)->assertOk(); + } + + /** @test */ + public function semester_can_be_created() + { + $this->loginAsCourseManager(); + $response = $this->post('/dashboard/semesters', [ + 'title' => 'Test Semester 1', + 'version' => 1, + 'academic_program' => 'undergraduate', + 'description' => 'Description of Semester 1', + 'url' => '/semester-1', + ]); + + $response->assertStatus(302); + $this->assertDatabaseHas('semesters', [ + 'title' => 'Test Semester 1', + ]); + } + /** @test */ + public function semester_can_be_updated() + { + $this->loginAsCourseManager(); + $semester = Semester::factory()->create(); + + $updateData = [ + 'title' => 'Test Semester 2', + 'version' => 2, + 'academic_program' => 'postgraduate', + 'description' => 'Updated description', + 'url' => '/semester-2', + ]; + + $response = $this->put("/dashboard/semesters/{$semester->id}", $updateData); + $response->assertStatus(302); + + $this->assertDatabaseHas('semesters', [ + 'title' => 'Test Semester 2', + ]); + } + + /** @test */ + public function semester_can_be_deleted() + { + $this->loginAsCourseManager(); + $semester = Semester::factory()->create(); + $this->delete('/dashboard/semesters/' . $semester->id); + $this->assertDatabaseMissing('semesters', ['id' => $semester->id]); + } + + /** @test */ + public function semester_can_not_be_deleted_if_courses_already_associated() + { + $this->loginAsCourseManager(); + $semester = Semester::factory()->create(); + $course = Course::factory()->create(); + $course->semester_id = $semester->id; + $course->save(); + + $response = $this->delete('/dashboard/semesters/' . $semester->id); + $response->assertSessionHasErrors(); + } + + /** @test */ + public function semester_url_must_be_unique() + { + $this->loginAsCourseManager(); + + Semester::factory()->create([ + 'url' => '/unique-url' + ]); + + $response = $this->post('/dashboard/semesters', [ + 'title' => 'Test Semester 2', + 'version' => 1, + 'academic_program' => 'undergraduate', + 'description' => 'Description of Semester 2', + 'url' => '/unique-url', + ]); + + $response->assertSessionHasErrors('url'); + } + + /** @test */ + public function semester_must_have_valid_academic_program() + { + $this->loginAsCourseManager(); + + $response = $this->post('/dashboard/semesters', [ + 'title' => 'Test Semester 3', + 'version' => 1, + 'academic_program' => 'InvalidProgram', + 'description' => 'Description of Semester 3', + 'url' => '/valid-url', + ]); + + $response->assertSessionHasErrors('academic_program'); + } + + /** @test */ + public function semester_can_be_updated_with_unique_url() + { + $this->loginAsCourseManager(); + + $semester = Semester::factory()->create([ + 'url' => '/old-url' + ]); + + Semester::factory()->create([ + 'url' => '/existing-url' + ]); + + $response = $this->put("/dashboard/semesters/{$semester->id}", [ + 'title' => 'Updated Semester', + 'version' => 1, + 'academic_program' => 'undergraduate', + 'description' => 'Updated description', + 'url' => '/existing-url', + ]); + + $response->assertSessionHasErrors('url'); + } + + /** @test */ + public function unauthorized_user_cannot_access_semester_pages() + { + $semester = Semester::factory()->create(); + + $this->get('/dashboard/semesters/')->assertRedirect('/login'); + $this->get('/dashboard/semesters/create')->assertRedirect('/login'); + $this->get('/dashboard/semesters/delete/' . $semester->id)->assertRedirect('/login'); + $this->post('/dashboard/semesters')->assertRedirect('/login'); + $this->put("/dashboard/semesters/{$semester->id}")->assertRedirect('/login'); + $this->delete('/dashboard/semesters/' . $semester->id)->assertRedirect('/login'); + } +} \ No newline at end of file diff --git a/tests/Feature/Frontend/RegistrationTest.php b/tests/Feature/Frontend/RegistrationTest.php index 0399c2e..98a9edf 100644 --- a/tests/Feature/Frontend/RegistrationTest.php +++ b/tests/Feature/Frontend/RegistrationTest.php @@ -29,11 +29,11 @@ public function registration_requires_validation() /** @test */ public function email_must_be_unique() { - User::factory()->create(['email' => 'john@example.com']); + User::factory()->create(['email' => 'johndoe@eng.pdn.ac.lk']); $response = $this->post('/register', [ 'name' => 'John Doe', - 'email' => 'john@example.com', + 'email' => 'johndoe@eng.pdn.ac.lk', 'password' => 'password', 'password_confirmation' => 'password', ]); @@ -46,7 +46,7 @@ public function password_must_be_confirmed() { $response = $this->post('/register', [ 'name' => 'John Doe', - 'email' => 'john@example.com', + 'email' => 'johndoe@eng.pdn.ac.lk', 'password' => 'password', ]); @@ -58,7 +58,7 @@ public function passwords_must_be_equivalent() { $response = $this->post('/register', [ 'name' => 'John Doe', - 'email' => 'john@example.com', + 'email' => 'johndoe@eng.pdn.ac.lk', 'password' => 'password', 'password_confirmation' => 'not_the_same', ]); @@ -79,14 +79,14 @@ public function a_user_can_register_an_account() { $this->post('/register', [ 'name' => 'John Doe', - 'email' => 'john@example.com', + 'email' => 'johndoe@eng.pdn.ac.lk', 'password' => 'OC4Nzu270N!QBVi%U%qX', 'password_confirmation' => 'OC4Nzu270N!QBVi%U%qX', 'terms' => '1', ])->assertRedirect(route(homeRoute())); $user = resolve(UserService::class) - ->where('email', 'john@example.com') + ->where('email', 'johndoe@eng.pdn.ac.lk') ->firstOrFail(); $this->assertSame($user->name, 'John Doe'); @@ -98,11 +98,11 @@ public function a_user_cant_register_an_account_if_they_dont_accept_the_terms() { $response = $this->post('/register', [ 'name' => 'John Doe', - 'email' => 'john@example.com', + 'email' => 'johndoe@eng.pdn.ac.lk', 'password' => 'OC4Nzu270N!QBVi%U%qX', 'password_confirmation' => 'OC4Nzu270N!QBVi%U%qX', ]); $response->assertSessionHasErrors(['terms']); } -} +} \ No newline at end of file diff --git a/tests/Feature/Services/DepartmentDataServiceTest.php b/tests/Feature/Services/DepartmentDataServiceTest.php new file mode 100644 index 0000000..aeb430a --- /dev/null +++ b/tests/Feature/Services/DepartmentDataServiceTest.php @@ -0,0 +1,17 @@ +assertTrue($api->isInternalEmail('nuwanjaliyagoda@eng.pdn.ac.lk')); + $this->assertFalse($api->isInternalEmail('user@example.com')); + } +} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index 18c1a74..4c5cb2d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -59,6 +59,16 @@ protected function loginAsEditor() return $user; } + protected function loginAsCourseManager() + { + $courseManagerRole = Role::where('name', 'Course Manager')->first(); + $user = User::factory()->user()->create(['name' => 'Test Course Manager']); + $user->assignRole($courseManagerRole->name); + $this->actingAs($user); + + return $user; + } + protected function logout() { diff --git a/yarn.lock b/yarn.lock index e8f7704..3999e87 100644 --- a/yarn.lock +++ b/yarn.lock @@ -217,15 +217,15 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-string-parser@^7.21.5": - version "7.21.5" - resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz" - integrity sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w== +"@babel/helper-string-parser@^7.24.8": + version "7.24.8" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz" + integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== -"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": - version "7.19.1" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz" - integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1", "@babel/helper-validator-identifier@^7.24.7": + version "7.24.7" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz" + integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== "@babel/helper-validator-option@^7.21.0": version "7.21.0" @@ -260,10 +260,12 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.18.4", "@babel/parser@^7.20.7", "@babel/parser@^7.21.9", "@babel/parser@^7.22.0", "@babel/parser@^7.22.4": - version "7.22.4" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.22.4.tgz" - integrity sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA== +"@babel/parser@^7.1.0", "@babel/parser@^7.18.4", "@babel/parser@^7.20.7", "@babel/parser@^7.21.9", "@babel/parser@^7.22.0", "@babel/parser@^7.22.4", "@babel/parser@^7.25.3": + version "7.25.6" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz" + integrity sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q== + dependencies: + "@babel/types" "^7.25.6" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" @@ -971,13 +973,13 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.22.0", "@babel/types@^7.22.3", "@babel/types@^7.22.4", "@babel/types@^7.4.4": - version "7.22.4" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.22.4.tgz" - integrity sha512-Tx9x3UBHTTsMSW85WB2kphxYQVvrZ/t1FxD88IpSgIjiUJlCm9z+xWIDwyo1vffTwSqteqyznB8ZE9vYYk16zA== +"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.22.0", "@babel/types@^7.22.3", "@babel/types@^7.22.4", "@babel/types@^7.25.6", "@babel/types@^7.4.4": + version "7.25.6" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz" + integrity sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw== dependencies: - "@babel/helper-string-parser" "^7.21.5" - "@babel/helper-validator-identifier" "^7.19.1" + "@babel/helper-string-parser" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" to-fast-properties "^2.0.0" "@coreui/coreui@^3.0.0": @@ -995,6 +997,11 @@ resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== +"@formkit/auto-animate@^1.0.0-beta.3": + version "1.0.0-pre-alpha.3" + resolved "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-1.0.0-pre-alpha.3.tgz" + integrity sha512-lMVZ3LFUIu0RIxCEwmV8nUUJQ46M2bv2NDU3hrhZivViuR1EheC8Mj5sx/ACqK5QLK8XB8z7GDIZBUGdU/9OZQ== + "@fortawesome/fontawesome-free@^5.12.1": version "5.15.3" resolved "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz" @@ -1027,10 +1034,10 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.15" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.0" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== "@jridgewell/sourcemap-codec@1.4.14": version "1.4.14" @@ -1050,6 +1057,13 @@ resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== +"@marcreichel/alpine-auto-animate@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@marcreichel/alpine-auto-animate/-/alpine-auto-animate-1.1.0.tgz" + integrity sha512-ulKU3TAmZ/YkpO34j+tpGFSNSyvXAcprWkO6laFuLODx28zfJBi61TPkD3Tw9BQAl57jL91yybMFhRT3VHRPlQ== + dependencies: + "@formkit/auto-animate" "^1.0.0-beta.3" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -1326,6 +1340,25 @@ dependencies: "@types/node" "*" +"@vue/compiler-core@3.5.8": + version "3.5.8" + resolved "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.8.tgz" + integrity sha512-Uzlxp91EPjfbpeO5KtC0KnXPkuTfGsNDeaKQJxQN718uz+RqDYarEf7UhQJGK+ZYloD2taUbHTI2J4WrUaZQNA== + dependencies: + "@babel/parser" "^7.25.3" + "@vue/shared" "3.5.8" + entities "^4.5.0" + estree-walker "^2.0.2" + source-map-js "^1.2.0" + +"@vue/compiler-dom@3.5.8": + version "3.5.8" + resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.8.tgz" + integrity sha512-GUNHWvoDSbSa5ZSHT9SnV5WkStWfzJwwTd6NMGzilOE/HM5j+9EB9zGXdtu/fCNEmctBqMs6C9SvVPpVPuk1Eg== + dependencies: + "@vue/compiler-core" "3.5.8" + "@vue/shared" "3.5.8" + "@vue/compiler-sfc@2.7.14": version "2.7.14" resolved "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz" @@ -1335,6 +1368,29 @@ postcss "^8.4.14" source-map "^0.6.1" +"@vue/compiler-sfc@3.5.8": + version "3.5.8" + resolved "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.8.tgz" + integrity sha512-taYpngQtSysrvO9GULaOSwcG5q821zCoIQBtQQSx7Uf7DxpR6CIHR90toPr9QfDD2mqHQPCSgoWBvJu0yV9zjg== + dependencies: + "@babel/parser" "^7.25.3" + "@vue/compiler-core" "3.5.8" + "@vue/compiler-dom" "3.5.8" + "@vue/compiler-ssr" "3.5.8" + "@vue/shared" "3.5.8" + estree-walker "^2.0.2" + magic-string "^0.30.11" + postcss "^8.4.47" + source-map-js "^1.2.0" + +"@vue/compiler-ssr@3.5.8": + version "3.5.8" + resolved "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.8.tgz" + integrity sha512-W96PtryNsNG9u0ZnN5Q5j27Z/feGrFV6zy9q5tzJVyJaLiwYxvC0ek4IXClZygyhjm+XKM7WD9pdKi/wIRVC/Q== + dependencies: + "@vue/compiler-dom" "3.5.8" + "@vue/shared" "3.5.8" + "@vue/component-compiler-utils@^3.1.0": version "3.2.1" resolved "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.2.1.tgz" @@ -1350,6 +1406,56 @@ optionalDependencies: prettier "^1.18.2" +"@vue/reactivity@~3.1.1": + version "3.1.5" + resolved "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz" + integrity sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg== + dependencies: + "@vue/shared" "3.1.5" + +"@vue/reactivity@3.5.8": + version "3.5.8" + resolved "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.8.tgz" + integrity sha512-mlgUyFHLCUZcAYkqvzYnlBRCh0t5ZQfLYit7nukn1GR96gc48Bp4B7OIcSfVSvlG1k3BPfD+p22gi1t2n9tsXg== + dependencies: + "@vue/shared" "3.5.8" + +"@vue/runtime-core@3.5.8": + version "3.5.8" + resolved "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.8.tgz" + integrity sha512-fJuPelh64agZ8vKkZgp5iCkPaEqFJsYzxLk9vSC0X3G8ppknclNDr61gDc45yBGTaN5Xqc1qZWU3/NoaBMHcjQ== + dependencies: + "@vue/reactivity" "3.5.8" + "@vue/shared" "3.5.8" + +"@vue/runtime-dom@3.5.8": + version "3.5.8" + resolved "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.8.tgz" + integrity sha512-DpAUz+PKjTZPUOB6zJgkxVI3GuYc2iWZiNeeHQUw53kdrparSTG6HeXUrYDjaam8dVsCdvQxDz6ZWxnyjccUjQ== + dependencies: + "@vue/reactivity" "3.5.8" + "@vue/runtime-core" "3.5.8" + "@vue/shared" "3.5.8" + csstype "^3.1.3" + +"@vue/server-renderer@3.5.8": + version "3.5.8" + resolved "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.8.tgz" + integrity sha512-7AmC9/mEeV9mmXNVyUIm1a1AjUhyeeGNbkLh39J00E7iPeGks8OGRB5blJiMmvqSh8SkaS7jkLWSpXtxUCeagA== + dependencies: + "@vue/compiler-ssr" "3.5.8" + "@vue/shared" "3.5.8" + +"@vue/shared@3.1.5": + version "3.1.5" + resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz" + integrity sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA== + +"@vue/shared@3.5.8": + version "3.5.8" + resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.5.8.tgz" + integrity sha512-mJleSWbAGySd2RJdX1RBtcrUBX6snyOc0qHpgk3lGi4l9/P/3ny3ELqFWqYdkXIwwNN/kdm8nD9ky8o6l/Lx2A== + "@webassemblyjs/ast@^1.11.5", "@webassemblyjs/ast@1.11.6": version "1.11.6" resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz" @@ -1573,10 +1679,12 @@ ajv@^8.8.2, ajv@^8.9.0: require-from-string "^2.0.2" uri-js "^4.2.2" -alpinejs@^2.3.5: - version "2.8.2" - resolved "https://registry.npmjs.org/alpinejs/-/alpinejs-2.8.2.tgz" - integrity sha512-5yOUtckn4CBp0qsHpo2qgjZyZit84uXvHbB7NJ27sn4FA6UlFl2i9PGUAdTXkcbFvvxDJBM+zpOD8RuNYFvQAw== +alpinejs@^3.13.3: + version "3.14.1" + resolved "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.1.tgz" + integrity sha512-ICar8UsnRZAYvv/fCNfNeKMXNoXGUfwHrjx7LqXd08zIP95G2d9bAOuaL97re+1mgt/HojqHsfdOLo/A5LuWgQ== + dependencies: + "@vue/reactivity" "~3.1.1" ansi-escapes@^4.3.1: version "4.3.2" @@ -2450,10 +2558,10 @@ csso@^4.2.0: dependencies: css-tree "^1.1.2" -csstype@^3.1.0: - version "3.1.2" - resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz" - integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== +csstype@^3.1.0, csstype@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== d@^1.0.1, d@1: version "1.0.1" @@ -2673,6 +2781,11 @@ entities@^2.0.0: resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== +entities@^4.5.0: + version "4.5.0" + resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + envinfo@^7.7.3: version "7.8.1" resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz" @@ -2756,6 +2869,11 @@ estraverse@^5.2.0: resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz" integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + esutils@^2.0.2: version "2.0.3" resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" @@ -3686,6 +3804,13 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +magic-string@^0.30.11: + version "0.30.11" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz" + integrity sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" @@ -3847,10 +3972,10 @@ multicast-dns@^7.2.5: dns-packet "^5.2.2" thunky "^1.0.2" -nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== negotiator@0.6.3: version "0.6.3" @@ -4147,10 +4272,10 @@ perfect-scrollbar@^1.5.0: resolved "https://registry.npmjs.org/perfect-scrollbar/-/perfect-scrollbar-1.5.1.tgz" integrity sha512-MrSImINnIh3Tm1hdPT6bji6fmIeRorVEegQvyUnhqko2hDGTHhmjPefHXfxG/Jb8xVbfCwgmUIlIajERGXjVXQ== -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.0, picocolors@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz" + integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" @@ -4424,14 +4549,14 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -"postcss@^7.0.0 || ^8.0.1", postcss@^8.0.9, postcss@^8.1, postcss@^8.1.0, postcss@^8.2.15, postcss@^8.2.2, postcss@^8.3.11, postcss@^8.4.14, postcss@>=8.0.9: - version "8.4.24" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz" - integrity sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg== +"postcss@^7.0.0 || ^8.0.1", postcss@^8.0.9, postcss@^8.1, postcss@^8.1.0, postcss@^8.2.15, postcss@^8.2.2, postcss@^8.3.11, postcss@^8.4.14, postcss@^8.4.47, postcss@>=8.0.9: + version "8.4.47" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz" + integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" + nanoid "^3.3.7" + picocolors "^1.1.0" + source-map-js "^1.2.1" postcss@7.0.36: version "7.0.36" @@ -5030,10 +5155,10 @@ source-list-map@^2.0.0: resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== -source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map-js@^1.2.0, source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== source-map-resolve@^0.5.2: version "0.5.3" @@ -5475,6 +5600,17 @@ vue@^2.7.14: "@vue/compiler-sfc" "2.7.14" csstype "^3.1.0" +vue@^3.0.0, vue@3.5.8: + version "3.5.8" + resolved "https://registry.npmjs.org/vue/-/vue-3.5.8.tgz" + integrity sha512-hvuvuCy51nP/1fSRvrrIqTLSvrSyz2Pq+KQ8S8SXCxTWVE0nMaOnSDnSOxV1eYmGfvK7mqiwvd1C59CEEz7dAQ== + dependencies: + "@vue/compiler-dom" "3.5.8" + "@vue/compiler-sfc" "3.5.8" + "@vue/runtime-dom" "3.5.8" + "@vue/server-renderer" "3.5.8" + "@vue/shared" "3.5.8" + watchpack@^2.4.0: version "2.4.0" resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz"