badgeteam/Hatchery

View on GitHub
app/Models/Project.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

declare(strict_types=1);

namespace App\Models;

use App\Support\Helpers;
use Database\Factories\ProjectFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Mail\Markdown;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Str;

/**
 * Class Project.
 *
 * @author annejan@badge.team
 * @property int         $id
 * @property int         $category_id
 * @property int         $user_id
 * @property string      $name
 * @property string      $slug
 * @property int|null    $min_firmware
 * @property int|null    $max_firmware
 * @property string|null $git
 * @property string|null $git_commit_id
 * @property Carbon|null $published_at
 * @property Carbon|null $deleted_at
 * @property Carbon      $created_at
 * @property Carbon      $updated_at
 * @property int         $download_counter
 * @property string      $license
 * @property bool        $allow_team_fixes
 * @property string      $project_type
 * @property-read Collection|Badge[] $badges
 * @property-read int|null $badges_count
 * @property-read string $category
 * @property-read Collection|Project[] $dependants
 * @property-read int|null $dependants_count
 * @property-read Collection|Project[] $dependencies
 * @property-read int|null $dependencies_count
 * @property-read string|null $description
 * @property-read string|null $description_html
 * @property-read string $revision
 * @property-read float $score
 * @property-read int $size_of_content
 * @property-read string $size_of_content_formatted
 * @property-read int $size_of_zip
 * @property-read string $size_of_zip_formatted
 * @property-read string $status
 * @property-read Collection|BadgeProject[] $states
 * @property-read int|null $states_count
 * @property-read User $user
 * @property-read string $author
 * @property-read Collection|Version[] $versions
 * @property-read int|null $versions_count
 * @property-read Collection|Vote[] $votes
 * @property-read int|null $votes_count
 * @property-read Collection|Warning[] $warnings
 * @property-read int|null $warnings_count
 * @property-read Collection|User[] $collaborators
 * @property-read int|null $collaborators_count
 * @property-read array $types
 * @method static bool|null forceDelete()
 * @method static Builder|Project newModelQuery()
 * @method static Builder|Project newQuery()
 * @method static Builder|Project onlyTrashed()
 * @method static Builder|Project query()
 * @method static bool|null restore()
 * @method static Builder|Project whereCategoryId($value)
 * @method static Builder|Project whereCreatedAt($value)
 * @method static Builder|Project whereDeletedAt($value)
 * @method static Builder|Project whereDownloadCounter($value)
 * @method static Builder|Project whereGit($value)
 * @method static Builder|Project whereGitCommitId($value)
 * @method static Builder|Project whereId($value)
 * @method static Builder|Project whereName($value)
 * @method static Builder|Project wherePublishedAt($value)
 * @method static Builder|Project whereSlug($value)
 * @method static Builder|Project whereUpdatedAt($value)
 * @method static Builder|Project whereUserId($value)
 * @method static Builder|Project withTrashed()
 * @method static Builder|Project withoutTrashed()
 * @method static Builder|Project whereMaxFirmware($value)
 * @method static Builder|Project whereMinFirmware($value)
 * @method static Builder|Project whereAllowTeamFixes($value)
 * @method static Builder|Project whereProjectType($value)
 * @method static Builder|Project whereLicense($value)
 * @method static ProjectFactory factory(...$parameters)
 * @mixin \Eloquent
 */
class Project extends Model
{
    use SoftDeletes;
    use HasFactory;

    /**
     * Create with these.
     *
     * @var array<string>
     */
    protected $fillable = [
        'name',
        'category_id',
    ];

    /**
     * Appended magic data.
     *
     * @var array<string>
     */
    protected $appends = [
        'revision',
        'size_of_zip',
        'size_of_content',
        'category',
        'description',
        'status',
        'author',
    ];

    /**
     * Hidden data.
     *
     * @var array<string>
     */
    protected $hidden = [
        'created_at',
        'updated_at',
        'deleted_at',
        'user_id',
        'id',
        'category_id',
        'pivot',
        'versions',
        'states',
        'git',
        'git_commit_id',
        'user',
        'allow_team_fixes',
        'project_type',
        'license',
        'license_name',
        'license_url',
    ];

    /**
     * DateTime conversion for these fields.
     *
     * @var array<string>
     */
    protected $dates = [
        'created_at', 'updated_at', 'deleted_at', 'published_at',
    ];

    /**
     * Forbidden names for apps.
     *
     * @var array<string>
     */
    public static $forbidden = [
        'os', 'uos', 'badge', 'esp32', 'ussl', 'time', 'utime', 'splash', 'launcher', 'installer', 'ota_update',
        'boot', 'appglue', 'database', 'dialogs', 'deepsleep', 'magic', 'ntp', 'rtcmem', 'machine', 'setup', 'version',
        'wifi', 'woezel', 'network', 'socket', 'uhashlib', 'hashlib', 'ugfx', 'btree', 'request', 'urequest', 'uzlib',
        'zlib', 'ssl', 'create', 'delete', 'system', 'categories', 'devices', 'types'
    ];

    /**
     * Magical methods that associate a user and make sure projects have an empty __init__.py added.
     */
    public static function boot(): void
    {
        parent::boot();

        static::creating(
            function ($project) {
                if ($project->user_id === null) {
                    $user = Auth::guard()->user();
                    $project->user()->associate($user);
                }
            }
        );

        static::created(
            function ($project) {
                $version = new Version();
                $version->revision = 1;
                $version->project()->associate($project);
                $version->save();
                if ($project->git === null) {
                    // add first empty python file :)
                    $file = new File();
                    $file->name = '__init__.py';
                    $file->content = '';
                    $file->version()->associate($version);
                    $file->save();
                }
            }
        );

        static::saving(
            function ($project) {
                $project->slug = Str::slug($project->name, '_');
                if (self::isForbidden($project->slug)) {
                    throw new \Exception('reserved name');
                }
            }
        );
    }

    /**
     * Get the User that owns the Project.
     *
     * @return BelongsTo
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class)->withTrashed();
    }

    /**
     * Get the Category this Project belongs to.
     *
     * @return BelongsTo
     */
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class)->withTrashed();
    }

    /**
     * Get the Versions this Project has.
     *
     * @return HasMany
     */
    public function versions(): HasMany
    {
        return $this->hasMany(Version::class);
    }

    /**
     * Get the Votes this Project has.
     *
     * @return HasMany
     */
    public function votes(): HasMany
    {
        return $this->hasMany(Vote::class);
    }

    /**
     * Get the Warnings for the Project.
     *
     * @return HasMany
     */
    public function warnings(): HasMany
    {
        return $this->hasMany(Warning::class);
    }

    /**
     * Get the BadgeProjects for the Project.
     * This contains support state per badge.
     *
     * @return HasMany
     */
    public function states(): HasMany
    {
        return $this->hasMany(BadgeProject::class);
    }

    /**
     * Collaborators.
     *
     * @return BelongsToMany
     */
    public function collaborators(): BelongsToMany
    {
        return $this->belongsToMany(User::class)->withTimestamps();
    }

    /**
     * @return string|null
     */
    public function getRevisionAttribute(): ?string
    {
        $version = $this->versions()->published()->get()->last();

        return $version === null ? null : (string) $version->revision;
    }

    /**
     * @return BelongsToMany
     */
    public function dependencies(): BelongsToMany
    {
        return $this->belongsToMany(self::class, 'dependencies', 'project_id', 'depends_on_project_id')
            ->withTimestamps();
    }

    /**
     * @return BelongsToMany
     */
    public function dependants(): BelongsToMany
    {
        return $this->belongsToMany(self::class, 'dependencies', 'depends_on_project_id', 'project_id')
            ->withTimestamps();
    }

    /**
     * @return BelongsToMany
     */
    public function badges(): BelongsToMany
    {
        return $this->belongsToMany(Badge::class)->withTimestamps();
    }

    /**
     * @return int|null
     */
    public function getSizeOfZipAttribute(): ?int
    {
        $version = $this->versions()->published()->get()->last();

        return $version === null ? null : (int) $version->size_of_zip;
    }

    /**
     * @return int|null
     */
    public function getSizeOfContentAttribute(): ?int
    {
        $version = $this->versions()->published()->get()->last();
        if ($version === null) {
            $version = $this->versions->last();
        }
        /** @var Version $version */
        $size = 0;
        foreach ($version->files as $file) {
            $size += strlen($file->content);
        }

        return $size;
    }

    /**
     * @return string
     */
    public function getSizeOfContentFormattedAttribute(): string
    {
        return Helpers::formatBytes((int) $this->getSizeOfContentAttribute());
    }

    /**
     * @return string
     */
    public function getSizeOfZipFormattedAttribute(): string
    {
        return Helpers::formatBytes((int) $this->getSizeOfZipAttribute());
    }

    /**
     * Get the route key for the model.
     *
     * @return string
     */
    public function getRouteKeyName(): string
    {
        return 'slug';
    }

    /**
     * @return string|null
     */
    public function getCategoryAttribute(): ?string
    {
        /** @var Category|null $category */
        $category = $this->category()->first();
        if ($category === null) {
            return 'uncategorised';
        }

        return $category->slug;
    }

    /**
     * @param string $slug
     *
     * @return bool
     */
    public static function isForbidden(string $slug): bool
    {
        return in_array($slug, self::$forbidden);
    }

    /**
     * @return string|null
     */
    public function getDescriptionAttribute(): ?string
    {
        $full = true;
        $request = request();
        if ($request->has('description') && $request->description === false) {
            $full = false;
        }

        /** @var Version|null $version */
        $version = $this->versions->last();
        if ($version && $version->files()->where('name', 'like', 'README.md')->count() === 1) {
            /** @var File $file */
            $file = $version->files()->where('name', 'like', 'README.md')->first();

            if ($full) {
                return $file->content;
            }

            return Str::limit((string) $file->content, 16);
        }

        return null;
    }

    /**
     * @return HtmlString|null
     */
    public function getDescriptionHtmlAttribute(): ?HtmlString
    {
        if ($this->description) {
            return Markdown::parse($this->description);
        }

        return null;
    }

    /**
     * @return bool|null
     */
    public function userVoted(): ?bool
    {
        /** @var User|null $user */
        $user = Auth::guard()->user();
        if ($user === null) {
            return null;
        }

        return $this->votes()->where('user_id', $user->id)->exists();
    }

    /**
     * @return string
     */
    public function getStatusAttribute(): string
    {
        foreach (['working', 'in_progress', 'broken'] as $status) {
            if ($this->states()->where('status', $status)->exists()) {
                return $status;
            }
        }

        return 'unknown';
    }

    /**
     * @return bool
     */
    public function hasValidIcon(): bool
    {
        /** @var Version $version */
        $version = $this->versions->last();
        /** @var File|null $file */
        $file = $version->files()->where('name', 'icon.png')->get()->last();
        if ($file === null) {
            return false;
        }

        return $file->isValidIcon();
    }

    /**
     * @return float
     */
    public function getScoreAttribute(): float
    {
        if ($this->votes === null || $this->votes->count() === 0) {
            return 0;
        }
        $score = 0;
        foreach ($this->votes as $vote) {
            if ($vote->type === 'up') {
                $score++;
            }
            if ($vote->type === 'down') {
                $score--;
            }
        }

        return $score / $this->votes->count();
    }

    /**
     * @return string
     */
    public function getAuthorAttribute(): string
    {
        if (empty($this->user->name)) {
            return 'Unknown';
        }

        return $this->user->name;
    }

    /**
     * @return Version
     */
    public function getUnpublishedVersion(): Version
    {
        /** @var Version|null $version */
        $version = $this->versions()->unPublished()->first();
        if ($version === null) {
            /** @var Version $previousVersion */
            $previousVersion = $this->versions->last();
            $revision = $previousVersion->revision + 1;
            $version = new Version();
            $version->user_id = $this->user_id;
            $version->revision = $revision;
            $version->project()->associate($this);
            $version->save();
        }

        return $version;
    }

    /**
     * @return array<string>
     */
    public function getTypesAttribute(): array
    {
        // @TODO project specific types
        return array_keys(Badge::$types);
    }

    /**
     * @return string
     */
    public function getLicenseNameAttribute(): string
    {
        if ($this->license === null) {
            return 'Commercial';
        }
        /** @var License|null $license */
        $license = License::where('LicenseId', $this->license)->first();
        if ($license === null) {
            return $this->license ;
        }
        return $license->name;
    }

    /**
     * @return string
     */
    public function getLicenseUrlAttribute(): string
    {
        if ($this->license === null) {
            return '';
        }
        /** @var License|null $license */
        $license = License::where('LicenseId', $this->license)->first();
        if ($license === null) {
            return $this->license ;
        }
        return $license->reference;
    }
}