Quellcode durchsuchen

Merge branch 'container-management' into dev

moell vor 10 Monaten
Ursprung
Commit
080c084c4c

+ 172 - 0
app/Http/Controllers/API/ContainerController.php

@@ -0,0 +1,172 @@
+<?php
+
+namespace App\Http\Controllers\API;
+
+use App\Http\Controllers\Controller;
+use App\Http\Requests\API\Container\CreateOrUpdateRequest;
+use App\Http\Resources\API\ContainerDetailResource;
+use App\Models\Container;
+use App\Models\ContainerContent;
+use App\Models\Enums\ActionObjectType;
+use App\Models\Enums\FileObjectType;
+use App\Models\Enums\ObjectAction;
+use App\Models\File;
+use App\Repositories\ActionRepository;
+use App\Repositories\CustomFieldRepository;
+use App\Services\File\FileAssociationService;
+use App\Services\File\ImageUrlService;
+use App\Services\History\ModelChangeDetector;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+
+class ContainerController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     */
+    public function index()
+    {
+        //
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     */
+    public function store(
+        CreateOrUpdateRequest $request,
+        ImageUrlService $imageUrlService,
+        FileAssociationService $service,
+        CustomFieldRepository $customFieldRepo
+    )
+    {
+        $formData = [
+            ...$request->all(),
+            'company_id' => Auth::user()->company_id,
+            'created_by' => Auth::id(),
+            'whitelist' => $request->whitelist ? sprintf(",%s,", implode(',', $request->whitelist)) : null,
+        ];
+
+        if ($request->has("naming_rule_id") && $request->get("naming_rule_id") > 0) {
+            $keys = $customFieldRepo->keysByGroup($request->get("naming_rule_id"));
+            $formData['naming_rules'] = $request->only($keys);
+        }
+
+        $container = new Container();
+        $container->mergeFillable(['company_id']);
+        $container->fill($formData);
+        $container->save();
+
+        ActionRepository::createByContainer($container, ObjectAction::CREATED);
+
+        $service->association(
+            $request->get("file_ids", []),
+            $container->id,
+            FileObjectType::CONTAINER
+        );
+
+        $files = File::query()->where('object_id', $container->id)
+            ->where('object_type', ActionObjectType::CONTAINER)
+            ->where('source', 1)
+            ->pluck("id")
+            ->sort();
+
+        $contentFormData = [
+            'description' => $imageUrlService->interceptImageUrl($request->description),
+            'container_id' => $container->id,
+            'created_by' => Auth::id(),
+            'name' => $request->name,
+            'files' => $files->implode(",") ?: null
+        ];
+
+        ContainerContent::query()->create($contentFormData);
+
+        return $this->created();
+    }
+
+    /**
+     * Display the specified resource.
+     */
+    public function show(string $id)
+    {
+        $container = Container::query()->allowed()
+            ->when(\request("version") > 0, fn($query) => $query->where("version", ">=", \request("version")))
+            ->findOrFail($id);
+
+        return new ContainerDetailResource($container);
+    }
+
+    /**
+     * Update the specified resource in storage.
+     */
+    public function update(
+        CreateOrUpdateRequest $request,
+        ImageUrlService $imageUrlService,
+        CustomFieldRepository $customFieldRepo,
+        string $id
+    )
+    {
+        $container = Container::query()->allowed()->findOrFail($id);
+
+        $formData = [
+            ...$request->all(),
+            'whitelist' => $request->whitelist ? sprintf(",%s,", implode(',', $request->whitelist)) : null,
+            'description' => $imageUrlService->interceptImageUrl($request->description) ,
+        ];
+
+        if ($request->has("naming_rule_id") && $request->get("naming_rule_id") > 0) {
+            $keys = $customFieldRepo->keysByGroup($request->get("naming_rule_id"));
+            $formData['naming_rules'] = $request->only($keys);
+        }
+
+        $container->fill($formData);
+        $changes = ModelChangeDetector::detector(ActionObjectType::CONTAINER, $container);
+
+        $files = File::query()->where('object_id', $container->id)
+            ->where('object_type', ActionObjectType::CONTAINER)
+            ->where('source', 1)
+            ->pluck("id")
+            ->sort();
+
+        $contentFormData = [
+            'description' => $imageUrlService->interceptImageUrl($request->description),
+            'name' => $request->name,
+            'files' => $files->implode(",") ?: null
+        ];
+
+        $containerContent = $container->content;
+        $containerContent->fill($contentFormData);
+        $contentChange = ModelChangeDetector::detector(ActionObjectType::CONTAINER_CONTENT, $containerContent);
+
+        if ($contentChange) {
+            $container->version++;
+            $changes = array_merge($changes, $contentChange);
+
+            ContainerContent::query()->create([
+                ...$contentFormData,
+                'container_id' => $container->id,
+                'created_by' => Auth::id(),
+                'version' => $container->version,
+            ]);
+        }
+
+        $container->save();
+
+        ActionRepository::createByContainer($container, ObjectAction::EDITED, objectChanges: $changes);
+
+        return $this->noContent();
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     */
+    public function destroy(string $id)
+    {
+        $container = Container::query()->allowed()->findOrFail($id);
+
+        $container->delete();
+
+        ActionRepository::createByContainer($container, ObjectAction::DELETED);
+
+        return $this->noContent();
+    }
+}

+ 66 - 0
app/Http/Requests/API/Container/CreateOrUpdateRequest.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace App\Http\Requests\API\Container;
+
+use App\Http\Requests\CustomFieldRuleHelper;
+use App\Http\Requests\NamingRuleHelper;
+use App\Http\Requests\RuleHelper;
+use App\Models\Enums\ContainerACL;
+use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Validation\Rule;
+use Illuminate\Validation\Rules\Enum;
+
+class CreateOrUpdateRequest extends FormRequest
+{
+    use RuleHelper, CustomFieldRuleHelper, NamingRuleHelper;
+
+    /**
+     * Determine if the user is authorized to make this request.
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
+     */
+    public function rules(): array
+    {
+        $rules =  [
+            'library_id' => [
+                'required',
+                Rule::exists('libraries', 'id')->where($this->userCompanyWhere()),
+            ],
+            'naming_rule_id' => [
+                Rule::when($this->get('naming_rule_id') > 0, [
+                    Rule::exists('naming_rules', 'id')->whereIn('company_id', [
+                        0, Auth::user()->company_id,
+                    ]),
+                ])
+            ],
+            'name' => 'required|max:150',
+            'email_subject' => 'max:255',
+            'acl' => [
+                new Enum(ContainerACL::class),
+            ],
+            'whitelist' => $this->usersCompanyRules(),
+            'mailto' => $this->usersCompanyRules(),
+        ];
+
+        $containerRules = $this->customFieldRuleByGroup("container", ['doc_type', 'doc_stage']);
+
+        if ($this->has("naming_rule_id") && $this->get("naming_rule_id") > 0) {
+            $this->namingRuleCheck($this->get("naming_rule_id"));
+
+            $namingRules = $this->customFieldRuleByGroup($this->get("naming_rule_id"));
+
+            $rules = [...$rules, ... $namingRules];
+        }
+
+        return [...$containerRules, ...$rules];
+    }
+}

+ 36 - 0
app/Http/Resources/API/ContainerDetailResource.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace App\Http\Resources\API;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class ContainerDetailResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        $content = $this->content($request->get("version", 0))->first();
+
+        return [
+            'id' => $this->id,
+            'name' => $content?->name,
+            'library' => new LibrarySimpleResource($this->library),
+            'naming_rule' => new NamingRuleSimpleResource($this->namingRule),
+            "mailto"  => $this->mailto,
+            "email_subject"  => $this->email_subject,
+            "doc_stage"  => $this->doc_stage,
+            "doc_type"  => $this->doc_type,
+            "description"  => $content?->description ? (new \App\Services\File\ImageUrlService)->getImageUrl($content?->description) : "",
+            "acl"  => $this->acl,
+            "whitelist"  => $this->whitelist,
+            "version" => $this->version,
+            "created_at"  => (string)$this->created_at,
+            "created_by" => new UserProfileResource($this->createdBy),
+        ];
+    }
+}

+ 22 - 0
app/Http/Resources/API/LibrarySimpleResource.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Http\Resources\API;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class LibrarySimpleResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        return [
+            'id' => $this->id,
+            'name' => $this->name,
+        ];
+    }
+}

+ 16 - 0
app/ModelFilters/ContainerFilter.php

@@ -0,0 +1,16 @@
+<?php 
+
+namespace App\ModelFilters;
+
+use EloquentFilter\ModelFilter;
+
+class ContainerFilter extends ModelFilter
+{
+    /**
+    * Related Models that have ModelFilters as well as the method on the ModelFilter
+    * As [relationMethod => [input_key1, input_key2]].
+    *
+    * @var array
+    */
+    public $relations = [];
+}

+ 56 - 0
app/Models/Container.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace App\Models;
+
+use App\Models\Scopes\CompanyScope;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
+
+class Container extends Model
+{
+    use HasFactory, SoftDeletes;
+
+    protected $fillable = [
+        'name', 'library_id', 'naming_rule_id', 'naming_rules', 'mailto', 'email_subject', 'acl', 'whitelist',
+        'doc_stage', 'doc_type'
+    ];
+
+    protected $casts = [
+        'mailto' => 'array',
+        'naming_rules' => 'array',
+    ];
+
+    protected static function booted(): void
+    {
+        static::addGlobalScope(new CompanyScope);
+    }
+
+    public function scopeAllowed(Builder $query, string $id = null)
+    {
+
+    }
+
+    public function library(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+    {
+        return $this->belongsTo(Library::class, "library_id");
+    }
+
+    public function namingRule(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+    {
+        return $this->belongsTo(NamingRule::class);
+    }
+
+    public function createdBy(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+    {
+        return $this->belongsTo(User::class, 'created_by');
+    }
+
+    public function content(int $version = 0): \Illuminate\Database\Eloquent\Relations\HasOne
+    {
+        $version = $version > 0 ? $version : $this->version;
+
+        return $this->hasOne(ContainerContent::class, "container_id")->where("version", $version);
+    }
+}

+ 15 - 0
app/Models/ContainerContent.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class ContainerContent extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'container_id', 'description', 'files', 'version', 'name', 'created_by',
+    ];
+}

+ 11 - 0
app/Models/Enums/ActionObjectType.php

@@ -3,10 +3,13 @@
 namespace App\Models\Enums;
 
 use App\Models\Asset;
+use App\Models\Container;
 use App\Models\Plan;
 use App\Models\Project;
 use App\Models\Requirement;
 use App\Models\Task;
+use App\Services\History\Detector\ContainerContentDetector;
+use App\Services\History\Detector\ContainerDetector;
 use App\Services\History\Detector\ProjectDetector;
 use App\Services\History\Detector\RequirementDetector;
 use App\Services\History\Detector\TaskDetector;
@@ -23,6 +26,10 @@ enum ActionObjectType: string
 
     case PLAN = "plan";
 
+    case CONTAINER = "container";
+
+    case CONTAINER_CONTENT = "container_content";
+
     public function modelBuilder(): \Illuminate\Database\Eloquent\Builder
     {
         return match ($this) {
@@ -31,6 +38,7 @@ enum ActionObjectType: string
             self::TASK => Task::query(),
             self::PLAN => Plan::query(),
             self::REQUIREMENT => Requirement::query(),
+            self::CONTAINER => Container::query(),
         };
     }
 
@@ -42,6 +50,7 @@ enum ActionObjectType: string
             self::TASK => Task::query()->allowed($id),
             self::PLAN => Plan::query(),
             self::REQUIREMENT => Requirement::query(),
+            self::CONTAINER => Container::query()->allowed($id),
         };
     }
 
@@ -60,6 +69,8 @@ enum ActionObjectType: string
             ActionObjectType::PROJECT => ProjectDetector::class,
             ActionObjectType::REQUIREMENT => RequirementDetector::class,
             ActionObjectType::TASK => TaskDetector::class,
+            ActionObjectType::CONTAINER => ContainerDetector::class,
+            ActionObjectType::CONTAINER_CONTENT => ContainerContentDetector::class,
             default => null
         };
     }

+ 10 - 0
app/Models/Enums/ContainerACL.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models\Enums;
+
+enum ContainerACL: string
+{
+    case PRIVATE = 'private';
+
+    case CUSTOM = 'custom';
+}

+ 4 - 2
app/Models/Enums/CustomFieldGroup.php

@@ -4,7 +4,9 @@ namespace App\Models\Enums;
 
 enum CustomFieldGroup: string
 {
-    case PRIVATE = 'private';
+    case TASK = 'task';
 
-    case CUSTOM = 'custom';
+    case PROJECT = 'project';
+
+    case CONTAINER = 'container';
 }

+ 2 - 0
app/Models/Enums/FileObjectType.php

@@ -23,6 +23,8 @@ enum FileObjectType: string
 
     case PLAN = "plan";
 
+    case CONTAINER = "container";
+
     public function modelBuilder(): \Illuminate\Database\Eloquent\Builder
     {
         return match ($this) {

+ 20 - 0
app/Repositories/ActionRepository.php

@@ -4,6 +4,7 @@ namespace App\Repositories;
 
 use App\Events\ObjectActionCreate;
 use App\Models\Action;
+use App\Models\Container;
 use App\Models\Enums\ActionObjectType;
 use App\Models\Enums\ObjectAction;
 use App\Models\History;
@@ -108,6 +109,25 @@ class ActionRepository
         );
     }
 
+    public static function createByContainer(
+        Container    $container,
+        ObjectAction $action,
+        string       $comment = null,
+        array        $extraFields = [],
+        array        $objectChanges = [],
+    ): \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Builder
+    {
+        return self::create(
+            $container->id,
+            ActionObjectType::CONTAINER,
+            $action,
+            $container->library?->project_id,
+            $comment,
+            $extraFields,
+            $objectChanges,
+        );
+    }
+
     public static function latestDynamic(Project $project)
     {
         $actions = Action::query()

+ 10 - 1
app/Services/File/ImageUrlService.php

@@ -16,7 +16,12 @@ use Illuminate\Support\Str;
 
 class ImageUrlService
 {
-    public function interceptImageUrl(string $comment){
+    public function interceptImageUrl(string $comment): bool|string|null
+    {
+        if (! $comment) {
+            return null;
+        }
+
         $newComment = null;
         $dom = new DOMDocument();
         libxml_use_internal_errors(true); // 禁用错误报告
@@ -48,6 +53,10 @@ class ImageUrlService
     }
 
     public function getImageUrl(string $comment){
+        if (! $comment) {
+            return $comment;
+        }
+
         $newComment = null;
         preg_match_all('/<img\s+[^>]*src=[\'"]([^\'"]*)[\'"][^>]*>/i', $comment, $matches);
         $ids = [];

+ 10 - 0
app/Services/History/Converter/ContainerConverter.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Services\History\Converter;
+
+use App\Models\Container;
+
+class ContainerConverter extends ModelConverter
+{
+    protected static string $modelClassName = Container::class;
+}

+ 30 - 0
app/Services/History/Detector/ContainerContentDetector.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Services\History\Detector;
+
+
+class ContainerContentDetector extends DetectorAbstract
+{
+    public static function fields(): array
+    {
+        return [
+            'name',
+            'description',
+            'files',
+        ];
+    }
+
+    public static function diffFields(): array
+    {
+        return [
+            'description'
+        ];
+    }
+
+    public static function converters(): array
+    {
+        return [
+
+        ];
+    }
+}

+ 56 - 0
app/Services/History/Detector/ContainerDetector.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace App\Services\History\Detector;
+
+use App\Models\Enums\CustomFieldGroup;
+use App\Services\History\Converter\ContainerConverter;
+use App\Services\History\Converter\CustomFieldSelectConverter;
+use App\Services\History\Converter\EmailConverter;
+use App\Services\History\Converter\ModelEnumConverter;
+use App\Services\History\Converter\NamingRuleConverter;
+use App\Services\History\Converter\WhitelistConverter;
+
+class ContainerDetector extends DetectorAbstract
+{
+    public static function fields(): array
+    {
+        return [
+            'library_id',
+            'naming_rule_id',
+            'task_type',
+            'doc_stage',
+            'acl',
+            'whitelist',
+            'description',
+            'mailto',
+            'email_subject',
+        ];
+    }
+
+    public static function diffFields(): array
+    {
+        return [
+            'description',
+        ];
+    }
+
+    public static function arrayFields(): array
+    {
+        return [
+            'mailto',
+        ];
+    }
+
+    public static function converters(): array
+    {
+        return [
+            "whitelist" => new WhitelistConverter(),
+            "acl" => new ModelEnumConverter("container.acl"),
+            "doc_stage" => new CustomFieldSelectConverter(CustomFieldGroup::CONTAINER->value, "doc_stage"),
+            "doc_type" => new CustomFieldSelectConverter(CustomFieldGroup::CONTAINER->value, "doc_type"),
+            "naming_rule_id" => new NamingRuleConverter(),
+            "library_id" => new ContainerConverter(),
+            "mailto" => new EmailConverter(),
+        ];
+    }
+}

+ 1 - 0
config/custom-field.php

@@ -7,5 +7,6 @@ return [
     'groups' => [
         'task',
         'project',
+        'container',
     ]
 ];

+ 41 - 0
database/migrations/2024_04_14_105925_create_containers_table.php

@@ -0,0 +1,41 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::create('containers', function (Blueprint $table) {
+            $table->id();
+            $table->string("name", 150);
+            $table->integer("company_id");
+            $table->integer("library_id")->nullable();
+            $table->integer("naming_rule_id")->nullable();
+            $table->string("doc_stage", 50)->nullable();
+            $table->string("doc_type", 50)->nullable();
+            $table->json("naming_rules")->nullable();
+            $table->json('mailto')->nullable();
+            $table->string('email_subject')->nullable();
+            $table->string('acl')->default('private')->comment('private,custom');
+            $table->string("whitelist")->nullable();
+            $table->integer("created_by")->nullable();
+            $table->integer("version")->default(1);
+            $table->softDeletes();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('containers');
+    }
+};

+ 33 - 0
database/migrations/2024_04_17_211914_create_container_contents_table.php

@@ -0,0 +1,33 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::create('container_contents', function (Blueprint $table) {
+            $table->id();
+            $table->string("name", 150);
+            $table->integer("container_id");
+            $table->string("files")->nullable();
+            $table->text("description")->nullable();
+            $table->integer("created_by")->nullable();
+            $table->integer("version")->default(1);
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('container_contents');
+    }
+};

+ 7 - 0
lang/en/model-enums.php

@@ -4,6 +4,7 @@ use \App\Models\Enums\ProjectStatus;
 use \App\Models\Enums\RequirementStatus;
 use \App\Models\Enums\ProjectACL;
 use \App\Models\Enums\TaskACL;
+use \App\Models\Enums\ContainerACL;
 use \App\Models\Enums\TaskStatus;
 
 return [
@@ -44,4 +45,10 @@ return [
             TaskACL::CUSTOM->value => "Custom",
         ]
     ],
+    'container' => [
+        'acl' => [
+            ContainerACL::PRIVATE->value => "Private",
+            ContainerACL::CUSTOM->value => "Custom",
+        ]
+    ]
 ];

+ 1 - 0
routes/api.php

@@ -58,6 +58,7 @@ Route::middleware(['auth:sanctum'])->group(function () {
             'library' => API\LibraryController::class,
             'department' => API\DepartmentController::class,
             'user' => API\UserController::class,
+            'container' => API\ContainerController::class,
         ]);
         Route::apiResource("company", API\CompanyController::class)->only([
             'index', 'show'