diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index 5c6beb1..0a9823b 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -19,9 +19,9 @@ jobs: - name: Install run: ./deploy.sh - name: Create Database - run: docker exec -t notar_app_1 php artisan migrate + run: vendor/bin/sail artisan migrate - name: Create admin user - run: docker exec -t notar_app_1 php artisan migrate:admin novasenha + run: vendor/bin/sail artisan migrate:admin novasenha - name: Cypress run uses: cypress-io/github-action@v6 with: diff --git a/app/Http/Controllers/CursoController.php b/app/Http/Controllers/CursoController.php new file mode 100644 index 0000000..fed819b --- /dev/null +++ b/app/Http/Controllers/CursoController.php @@ -0,0 +1,96 @@ +with('turmas'); + + return View('curso.index')->with('cursos', $cursos->get()) + ->with('semCurso', $semCurso->get()); + } + + /** + * Show the form for creating a new resource. + */ + public function create() + { + return View('curso.create'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $this->authorize('create', Curso::class); + $rules = array( + 'name' => 'required', + ); + $data = $request->validate($rules); + + // store + $curso = tap(new Curso($data))->save(); + + // redirect to show + return redirect()->action([get_class($this), 'show'], ['curso' => $curso]); + } + + /** + * Display the specified resource. + */ + public function show(Curso $curso) + { + // index is the same for turma and curso + return redirect()->action([get_class($this), 'index']); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(Curso $curso) + { + $this->authorize('edit', $curso); + return View('curso.edit')->with('curso', $curso); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Curso $curso) + { + $this->authorize('edit', $curso); + + $rules = [ + 'name' => 'required', + ]; + $data = $request->validate($rules); + $curso->update(['name' => $data['name']]); + return redirect()->action([get_class($this), 'index']); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Curso $curso) + { + $this->authorize('delete', $curso); + // remove turmas + $curso->turmas()->detach(); + + $curso->delete(); + return redirect()->action([get_class($this), 'index']); + } + +} diff --git a/app/Http/Controllers/ExercicioController.php b/app/Http/Controllers/ExercicioController.php index 1197e95..c5f5790 100644 --- a/app/Http/Controllers/ExercicioController.php +++ b/app/Http/Controllers/ExercicioController.php @@ -30,19 +30,19 @@ class ExercicioController extends Controller */ public function index() { - $topicos = Topico::orderBy('order'); - $semTopico = Exercicio::whereDoesntHave('topico'); - /** @var \App\Models\User */ - $user = Auth::user(); - if (optional($user)->isAdmin()) { - $topicos = $topicos->with('exercicios'); - } else { - $topicos = $topicos->with('exerciciosPublished'); - $semTopico = $semTopico->published(); - } - - return View('exercicio.index')->with('topicos', $topicos->get()) - ->with('semTopico', $semTopico->get()); + $topicos = Topico::orderBy('order'); + $semTopico = Exercicio::whereDoesntHave('topico'); + /** @var \App\Models\User */ + $user = Auth::user(); + if (optional($user)->isAdmin()) { + $topicos = $topicos->with('exercicios'); + } else { + $topicos = $topicos->with('exerciciosPublished'); + $semTopico = $semTopico->published(); + } + + return View('exercicio.index')->with('topicos', $topicos->get()) + ->with('semTopico', $semTopico->get()); } /** @@ -59,12 +59,12 @@ public function create() } /** - * Store a newly created resource in storage. + * Validate model * * @param \Illuminate\Http\Request $request * @return mixed */ - private function validateExercicio(Request $request, Exercicio|null $exercicio) + private function validateRequest(Request $request, Exercicio|null $exercicio = null) { $rules = array( 'name' => 'required|string|unique:exercicios' . ($exercicio ? ',name,' . $exercicio->id : ''), @@ -98,7 +98,7 @@ public function store(Request $request) { $this->authorize('create', Exercicio::class); - $data = $this->validateExercicio($request, null); + $data = $this->validateRequest($request); // store $exercicio = new Exercicio($data); DB::transaction(function () use ($data, $exercicio) { @@ -196,7 +196,7 @@ private function corretoR(Exercicio $exercicio, string $file) . 'dbname <- "' . env('DB_DATABASE') . '";' . 'con <- connect(dbusr, dbpass, dbname);' . 'TRUE;' // This is to prevent connection object being returned (which causes an echo warning visible in production) - ; + ; $r = $cnx->evalString($rcode); } catch (Exception $e) { Log::error('Erro ao na conexão RMySql'); @@ -339,7 +339,7 @@ private function recebeCodigo(string $codigo, Exercicio $exercicio, \Illuminate\ } // corrigir EOL - $codigo = str_replace("\r\n", PHP_EOL, $codigo).PHP_EOL; + $codigo = str_replace("\r\n", PHP_EOL, $codigo) . PHP_EOL; // salva um arquivo com o codigo $tempfile = TmpFile::generateTmpFileName(md5($codigo), '.R'); @@ -393,7 +393,7 @@ public function edit(Exercicio $exercicio) public function update(Request $request, Exercicio $exercicio) { $this->authorize('edit', $exercicio); - $data = $this->validateExercicio($request, $exercicio); + $data = $this->validateRequest($request, $exercicio); // store DB::transaction(function () use ($data, $exercicio) { diff --git a/app/Http/Controllers/TurmaController.php b/app/Http/Controllers/TurmaController.php index acfc1cc..f7f6160 100644 --- a/app/Http/Controllers/TurmaController.php +++ b/app/Http/Controllers/TurmaController.php @@ -6,14 +6,45 @@ use Illuminate\Http\Request; use Illuminate\View\View; use App\Models\Turma; +use App\Models\Curso; use App\Models\User; use App\Models\Exercicio; use App\Models\Prazo; use App\Rules\CsvRule; use App\Utils\Csv; +use Illuminate\Support\Facades\DB; +use Carbon\Carbon; class TurmaController extends Controller { + /** + * Validate model input + * + * @param \Illuminate\Http\Request $request + * @return mixed + */ + private function validateRequest(Request $request, Turma|null $model = null) + { + $rules = [ + 'name' => 'required|unique:turmas' . ($model ? ',name,' . $model->id : ''), + 'description'=> 'required', + 'curso_id' => 'sometimes|int|exists:cursos,id|nullable', + 'maillist' => [ + 'file', + new CsvRule([ + 'name' => 'required', + 'email' => 'required|email' + ]) + ], + 'defaultpassword' => 'required_with:maillist', + 'copyfrom' => 'int|exists:turmas,id|nullable', + 'datainicial' => 'date|nullable' + ]; + + $data = $request->validate($rules); + return ['name' => $data['name'], 'description' => $data['description'], 'curso_id' => $data['curso_id']]; + } + /** * Display a listing of the resource. * @@ -21,18 +52,20 @@ class TurmaController extends Controller */ public function index() { - $this->authorize('list', Turma::class); - return View('turma.index')->with('turmas',Turma::all()); + return redirect()->action([CursoController::class, 'index']); } /** * Show the form for creating a new resource. * - * @return \Illuminate\Http\Response + * @return View */ public function create() { - return View('turma.create'); + $this->authorize('create', Turma::class); + return View('turma.create') + ->with('turmas', Turma::all()) + ->with('cursos', Curso::all()); } /** @@ -44,14 +77,37 @@ public function create() public function store(Request $request) { $this->authorize('create', Turma::class); - $rules = array( - 'name' => 'required', - 'description'=> 'required', - ); - $data = $request->validate($rules); + $data = $this->validateRequest($request); + DB::beginTransaction(); // store $turma = tap(new Turma($data))->save(); + + // bulk add users + if ($request->maillist ?? "") { + $this->bulkAddUsers($turma, + new Csv($request->maillist->get()), + $request->defaultpassword); + } + + // copy prazos from another turma + if ($request->copyfrom ?? "") { + $original = Turma::find($request->copyfrom); + if ($request->datainicial ?? "") { + $original_firstdate = Carbon::parse($original->prazos()->min('prazo')); + $new_firstdate = Carbon::parse($request->datainicial); + $date_shift = $original_firstdate->diffInDays($new_firstdate); + } + foreach ($original->prazos as $prazo) { + $newprazo = $prazo->replicate(); + if ($request->datainicial ?? "") { + $newprazo->shiftBy($date_shift); + } + $turma->prazos()->save($newprazo); + } + } + DB::commit(); + return redirect()->action([get_class($this),'show'], ['turma' => $turma]); } @@ -82,19 +138,33 @@ public function show(Turma $turma) * Show the form for editing the specified resource. * * @param \App\Models\Turma $turma - * @return \Illuminate\Http\Response + * @return View */ public function edit(Turma $turma) { $this->authorize('edit', $turma); - return View('turma.edit')->with('turma',$turma); + + $prazos = $turma->prazosOrdered()->get()->groupBy('futuro'); + $v = View('turma.edit')->with('turma',$turma) + ->with('cursos', Curso::all()); + + if ($prazos->isNotEmpty()) { + if ($prazos->keys()->contains(0)) { + $v = $v->with('prazosPassados',$prazos[0]); + + } + if ($prazos->keys()->contains(1)) { + $v = $v->with('prazosFuturos',$prazos[1]); + } + } + return $v; } /** * Show the form for editing the prazos * * @param \App\Models\Turma $turma - * @return \Illuminate\Http\Response + * @return View */ public function editprazos(Turma $turma) { @@ -155,43 +225,14 @@ public function updateprazos(Request $request, Turma $turma) public function update(Request $request, Turma $turma) { $this->authorize('edit',$turma); - $rules = [ - 'name' => 'required', - 'description'=> 'required', - 'maillist' => [ - 'file', - new CsvRule([ - 'name' => 'required', - 'email' => 'required|email' - ]) - ], - 'defaultpassword' => 'required_with:maillist' - ]; - $data = $request->validate($rules); - $turma->update(['name' => $data['name'],'description' => $data['description']]); + $data = $this->validateRequest($request, $turma); + $turma->update($data); // bulk add users if ($request->maillist ?? "") { - $csv = new Csv($request->maillist->get()); - $pssw = $request->defaultpassword; - foreach( $csv->getData() as $user ) { - $email = $user['email']; - $name = $user['name']; - $newmember = User::where('email', $email)->first(); - if($newmember) { - // if user not in turma, add - if ($turma->users()->find($newmember) == null) { - $turma->users()->save($newmember); - } - } else { - // if user doesn't exist, create new - $newmember = User::create([ - 'email' => $email, - 'name' => $name, - 'password' => $pssw]); - $turma->users()->save($newmember); - } - } + $this->bulkAddUsers($turma, + new Csv($request->maillist->get()), + $request->defaultpassword); } return redirect()->action([get_class($this),'show'], ['turma' => $turma]); @@ -229,4 +270,26 @@ public function destroy(Turma $turma) $turma->delete(); return redirect()->action([get_class($this),'index']); } + + protected function bulkAddUsers (Turma $turma, Csv $csv, String $psswd) + { + foreach( $csv->getData() as $user ) { + $email = $user['email']; + $name = $user['name']; + $newmember = User::where('email', $email)->first(); + if($newmember) { + // if user not in turma, add + if ($turma->users()->find($newmember) == null) { + $turma->users()->save($newmember); + } + } else { + // if user doesn't exist, create new + $newmember = User::create([ + 'email' => $email, + 'name' => $name, + 'password' => $psswd]); + $turma->users()->save($newmember); + } + } + } } diff --git a/app/Models/Curso.php b/app/Models/Curso.php new file mode 100644 index 0000000..20a3df6 --- /dev/null +++ b/app/Models/Curso.php @@ -0,0 +1,22 @@ +hasMany(Turma::class); + } +} diff --git a/app/Models/Prazo.php b/app/Models/Prazo.php index 9994b3b..0336e2f 100644 --- a/app/Models/Prazo.php +++ b/app/Models/Prazo.php @@ -18,6 +18,17 @@ class Prazo extends Pivot use DefaultOrderBy; protected static $orderByColumn = 'prazo'; + public function prazoParsed() + { + return Carbon::parse($this->prazo); + } + + // shift prazo by a fixed ammount + public function shiftBy(int $days) + { + $this->prazo = $this->prazoParsed()->addDays($days); + } + // relationships public function turma() { diff --git a/app/Models/Topico.php b/app/Models/Topico.php index 74ce492..c818283 100644 --- a/app/Models/Topico.php +++ b/app/Models/Topico.php @@ -10,7 +10,6 @@ class Topico extends Model { use HasFactory; protected $table = 'topicos'; - protected $guarded = []; /** * Set a default ordering for this model diff --git a/app/Models/Turma.php b/app/Models/Turma.php index 1dcc15b..8715f7d 100644 --- a/app/Models/Turma.php +++ b/app/Models/Turma.php @@ -24,6 +24,10 @@ public function users() { return $this->belongsToMany(User::class); } + public function curso() + { + return $this->belongsTo(Curso::class); + } public function prazos() { return $this->hasMany(Prazo::class); diff --git a/app/Models/User.php b/app/Models/User.php index ee7120c..422e35b 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -71,19 +71,6 @@ public function isAdmin() { return $this->is_admin; } - /** - * Nota final (atual) dentro do prazo (get from DB) - * - * @var Prazo $prazo - * @return float - */ - public function getNotaFinal(Prazo $prazo) { - return $this->notas()-> - where('exercicio_id',$prazo->exercicio_id)-> //exercicio correto - where('created_at','<',$prazo->prazo)-> //dentro do prazo - get()->max('nota'); //maior nota - } - /** * Nota final (atual) dentro do prazo * diff --git a/app/Policies/CursoPolicy.php b/app/Policies/CursoPolicy.php new file mode 100644 index 0000000..92ad4ca --- /dev/null +++ b/app/Policies/CursoPolicy.php @@ -0,0 +1,47 @@ +isAdmin(); + } + + /** + * Determine whether the user can update the model. + * + * @param \App\Models\User $user + * @param \App\Models\Curso $curso + * @return mixed + */ + public function edit(User $user, Curso $curso) + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can delete the model. + * + * @param \App\Models\User $user + * @param \App\Models\Curso $curso + * @return mixed + */ + public function delete(User $user, Curso $curso) + { + return $user->isAdmin(); + } +} diff --git a/app/Policies/TurmaPolicy.php b/app/Policies/TurmaPolicy.php index 998e838..17945dc 100644 --- a/app/Policies/TurmaPolicy.php +++ b/app/Policies/TurmaPolicy.php @@ -33,6 +33,19 @@ public function view(User $user, Turma $turma) return true; } + /** + * Determine whether the user can view the members. + * + * @param \App\Models\User $user + * @param \App\Models\Turma $turma + * @return mixed + */ + public function viewMembers(User $user, Turma $turma) + { + // Should students be able to see other students? + return $user->isAdmin(); + } + /** * Determine whether the user can create models. * diff --git a/database/factories/CursoFactory.php b/database/factories/CursoFactory.php new file mode 100644 index 0000000..76dc47d --- /dev/null +++ b/database/factories/CursoFactory.php @@ -0,0 +1,29 @@ + substr($this->faker->sentence(2), 0, -1), + 'description' => $this->faker->paragraph, + ]; + } +} diff --git a/database/migrations/2024_07_25_122648_create_cursos_table.php b/database/migrations/2024_07_25_122648_create_cursos_table.php new file mode 100644 index 0000000..15d5259 --- /dev/null +++ b/database/migrations/2024_07_25_122648_create_cursos_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('name'); + $table->timestamps(); + }); + Schema::table('turmas', function (Blueprint $table) { + $table->foreignId('curso_id')->nullable()->constrained(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('turmas', function (Blueprint $table) { + $table->dropForeign(['curso_id']); + $table->dropColumn('curso_id'); + }); + Schema::dropIfExists('cursos'); + } +}; diff --git a/resources/views/curso/create.blade.php b/resources/views/curso/create.blade.php new file mode 100644 index 0000000..6971e71 --- /dev/null +++ b/resources/views/curso/create.blade.php @@ -0,0 +1,21 @@ +@extends('layouts.base') +@section('content') +
+
+

Criar novo curso

+
+
+
+ @csrf + @include ('includes.error_alert') +
+ + @error('name') +
{{ $message }}
+ @enderror +
+ +
+
+
+ @endsection diff --git a/resources/views/curso/edit.blade.php b/resources/views/curso/edit.blade.php new file mode 100644 index 0000000..b7c38b7 --- /dev/null +++ b/resources/views/curso/edit.blade.php @@ -0,0 +1,38 @@ +@extends('layouts.base') +@section('content') + +
+
+

Editando {{ $curso->name }}

+
+
+
id)}} method="POST" > + @csrf + @method('PUT') + @include ('includes.error_alert') + +
+ + + @error('name') +
{{ $message }}
+ @enderror +
+ + +
+
+ + @can ('delete', $curso) +
id)}}"> + {{ csrf_field() }} + {{ method_field('DELETE') }} + +
+ +
+
+ @endcan + +
+@endsection diff --git a/resources/views/curso/index.blade.php b/resources/views/curso/index.blade.php new file mode 100644 index 0000000..337f343 --- /dev/null +++ b/resources/views/curso/index.blade.php @@ -0,0 +1,29 @@ +@extends('layouts.base') +@section('content') +
+

Cursos

+
+ +
+ @can ('create', App\Models\Turma::class) + Cadastrar turma + + @endcan + @can ('create', App\Models\Curso::class) + Novo curso + + @endcan +
+ +
+ + @if (Session::has('message')) +
{{ Session::get('message') }}
+ @endif +
+ + @include('curso.table', ['editButton' => true]) + +@endsection diff --git a/resources/views/curso/table.blade.php b/resources/views/curso/table.blade.php new file mode 100644 index 0000000..7dede32 --- /dev/null +++ b/resources/views/curso/table.blade.php @@ -0,0 +1,34 @@ +@foreach ($cursos as $value) + +

Editar + @endcan +

+ +
+ @include('turma.table', ['editButton' => true, 'turmas' => $value->turmas]) +
+ +@endforeach + +{{-- turmas sem curso --}} + +
+ @include('turma.table', ['editButton' => true, 'turmas' => $semCurso]) +
\ No newline at end of file diff --git a/resources/views/exercicio/create.blade.php b/resources/views/exercicio/create.blade.php index 0d18804..08bc52f 100644 --- a/resources/views/exercicio/create.blade.php +++ b/resources/views/exercicio/create.blade.php @@ -10,6 +10,7 @@
@csrf @method ('put') + @error('file') @@ -17,9 +18,11 @@ @enderror
+ @if (old('from_import',false))
Arquivo carregado. Verifique os campos antes de salvar!
@endif +
@csrf @@ -28,7 +31,10 @@
- + @error('name')
{{ $message }}
@enderror @@ -47,6 +53,7 @@
{{ $message }}
@enderror
+
diff --git a/resources/views/includes/sidebar.blade.php b/resources/views/includes/sidebar.blade.php index d221e1a..bca75ce 100644 --- a/resources/views/includes/sidebar.blade.php +++ b/resources/views/includes/sidebar.blade.php @@ -1,10 +1,8 @@
-@endsection diff --git a/resources/views/turma/show.blade.php b/resources/views/turma/show.blade.php index 78ae6a5..113b531 100644 --- a/resources/views/turma/show.blade.php +++ b/resources/views/turma/show.blade.php @@ -5,17 +5,20 @@

{!! nl2br($turma->description) !!}

- -

Membros

-
-
- @can ('edit', $turma) - @include('user.table',['users' => $turma->users, - 'removeButton' => true]) - @else - @include('user.table',['users' => $turma->users]) - @endcan -
+ + @can('seeMembers', $turma) + +

Membros

+
+
+ @can ('edit', $turma) + @include('user.table',['users' => $turma->users, + 'removeButton' => true]) + @else + @include('user.table',['users' => $turma->users]) + @endcan +
+ @endcan @if($prazosFuturos ?? '') diff --git a/routes/web.php b/routes/web.php index 6575fb5..7e75163 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,6 +3,7 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\UserController; use App\Http\Controllers\TurmaController; +use App\Http\Controllers\CursoController; use App\Http\Controllers\TopicoController; use App\Http\Controllers\PrazoController; use App\Http\Controllers\ExercicioController; @@ -49,6 +50,7 @@ Route::resources([ 'user' => UserController::class, 'turma' => TurmaController::class, + 'curso' => CursoController::class, 'topico' => TopicoController::class, 'exercicio' => ExercicioController::class, 'prazo' => PrazoController::class, diff --git a/tests/cypress/integration/01_admin_login.cy.js b/tests/cypress/integration/01_admin_login.cy.js index 630a83e..564c6fd 100644 --- a/tests/cypress/integration/01_admin_login.cy.js +++ b/tests/cypress/integration/01_admin_login.cy.js @@ -32,9 +32,9 @@ describe('Login test', () => { cy.get('h1').should('contain', 'Admin') // Can see turmas - cy.contains('Turmas').click() - cy.url().should('include', '/turma') - cy.contains('Turmas') + cy.contains('Cursos').click() + cy.url().should('include', '/curso') + cy.contains('Cursos') cy.go('back') // Can see Users