From: Hamatoma Date: Wed, 26 Mar 2025 22:28:54 +0000 (+0100) Subject: V0.1.3 Process ExportController drawio2db.py X-Git-Url: https://gitweb.hamatoma.de/?a=commitdiff_plain;h=6047501850f137e2c94a46cf59117137b29db89e;p=zentrum.gemeinwohl-gesellschaft.org.git V0.1.3 Process ExportController drawio2db.py - ExportController: $_SERVER['documentroot] replaced by FileHelper::documentRoot() - new module Process - new drawio2db.py --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e84b8a..cf2f7d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Änderungen an zentrum +# V0.1.3 Process ExportController drawio2db.py + +- ExportController: $_SERVER['documentroot] replaced by FileHelper::documentRoot() +- new module Process +- new drawio2db.py + # V0.1.2 Layout: Startmenü oder Startmenü-Public - User: neu: isGuest() - zentrum.blade: ohne Anmeldung: keine "Verwaltung", "Anmelden"-Button diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php index d8f58d6..8ac43e5 100644 --- a/app/Http/Controllers/ExportController.php +++ b/app/Http/Controllers/ExportController.php @@ -35,7 +35,7 @@ class ExportController extends Controller 'patterns' => '', ]; } - $path = $_SERVER['DOCUMENT_ROOT'] . "/export"; + $path = FileHelper::documentRoot() . "/export"; $patterns = str_replace(['*', '?'], ['.*', '.'], $fields['patterns']); $patterns = '/^' . auth()->user()->name . "\\.$patterns/"; $files = FileHelper::fileInfoList($path, $patterns); @@ -55,7 +55,7 @@ class ExportController extends Controller 'filename' => '', ]; } - $path = $_SERVER['DOCUMENT_ROOT'] . "/temp"; + $path = FileHelper::documentRoot() . "/temp"; if ($request->btnSubmit === 'btnUpload') { if (($file = $request->file('file')) != null) { $relativePath = basename($path); @@ -88,7 +88,7 @@ class ExportController extends Controller public function removeFile(string $nodeEncoded, Request $request) { $node = FileHelper::decodeUrl($nodeEncoded); - $full = $_SERVER['DOCUMENT_ROOT'] . "/export/$node"; + $full = FileHelper::documentRoot() . "/export/$node"; if (file_exists($full)) { unlink($full); } diff --git a/app/Http/Controllers/ProcessController.php b/app/Http/Controllers/ProcessController.php new file mode 100644 index 0000000..7cd1cb1 --- /dev/null +++ b/app/Http/Controllers/ProcessController.php @@ -0,0 +1,332 @@ +btnSubmit === 'btnCancel') { + $rc = redirect('/process-index'); + } else { + $fields = $request->all(); + if (count($fields) === 0) { + $fields = [ + 'division_scope' => '', + 'activity_scope' => '', + 'file' => '', + 'owner_id' => auth()->id() + ]; + } + $context = new ContextLaraKnife($request, $fields); + $optionsDivision = SProperty::optionsByScope('division', $fields['division_scope'], ''); + $optionsActivity = SProperty::optionsByScope('activity', $fields['activity_scope'], ''); + $rc = view('process.create', [ + 'context' => $context, + 'optionsDivision' => $optionsDivision, + 'optionsActivity' => $optionsActivity + ]); + } + return $rc; + } + /** + * Show the form for editing the specified resource. + */ + public function edit(Process $process, Request $request) + { + if ($request->btnSubmit === 'btnCancel') { + $rc = redirect('/process-index'); + } else { + $fields = $request->all(); + if (count($fields) === 0) { + $fields = [ + 'serialno' => Process::nextSerialNo(), + 'division_scope' => $process->division_scope, + 'activity_scope' => $process->activity_scope, + 'path' => $process->path, + 'title' => $process->title, + 'roles' => $process->roles, + 'subprocesses' => $process->subprocesses, + 'datasources' => $process->datasources, + 'texts' => $process->texts, + 'info' => $process->info, + 'owner_id' => $process->owner_id + ]; + } + $context = new ContextLaraKnife($request, $fields, $process); + $optionsDivision = SProperty::optionsByScope('division', $process->division_scope, ''); + $optionsActivity = SProperty::optionsByScope('activity', $process->activity_scope, ''); + $optionsOwner = DbHelper::comboboxDataOfTable('users', 'name', 'id', $fields['owner_id'], __('')); + $rc = view('process.edit', [ + 'context' => $context, + 'optionsDivision' => $optionsDivision, + 'optionsActivity' => $optionsActivity, + 'optionsOwner' => $optionsOwner, + ]); + } + return $rc; + } + /** + * Remove the specified resource from storage. + */ + public function destroy(Process $process, Request $request) + { + if ($request->btnSubmit === 'btnDelete') { + $process->delete(); + } + return redirect('/process-index'); + } + /** + * Display the database records of the resource. + */ + public function index(Request $request) + { + if ($request->btnSubmit === 'btnNew') { + return redirect('/process-create'); + } else { + $sql = " +SELECT t0.*, + t1.name as division, + t2.name as activity, + t3.name as owner +FROM processes t0 +LEFT JOIN sproperties t1 ON t1.id=t0.division_scope +LEFT JOIN sproperties t2 ON t2.id=t0.activity_scope +LEFT JOIN users t3 ON t3.id=t0.owner_id +"; + $parameters = []; + $fields = $request->all(); + if (count($fields) == 0) { + $fields = [ + 'division' => '', + 'activity' => '', + 'owner' => '', + 'serialno' => '', + 'path' => '', + 'title' => '', + 'roles' => '', + 'subprocesses' => '', + 'datasources' => '', + 'texts' => '', + 'info' => '', + '_sortParams' => 'id:asc' + ]; + } + $conditions = []; + ViewHelper::addConditionComparison($fields, $conditions, $parameters, 'division_scope', 'division'); + ViewHelper::addConditionComparison($fields, $conditions, $parameters, 'activity_scope', 'activity'); + ViewHelper::addConditionComparison($fields, $conditions, $parameters, 'owner_id', 'owner'); + ViewHelper::addConditionPattern($fields, $conditions, $parameters, 'path'); + ViewHelper::addConditionPattern($fields, $conditions, $parameters, 'title'); + ViewHelper::addConditionPattern($fields, $conditions, $parameters, 'roles'); + ViewHelper::addConditionPattern($fields, $conditions, $parameters, 'subprocesses'); + ViewHelper::addConditionPattern($fields, $conditions, $parameters, 'datasources'); + ViewHelper::addConditionPattern($fields, $conditions, $parameters, 'texts'); + $sql = DbHelper::addConditions($sql, $conditions); + $sql = DbHelper::addOrderBy($sql, $fields['_sortParams']); + $pagination = new Pagination($sql, $parameters, $fields); + $records = $pagination->records; + $optionsDivision = SProperty::optionsByScope('division', $fields['division'], 'all'); + $optionsActivity = SProperty::optionsByScope('activity', $fields['activity'], 'all'); + $optionsOwner = DbHelper::comboboxDataOfTable('users', 'name', 'id', $fields['owner'], __('all')); + $context = new ContextLaraKnife($request, $fields); + return view('process.index', [ + 'context' => $context, + 'records' => $records, + 'optionsDivision' => $optionsDivision, + 'optionsActivity' => $optionsActivity, + 'optionsOwner' => $optionsOwner, + 'pagination' => $pagination + ]); + } + } + public function parseDrawIoXml(string $filename, array &$fields): void + { + $script = FileHelper::baseDirectory() . '/scripts/drawio2db.py'; + $output = []; + $fullPath = storage_path('app/public/' . $filename); + exec("$script $fullPath", $output); + $multiline = ''; + $matches = null; + $target = null; + $fields['roles'] = $fields['subprocesses'] = $fields['datasources'] = $fields['texts'] = null; + foreach ($output as $line) { + if (str_starts_with($line, 'Title: ')) { + $fields['title'] = substr($line, 7); + } elseif (str_starts_with($line, ' ')) { + $multiline .= substr($line, 2) . "\n"; + } elseif (preg_match('/^(Roles|SubProcesses|Data Sources|Texts)/', $line, $matches)) { + switch ($target) { + case 'Roles': + $fields['roles'] = $multiline; + break; + case 'SubProcesses': + $fields['subprocesses'] = $multiline; + break; + case 'Data Sources': + $fields['datasources'] = $multiline; + break; + case 'Texts': + $fields['texts'] = $multiline; + break; + default: + break; + } + $multiline = ''; + $target = $matches[1]; + } + } + if ($target === 'Texts') { + $fields['texts'] = $multiline; + } + } + + /** + * Returns the validation rules. + * @return array The validation rules. + */ + private function rules(bool $isCreate = false): array + { + $rc = [ + 'serialno' => $isCreate ? '' : 'required', + 'division_scope' => $isCreate ? '' : 'required', + 'activity_scope' => $isCreate ? '' : 'required', + 'path' => $isCreate ? '' : 'required', + 'title' => $isCreate ? '' : 'required', + 'roles' => '', + 'subprocesses' => '', + 'datasources' => '', + 'texts' => '', + 'info' => '', + 'owner_id' => $isCreate ? '' : 'required' + ]; + return $rc; + } + public static function routes() + { + Route::get('/process-index', [ProcessController::class, 'index'])->middleware('auth'); + Route::post('/process-index', [ProcessController::class, 'index'])->middleware('auth'); + Route::get('/process-create', [ProcessController::class, 'create'])->middleware('auth'); + Route::put('/process-store', [ProcessController::class, 'store'])->middleware('auth'); + Route::post('/process-edit/{process}', [ProcessController::class, 'edit'])->middleware('auth'); + Route::get('/process-edit/{process}', [ProcessController::class, 'edit'])->middleware('auth'); + Route::post('/process-update/{process}', [ProcessController::class, 'update'])->middleware('auth'); + Route::get('/process-show/{process}/delete', [ProcessController::class, 'show'])->middleware('auth'); + Route::delete('/process-show/{process}/delete', [ProcessController::class, 'destroy'])->middleware('auth'); + } + /** + * Display the specified resource. + */ + public function show(Process $process, Request $request) + { + if ($request->btnSubmit === 'btnCancel') { + $rc = redirect('/process-index')->middleware('auth'); + } else { + $optionsDivision = SProperty::optionsByScope('division', $process->division_scope, ''); + $optionsActivity = SProperty::optionsByScope('activity', $process->activity_scope, ''); + $optionsOwner = DbHelper::comboboxDataOfTable('users', 'name', 'id', $process->owner_id, __('')); + $context = new ContextLaraKnife($request, null, $process); + $rc = view('process.show', [ + 'context' => $context, + 'optionsDivision' => $optionsDivision, + 'optionsActivity' => $optionsActivity, + 'optionsOwner' => $optionsOwner, + 'mode' => 'delete' + ]); + } + return $rc; + } + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $rc = null; + if ($request->btnSubmit === 'btnStore') { + $fields = $request->all(); + $validator = Validator::make($fields, $this->rules(true)); + if ($validator->fails()) { + $rc = back()->withErrors($validator)->withInput(); + } else { + $validated = $validator->validated(); + $fullName = $this->storeFile($request); + if ($fullName == null) { + $rc = back()->withErrors($validator)->withInput(); + } else { + $this->parseDrawIoXml($fullName, $validated); + $process = Process::create($validated); + Change::createFromFields($validated, Change::$CREATE, 'Process', $process->id); + $rc = redirect('/process-edit/' . $process->id); + } + } + } + if ($rc == null) { + $rc = redirect('/process-index'); + } + return $rc; + } + /** + * Stores the file and returns the full file name. + * @param \Illuminate\Http\Request $request the request information + * @return string|null the full file name or null if no file was uploaded + */ + public function storeFile(Request $request): ?string + { + $rc = null; + $file = $request->file(); + $file = $request->file('file'); + if ($file != null) { + $name = $file->getClientOriginalName(); + $filename = session('userName') . '_' . strval(time()) . '!' . $name; + $rc = FileHelper::storeFile($request, 'file', $filename); + } + return $rc; + } + + /** + * Update the specified resource in storage. + */ + public function update(Process $process, Request $request) + { + $rc = null; + if ($request->btnSubmit === 'btnStore') { + $fields = $request->all(); + $validator = Validator::make($fields, $this->rules(false)); + if ($validator->fails()) { + $rc = back()->withErrors($validator)->withInput(); + } else { + $validated = $validator->validated(); + $validated['path'] = strip_tags($validated['path']); + $validated['roles'] = strip_tags($validated['roles']); + $validated['subprocesses'] = strip_tags($validated['subprocesses']); + $validated['datasources'] = strip_tags($validated['datasources']); + $validated['texts'] = strip_tags($validated['texts']); + $validated['info'] = strip_tags($validated['info']); + $process->update($validated); + } + } + if ($rc == null) { + $rc = redirect('/process-index'); + } + return $rc; + } +} diff --git a/app/Models/Process.php b/app/Models/Process.php new file mode 100644 index 0000000..307ca87 --- /dev/null +++ b/app/Models/Process.php @@ -0,0 +1,33 @@ +id(); + $table->timestamps(); + $table->integer('serialno')->nullable()->unique(); + $table->integer('division_scope'); + $table->integer('activity_scope'); + $table->text('path')->nullable(); + $table->string('title')->unique(); + $table->text('roles')->nullable(); + $table->text('subprocesses')->nullable(); + $table->text('datasources')->nullable(); + $table->text('texts')->nullable(); + $table->text('info')->nullable(); + $table->foreignId('owner_id')->nullable()->references('id')->on('users'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('processes'); + } +}; diff --git a/database/seeders/ProcessSeeder.php b/database/seeders/ProcessSeeder.php new file mode 100644 index 0000000..4ab5424 --- /dev/null +++ b/database/seeders/ProcessSeeder.php @@ -0,0 +1,44 @@ + + @csrf + @method('PUT') + + + + + + + + +@endsection diff --git a/resources/views/process/edit.blade.php b/resources/views/process/edit.blade.php new file mode 100644 index 0000000..e861983 --- /dev/null +++ b/resources/views/process/edit.blade.php @@ -0,0 +1,31 @@ +@extends('layouts.backend') + +@section('content') +
+ @csrf + + + + + + + + + + + + + +
+@endsection diff --git a/resources/views/process/index.blade.php b/resources/views/process/index.blade.php new file mode 100644 index 0000000..33fc8c6 --- /dev/null +++ b/resources/views/process/index.blade.php @@ -0,0 +1,49 @@ +@extends('layouts.backend') + +@section('content') +
+ @csrf + + + + + + + + + + + + + + + + + + Nummer + Titel + Abteilung + Tätigkeitsfeld + Pfad + Besitzer + + + + +@foreach ($records as $process) + + + {{$process->serialno}} + {{$process->title}} + {{ __($process->division) }} + {{ __($process->activity) }} + {{$process->path}} + {{$process->owner}} + + +@endforeach + + + +
+@endsection diff --git a/resources/views/process/show.blade.php b/resources/views/process/show.blade.php new file mode 100644 index 0000000..5bb7199 --- /dev/null +++ b/resources/views/process/show.blade.php @@ -0,0 +1,36 @@ +@extends('layouts.backend') + +@section('content') +
+ @csrf + @if ($mode === 'delete') + @method('DELETE') + @endif + + + + + + + + + + + + + +
+@endsection diff --git a/routes/web.php b/routes/web.php index 0b0f80a..730b733 100644 --- a/routes/web.php +++ b/routes/web.php @@ -18,6 +18,7 @@ use App\Http\Controllers\LocationController; use App\Http\Controllers\MandatorController; use App\Http\Controllers\MenuitemController; use App\Http\Controllers\SPropertyController; +use App\Http\Controllers\ProcessController; use App\Http\Controllers\TransactionController; if (User::isGuest()) { @@ -47,3 +48,4 @@ AddressController::routes(); MandatorController::routes(); AccountController::routes(); TransactionController::routes(); +ProcessController::routes(); diff --git a/scripts/drawio2db.py b/scripts/drawio2db.py new file mode 100755 index 0000000..e03642a --- /dev/null +++ b/scripts/drawio2db.py @@ -0,0 +1,177 @@ +#! /usr/bin/python3 +import xml.etree.ElementTree as ET +import sys +import os +import re + + +class AnalyseDrawio: + """ + A tool for analyzing a drawio file (XML) + Prints the database relevant information. + """ + + def __init__(self, xml_file: str): + """ + Constructor + :param xml_file: the name of the drawio file to analyze + """ + self.xml_file = xml_file + self.actions = 0 + self.comments = 0 + self.decisions = 0 + self.delays = 0 + self.documents = 0 + self.manual_inputs = 0 + self.start_end = 0 + self.data_sources = [] + self.subProcesses = {} + self.texts = [] + self.title = "" + self.roles = {} + self.regColor = re.compile(r"fillColor=.*?(#[0-9A-Fa-f]{6})") + self.role_to_color = { + "Ladenteam": "#E6FFCC", + "Eventteam": "#CCFF99", + "Beirät:innen": "#CCE5FF", + "Vorstandschaft": "#99CCFF", + "Buchhaltung": "#FFCCFF", + "Einkauf": "#FF99CC", + "Postfachverwaltung": "#FFE6CC", + "Mitgliederverwaltung": "#FFCC99", + "Admins": "#E9E0CB", + "Ehrenamtlicher": "#FF9999", + "Mitglieder": "#FF6666", + "Community": "#FF3333", + "Interessierte": "#B266FF", + "Kund:innen": "#7F00FF", + } + self.color_to_role = {v: k for k, v in self.role_to_color.items()} + if len(self.color_to_role) != len(self.role_to_color): + print("+++ Error: duplicate color in role_to_color") + + def one_item(self, elem): + """ + Analyze one item in the XML file + :param elem: the item to analyze + """ + has_value = "value" in elem.attrib + text = "" + if has_value: + text = elem.attrib["value"] + if text != "" and text not in self.texts: + self.texts.append(text) + if "style" in elem.attrib: + style = elem.attrib["style"] + if style.startswith("ellipse"): + self.start_end += 1 + elif style.startswith("rhombus"): + self.decisions += 1 + elif style.find("shape=callout") >= 0: + self.comments += 1 + elif style.find("shape=delay") >= 0: + self.delays += 1 + elif style.find("shape=manualInput") >= 0: + self.manual_inputs += 1 + elif style.find("shape=note") >= 0: + self.documents += 1 + elif style.startswith("shape=process"): + self.subProcesses[text] = 1 + elif style.startswith("shape=parallelogram"): + self.subProcesses[text] = 1 + elif style.startswith("text;"): + if style.find("fontSize=24") >= 0: + self.title = text + elif style.find("shape=mxgraph.flowchart.stored_data") >= 0: + if text not in self.data_sources: + self.data_sources.append(text) + elif style.startswith("rounded=1"): + self.actions += 1 + # if style.find('fillColor=') >= 0: + matcher = self.regColor.search(style) + if matcher: + color = matcher.group(1).upper() + if color in self.color_to_role: + role = self.color_to_role[color] + if role not in self.roles: + self.roles[role] = 1 + else: + self.roles[role] += 1 + + def analyze_mxCell(self): + """ + Analyze the mxCell elements in the XML file + """ + tree = ET.parse(self.xml_file) + root = tree.getroot() + + for elem in root.iter("mxCell"): + self.one_item(elem) + + def print_results(self): + """ + Print the results of the analysis + """ + print(f"Title: {self.title}") + print(f"Actions: {self.actions}") + print(f"Comments: {self.comments}") + print(f"Decisions: {self.decisions}") + print(f"Delays: {self.delays}") + print(f"Documents: {self.documents}") + print(f"Manual inputs: {self.manual_inputs}") + print(f"Start/End: {self.start_end}") + print(f"Roles ({len(self.roles.keys())}):") + keys = self.roles.keys() + keys = sorted(keys, key=lambda x: x.lower()) + for key in keys: + print(f" {key}") + print(f"SubProcesses ({len(self.subProcesses)}):") + keys = self.subProcesses.keys() + keys = sorted(keys, key=lambda x: x.lower()) + for key in self.subProcesses: + key = self.stripText(key) + print(f" {key}") + print(f"Data sources ({len(self.data_sources)}):") + keys = self.data_sources + keys = sorted(keys, key=lambda x: x.lower()) + for key in self.data_sources: + key = self.stripText(key) + print(f" {key}") + print(f"Texts ({len(self.texts)}):") + texts = self.texts + texts = sorted(texts, key=lambda x: x.lower()) + for text in texts: + text = self.stripText(text) + print(f" {text}") + + def stripText(self, text: str) -> str: + """ + Strip the text from unwanted HTML elements + :param text: the text to strip + :return: the stripped text + """ + text = text.replace("-
", "") + text = text.replace("
", "") + text = text.replace("
", "") + text = text.replace("
", "") + text = text.replace("
", "") + text = text.replace(" ", " ") + return text + + +def main(argv: list): + """ + Main program + :param argv: the list of command line arguments (without the program name) + """ + xml_file = argv[0] if len(argv) > 0 else "data/drawio.xml" + if not os.path.exists(xml_file): + print(f"File {xml_file} not found") + else: + analyse = AnalyseDrawio(xml_file) + analyse.analyze_mxCell() + analyse.print_results() + + +if __name__ == "__main__": + main(sys.argv[1:])