Taskx mittels Laraknife: Unterschied zwischen den Versionen
Zur Navigation springen
Zur Suche springen
Celine (Diskussion | Beiträge) |
|||
Zeile 12: | Zeile 12: | ||
* Benutzerverwaltung: nur angemeldete Benutzer können Applikation nutzen | * Benutzerverwaltung: nur angemeldete Benutzer können Applikation nutzen | ||
* Rechteverwaltung mittels Rollen | * Rechteverwaltung mittels Rollen | ||
* Verwaltung von Notizen: Titel, Text, Kategorie, | * Verwaltung von Notizen: Titel, Text, Kategorie, Gruppen, Status: offen, erledigt | ||
= Installation = | = Installation = |
Version vom 31. Dezember 2023, 12:53 Uhr
Links
Zielsetzung
Es soll eine minimale Web-Applikation mit den Werkzeugen Laravel und Laraknife erstellt werden: Eine Verwaltung von Notizen.
Name
Eine Verballhornung des Wortes "Tasks": Die schwäbische Aussprache spricht ein x am Ende.
Eigenschaften
- Benutzerverwaltung: nur angemeldete Benutzer können Applikation nutzen
- Rechteverwaltung mittels Rollen
- Verwaltung von Notizen: Titel, Text, Kategorie, Gruppen, Status: offen, erledigt
Installation
- als normaler Benutzer (nicht root):
sudo apt install php-laravel-framework npm php-intl
PROJ=taskx
PASSW=topsecret
BASE=/home/ws/php/$PROJ
cd $(dirname $BASE)
composer create-project laravel/laravel $PROJ
cd $BASE
composer require laravel/ui
composer require spatie/laravel-permission
php artisan ui bootstrap --auth
dbtool create-db-and-user lrv$PROJ $PROJ "$PASSW"
sed -i -e "s/DB_DATABASE=.*/DB_DATABASE=lrv$PROJ/" \
-e "s/DB_USERNAME=.*/DB_USERNAME=$PROJ/" \
-e "s/DB_PASSWORD=.*/DB_PASSWORD=$PASSW/" .env
M_HOST=mail.gmx.net
M_PORT=587
M_USER=example@gmx.de
M_PW=Top.Secret42
sed -i \
-e "s/APP_NAME=.*/APP_NAME=$PROJ"/ \
-e "s/MAIL_MAILER=.*/MAIL_MAILER=smtp/" \
-e "s/MAIL_HOST=.*/MAIL_HOST=$M_HOST/" \
-e "s/MAIL_PORT=.*/MAIL_PORT=$M_PORT/" \
-e "s/MAIL_USERNAME=.*/MAIL_USERNAME=$M_USER/" \
-e "s/MAIL_PASSWORD=.*/MAIL_PASSWORD=$M_PW/" \
-e "s/MAIL_ENCRYPTION=.*/MAIL_ENCRYPTION=STARTTLS/" \
-e "s/MAIL_FROM_ADDRESS=.*/MAIL_FROM_ADDRESS=\"$M_USER\"/" .env
sudo sed -i -e "3 i 127.0.0.1 $PROJ.test" /etc/hosts
grep "$PROJ.test" /etc/hosts
php artisan migrate
npm install
npm run dev
# Abbruch mit Strg-C
Einrichten LaraKnife
composer config repositories.laraknife vcs https://github.com/hamatoma/laraknife
# Branch main:
composer require hamatoma/laraknife:dev-main
composer update
vendor/hamatoma/laraknife/scripts/laraknife-tool.sh build-links
Mehrsprachigkeit (I18N)
- config/app.php
'locale' => 'en', # ersetzen durch: 'locale' => 'de_DE', 'available_locales' => [ 'English' => 'en', 'German' => 'de_DE', ],
- Übersetzungen aus LaraKnife aktivieren:
# App-spezifische Übersetzung anlegen:
cat <<EOS >resources/lang/sources/$PROJ.de.json
{
"!comment": "Bitte alphabetisch sortiert eintragen",
"ZZZZZ_last": ""
}
EOS
./Join
- Die neuen Übersetzungen müssen in resources/lang/sources/taskx.de.json eingetragen werden.
Roles und SProperties füllen
php artisan migrate
sudo mysql lrv$PROJ <<'EOS'
insert into roles (name, priority, created_at, updated_at) values
('Administrator', 10, '2023.12.28', '2023-12-28'),
('Manager', 20, '2023.12.28', '2023-12-28'),
('User', 30, '2023.12.28', '2023-12-28'),
('Guest', 90, '2023.12.28', '2023-12-28');
insert into sproperties (id, scope, name, `order`, shortname, created_at) values
(1001, 'status', 'active', 10, 'A', '2023-12-28'),
(1002, 'status', 'inactive', 20, 'I', '2023-12-28'),
(2001, 'category', 'standard', 10, '-', '2023-12-28'),
(2002, 'category', 'private', 20, 'P', '2023-12-28'),
(2003, 'category', 'work', 30, 'W', '2023-12-28'),
(2501, 'group', 'standard', 10, '-', '2023-12-28'),
(2502, 'group', 'private', 20, 'P', '2023-12-28'),
(2503, 'group', 'work', 20, 'W', '2023-12-28'),
(2601, 'notestatus', 'open', 10, 'O', '2023-12-28'),
(2602, 'notestatus', 'closed', 20, 'C', '2023-12-28');
EOS
User anpassen
- Datei app/Models/User.php ergänzen:
protected $fillable = [ 'name', 'email', 'password', 'role_id', // <==== ];
Modul Notes erstellen
Tabellenbeschreibung erstellen
- Konvention: Tabelle wird kleingeschrieben, im Plural
TABLE=notes
php artisan make:migration create_${TABLE}_table
- Es wird die Datei database/migrations/2023_12_29_180821_create_notes_table.php erzeugt
- Diese Datei anpassen:
Schema::create('notes', function (Blueprint $table) { $table->id(); $table->timestamps(); $table->string('title'); $table->text('body'); // foreign key of sproperties. $table->integer('category_scope'); // foreign key of sproperties. $table->integer('status_scope'); // foreign key of sproperties. $table->integer('group_scope'); $table->foreignId('user_id')->references('id')->on('users')->nullable(); });
Modul erzeugen
php artisan migrate
./Lara create:module database/migrations/*_create_notes_table.php
Layout erstellen
- Datei resources/views/layouts/taskx.blade.php erstellen:
cat <<'EOS' >resources/views/layouts/taskx.blade.php
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<!-- link rel="dns-prefetch" href="//fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=Nunito" rel="stylesheet" -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Scripts -->
@vite(['resources/sass/app.scss', 'resources/js/app.js'])
<link href="/css/laraknife.css" rel="stylesheet">
<link href="/css/taskx.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"></script>
<script src="/js/laraknife.js"></script>
</head>
<body>
<header>
<nav class="navbar navbar-expand-md bg-primary ">
<a class="navbar-brand" href="/home">Booking</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarTop">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="#">Start</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/user-edit">{{ __('Settings') }}</a>
</li>
<li>
<a class="nav-link" href="/public/doc/Impressum.pdf" target="_blank">{{ __('Imprint') }}</a>
</li>
<li>
<a class="nav-link" href="/public/doc/Datenschutz.pdf" target="_blank">{{ __('Privacy') }}</a>
</li>
</ul>
<form class="form-inline my-2 my-lg-0" action="/logout" method="post">
<button class="btn btn-outline-success my-2 my-sm-0 logout" name="btnLogout"
type="submit"> {{ __('Logout') }}</button>
</form>
</div>
</nav>
</header>
@yield('content')
</body>
</html>
EOS
cd resources/views/layouts
ln -sv taskx.blade.php backend.blade.php
cd ../../..
Homepage einrichten
- Datei resources/views/home.blade.php
cat <<'EOS' >resources/views/home.blade.php
@extends('layouts.taskx')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Dashboard') }}</div>
<div class="card-body">
@if (session('status'))
<div class="alert alert-success" role="alert">
{{ session('status') }}
</div>
@endif
{{ __('You are logged in!') }}
<ul>
<li><a href="/sproperty-index">SProperties</a></li>
<li><a href="/user-index">Benutzer</a></li>
<li><a href="/role-index">Rollen</a></li>
<li><a href="/note-index">Notizen</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
@endsection
EOS
Routing einrichten
- In Datei routes/web.php einfügen:
SPropertyController::routes();
RoleController::routes();
UserController::routes();
NoteController::routes();
- Es sind 4 Namen unterstrichen: mit Strg-Alt-I Klassen einbinden.
Starten, Registrieren, Login
- Den Webserver starten:
./Build
php artisan serve
- Im Brower aufrufen: http://localhost:8000
- Oben rechts: Register
- Felder ausfüllen
- Oben rechts: Login
Controller und Views anpassen
Übersicht (index)
- Datei app/Http/Controllers/NoteController.php
public function index()
{
if (array_key_exists('btnSubmit', $_POST) && $_POST['btnSubmit'] === 'btnNew') {
return redirect('/note-create');
} else {
$sql = 'SELECT id, title, body, user_id, cast(body AS VARCHAR(60)) as body2 FROM notes';
$userId = auth()->id();
$parameters = [':user_id' => $userId];
$conditions = ['user_id = :user_id'];
if (count($_POST) == 0) {
$fields = [
'text' => '',
'_sortParams' => 'id:desc'
];
} else {
$fields = $_POST;
ViewHelper::addConditionPattern($conditions, $parameters, 'title,body', 'text');
}
$sql = DbHelper::addConditions($sql, $conditions);
$sql = DbHelper::addOrderBy($sql, $fields['_sortParams']);
$records = DB::select($sql, $parameters);
$pagination = new Pagination($sql, $parameters, $fields);
return view('note.index', [
'records' => $records,
'fields' => $fields,
'pagination' => $pagination
]);
}
}
- Datei resources/views/note/index.blade.php
<form id="note-index" action="/note-index" method="POST">
@csrf
<x-laraknife.index-panel title="{{ __('Notes') }}">
<x-laraknife.filter-panel legend="{{ $pagination->legendText() }}">
<x-laraknife.text position="alone" name="text" label="Text" value="{{$fields['text']}}" width2="4" />
</x-laraknife.filter-panel>
<x-laraknife.index-button-panel buttonType="new"/>
<x-laraknife.sortable-table-panel :fields="$fields" :pagination="$pagination">
<thead>
<tr>
<th></th>
<th sortId="id">{{__('Id')}}</th>
<th sortId="title">{{__('Title')}}</th>
<th sortId="body">{{__('Body')}}</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach ($records as $note)
<tr>
<td><a href="/note-edit/{{$note->id}}">{{ __('Change')}}</a></td>
<td>{{$note->id}}</td>
<td>{{$note->title}}</td>
<td>{{$note->body2}}</td>
<td><a href="/note-delete/{{$note->id}}">{{ __('Delete')}}</a></td>
</tr>
@endforeach
</tbody>
</x-laraknife.sortable-table-panel>
</x-laraknife.index-panel>
Anlegen (create)
- Datei app/Http/Controllers/NoteController.php
- Die drei Methoden ersetzen:
public function create(Request $request)
{
if (array_key_exists('btnSubmit', $_POST) && $_POST['btnSubmit'] === 'btnCancel') {
$rc = redirect('/note-index');
} else {
$rc = null;
$error = null;
if (count($_POST) > 0) {
$fields = $_POST;
try {
$incomingFields = $request->validate($this->rules(true));
$rc = $this->store($request);
} catch (\Exception $e) {
$error = $e->getMessage();
}
} else {
$fields = [
'title' => '',
'body' => '',
'category_scope' => '2001',
'status_scope' => '2601',
'group_scope' => '2501',
];
}
if ($rc == null) {
$optionsCategory = SProperty::optionsByScope('category', $fields['category_scope'], '-');
$optionsStatus = SProperty::optionsByScope('notestatus', $fields['status_scope'], '-');
$optionsGroup = SProperty::optionsByScope('group', $fields['group_scope'], '-');
$rc = view('note.create', ['fields' => $fields,
'optionsCategory' => $optionsCategory,
'optionsStatus' => $optionsStatus,
'optionsGroup' => $optionsGroup,
'error' => $error]);
}
}
return $rc;
}
private function rules(bool $isCreation=false): array
{
$rc = [
'title' => 'required',
'body' => 'required',
'category_scope' => 'required',
'status_scope' => 'required',
'group_scope' => 'required',
];
return $rc;
}
public function store(Request $request)
{
if ($request->btnSubmit === 'btnStore') {
$incomingFields = $request->validate($this->rules());
$incomingFields['user_id'] = auth()->id();
$incomingFields['body'] = strip_tags($incomingFields['body']);
Note::create($incomingFields);
}
return redirect('/note-index');
}
- Datei resources/views/note/create.blade.php
<form id="note-create" action="/note-create" method="POST">
@csrf
<x-laraknife.create-panel title="{{ __('Creation of a Note') }}" error="{{$error}}">
<x-laraknife.combobox position="first" name="status_scope" label="Status" value="{{$fields['status_scope']}}" :options="$optionsStatus" width2="4" />
<x-laraknife.combobox position="last" name="group_scope" label="Group" value="{{$fields['group_scope']}}" :options="$optionsGroup" width2="4" />
<x-laraknife.combobox position="alone" name="category_scope" label="Category" value="{{$fields['category_scope']}}" :options="$optionsCategory" width2="4" />
<x-laraknife.text position="alone" name="title" label="Title" value="{{$fields['title']}}" />
<x-laraknife.bigtext position="alone" name="body" label="Body" value="{{$fields['body']}}" rows="10" />
</x-laraknife.create-panel>
</form>
- Übersetzungen in resources/lang/sources/taskx.de.json
"Body": "Info", "Category": "Kategorie", "Creation of a Note": "Anlegen einer Notiz", "Group": "Gruppe", "open": "offen", "standard": "Standard", "Status": "Status", "Title": "Titel",
- Aktivieren:
./Join
Ändern (edit)
- Datei app/Http/Controllers/NoteController.php
public function edit(Note $note)
{
if (array_key_exists('btnSubmit', $_POST) && $_POST['btnSubmit'] === 'btnCancel') {
$rc = redirect('/note-index');
} else {
$optionsCategory = SProperty::optionsByScope('category', $note->category_scope, '-');
$optionsStatus = SProperty::optionsByScope('notestatus', $note->status_scope, '-');
$optionsGroup = SProperty::optionsByScope('group', $note->group_scope, '-');
$rc = view('note.edit', [
'note' => $note,
'optionsCategory' => $optionsCategory,
'optionsStatus' => $optionsStatus,
'optionsGroup' => $optionsGroup,
]);
}
return $rc;
}
public function update(Note $note, Request $request)
{
$rc = null;
if ($request->btnSubmit === 'btnStore') {
try {
$incomingFields = $request->validate($this->rules());
$incomingFields['body'] = strip_tags($incomingFields['body']);
$note->update($incomingFields);
$rc = redirect('/note-index');
} catch (\Exception $exc) {
$msg = $exc->getMessage();
$rc = back();
}
}
if ($rc == null) {
$rc = redirect('/note-index');
}
return $rc;
}
- Datei resources/views/note/edit.blade.php
<form id="note-edit" action="/note-update/{{ $note->id }}" method="POST">
@csrf
<x-laraknife.edit-panel title="{{ __('Change of a Note') }}">
<x-laraknife.combobox position="first" name="status_scope" label="Status" value="{{$note->status_scope}}" :options="$optionsStatus" width2="4" />
<x-laraknife.combobox position="last" name="group_scope" label="Group" value="{{$note->group_scope}}" :options="$optionsGroup" width2="4" />
<x-laraknife.combobox position="alone" name="category_scope" label="Category" value="{{$note->category_scope}}" :options="$optionsCategory" width2="4" />
<x-laraknife.text position="alone" name="title" label="Title" value="{{$note->title}}" />
<x-laraknife.bigtext position="alone" name="body" label="Body" value="{{$note->body}}" rows="10" />
</x-laraknife.edit-panel>
</form>
- Übersetzungen in resources/lang/sources/taskx.de.json
"Change of a Note": "Ändern einer Notiz",
- Aktivieren:
./Join
Anzeigen (show/delete)
- Datei app/Http/Controllers/NoteController.php
public function show(Note $note)
{
if (array_key_exists('btnSubmit', $_POST) && $_POST['btnSubmit'] === 'btnCancel') {
$rc = redirect('/note-index');
} else {
$optionsCategory = SProperty::optionsByScope('category', 'category', '');
$optionsGroup = SProperty::optionsByScope('group', 'group', '');
$optionsStatus = SProperty::optionsByScope('notestatus', 'status', '');
$rc = view('note.show', [
'note' => $note,
'optionsCategory' => $optionsCategory,
'optionsGroup' => $optionsGroup,
'optionsStatus' => $optionsStatus,
'mode' => 'delete'
]);
}
return $rc;
}
- Datei resources/views/note/show.blade.php
<form id="note-show" action="/note-show/{{ $note->id }}/{{ $mode }}" method="POST">
@csrf
@if ($mode === 'delete')
@method('DELETE')
@endif
<x-laraknife.show-panel title="{{ __($mode !== 'delete' ? 'A Note' : 'Deletion of a Note') }}"
mode="{{ $mode }}">
<x-laraknife.combobox position="first" name="status_scope" label="Status" value="{{ $note->status_scope }}"
:options="$optionsStatus" width2="4" attribute="readonly" />
<x-laraknife.combobox position="last" name="group_scope" label="Group" value="{{ $note->group_scope }}"
:options="$optionsGroup" width2="4" attribute="readonly" />
<x-laraknife.combobox position="first" name="category_scope" label="Category" value="{{ $note->category_scope }}"
:options="$optionsCategory" width2="4" attribute="readonly" />
<x-laraknife.text position="last" name="id" label="Id" value="{{ $note->id }}" width2="4"
attribute="readonly" />
<x-laraknife.text position="alone" name="title" label="Title" value="{{ $note->title }}"
attribute="readonly" />
<x-laraknife.bigtext position="alone" name="body" label="Body" value="{{ $note->body }}" rows="10"
attribute="readonly" />
</x-laraknife.show-panel>
</form>
- Übersetzungen in resources/lang/sources/taskx.de.json
"A Note" : "Eine Notiz", "Deletion of a Note": "Löschen einer Notiz",
- Aktivieren:
./Join