From 10827d3592c125ad8dc03eab8edfce4eea68b57e Mon Sep 17 00:00:00 2001 From: Ando Roots Date: Mon, 19 Oct 2020 17:43:20 +0300 Subject: [PATCH 1/6] Add backend API CRUD for a new domain object - "Gigad" Gigad (an ad for a gig) is a public posting from Organizations; that they're willing to pick up offers for gigs of a certain type. This allows companies to hire performers. Ref #21 --- .../Controllers/Api/V1/GigadController.php | 116 ++++++++++++++++ .../Requests/Gigad/DeleteGigadRequest.php | 38 +++++ .../Http/Requests/Gigad/StoreGigadRequest.php | 16 +++ .../Requests/Gigad/UpdateGigadRequest.php | 28 ++++ src/app/Http/Resources/V1/GigadResource.php | 43 ++++++ src/app/Orm/GigCategory.php | 42 ++++++ src/app/Orm/GigCategoryTranslation.php | 32 +++++ src/app/Orm/Gigad.php | 104 ++++++++++++++ src/app/Orm/GigadTranslation.php | 26 ++++ src/app/Orm/Organization.php | 10 ++ src/app/Rules/ContainsMyOrganization.php | 15 +- .../factories/Orm/GigCategoryFactory.php | 29 ++++ src/database/factories/Orm/GigadFactory.php | 36 +++++ ...10_17_142151_create_gig_category_table.php | 45 ++++++ ...2020_10_17_142610_create_gig_ads_table.php | 56 ++++++++ src/database/seeders/DatabaseSeeder.php | 3 + src/database/seeders/GigCategorySeeder.php | 47 +++++++ src/database/seeders/GigadSeeder.php | 25 ++++ src/database/seeders/ProductionsSeeder.php | 4 +- src/database/seeders/UserSeeder.php | 4 +- src/resources/js/public/App.vue | 5 + src/resources/js/public/routes.js | 6 + src/resources/js/public/views/gigs/Form.vue | 83 +++++++++++ .../js/public/views/organizations/List.vue | 3 +- src/resources/lang/en/public.json | 5 +- src/resources/lang/et/public.json | 5 +- src/routes/api.php | 4 +- src/tests/Feature/Api/GigadTest.php | 131 ++++++++++++++++++ .../Unit/Rules/ContainsMyOrganizationTest.php | 6 +- 29 files changed, 950 insertions(+), 17 deletions(-) create mode 100644 src/app/Http/Controllers/Api/V1/GigadController.php create mode 100644 src/app/Http/Requests/Gigad/DeleteGigadRequest.php create mode 100644 src/app/Http/Requests/Gigad/StoreGigadRequest.php create mode 100644 src/app/Http/Requests/Gigad/UpdateGigadRequest.php create mode 100644 src/app/Http/Resources/V1/GigadResource.php create mode 100644 src/app/Orm/GigCategory.php create mode 100644 src/app/Orm/GigCategoryTranslation.php create mode 100644 src/app/Orm/Gigad.php create mode 100644 src/app/Orm/GigadTranslation.php create mode 100644 src/database/factories/Orm/GigCategoryFactory.php create mode 100644 src/database/factories/Orm/GigadFactory.php create mode 100644 src/database/migrations/2020_10_17_142151_create_gig_category_table.php create mode 100644 src/database/migrations/2020_10_17_142610_create_gig_ads_table.php create mode 100644 src/database/seeders/GigCategorySeeder.php create mode 100644 src/database/seeders/GigadSeeder.php create mode 100644 src/resources/js/public/views/gigs/Form.vue create mode 100644 src/tests/Feature/Api/GigadTest.php diff --git a/src/app/Http/Controllers/Api/V1/GigadController.php b/src/app/Http/Controllers/Api/V1/GigadController.php new file mode 100644 index 00000000..c4902b70 --- /dev/null +++ b/src/app/Http/Controllers/Api/V1/GigadController.php @@ -0,0 +1,116 @@ +allowedFilters(AllowedFilter::exact('is_public')) + ->orderBy('id', 'asc') + ->onlyMine($request->input('onlyMine', false)) + ->paginate(15); + + return GigadResource::collection($gigads); + } + + /** + * Create a new Gigad + * + * @param StoreGigadRequest $request + * @return GigadResource + * @authenticated + * @bodyParam description string Long (markdown-enabled) description of the organization + * @bodyParam is_public boolean Whether or not to show this publicly + * @bodyParam link string Link to "read more" + * @bodyParam int gig_category_id Numeric category ID + * @bodyParam string organization_uid Organization who owns this + */ + public function store(StoreGigadRequest $request) + { + + $gigad = new Gigad; + $gigad->link = $request->input('link'); + $gigad->description = $request->input('description'); + $gigad->organization_id = Organization::where('uid', $request->input('organization_uid'))->first()->id; + $gigad->gig_category_id = $request->input('gig_category_id'); + $gigad->setToken(); + $gigad->save(); + + return new GigadResource($gigad); + } + + /** + * Update an ad + * + * @param Gigad $gigad + * @param UpdateGigadRequest $request + * @return GigadResource + * @bodyParam description string Long (markdown-enabled) description of the organization + * @bodyParam is_public boolean Whether or not to show this publicly + * @bodyParam link string Link to "read more" + * @bodyParam int gig_category_id Numeric category ID + * @bodyParam string organization_uid Organization who owns this + * @authenticated + */ + public function update(Gigad $gigad, UpdateGigadRequest $request) + { + $gigad->link = $request->input('link'); + $gigad->description = $request->input('description'); + $gigad->organization_id = Organization::where('uid', $request->input('organization_uid'))->first()->id; + $gigad->gig_category_id = $request->input('gig_category_id'); + $gigad->save(); + + return new GigadResource($gigad); + } + + + /** + * Delete a Gigad + * + * @param Gigad $gigad + * @param DeleteGigadRequest $request + * @throws \Exception + * @authenticated + */ + public function destroy(Gigad $gigad, DeleteGigadRequest $request) + { + $gigad->delete(); + } +} diff --git a/src/app/Http/Requests/Gigad/DeleteGigadRequest.php b/src/app/Http/Requests/Gigad/DeleteGigadRequest.php new file mode 100644 index 00000000..47eb1ed6 --- /dev/null +++ b/src/app/Http/Requests/Gigad/DeleteGigadRequest.php @@ -0,0 +1,38 @@ +gigad->organization()->get()->pluck('id')->toArray(); + foreach (Auth::user()->organizations()->get() as $organization) { + if (in_array($organization->id, $gigadOrganizations)) { + return true; + } + } + return false; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + // + ]; + } +} diff --git a/src/app/Http/Requests/Gigad/StoreGigadRequest.php b/src/app/Http/Requests/Gigad/StoreGigadRequest.php new file mode 100644 index 00000000..51662e5b --- /dev/null +++ b/src/app/Http/Requests/Gigad/StoreGigadRequest.php @@ -0,0 +1,16 @@ + 'max:255|required|min:5|url', + 'description' => 'max:5000|required', + 'organization_uid' => ['required', 'exists:organizations,uid', new ContainsMyOrganization], + 'images.header.content' => ['nullable', new Base64HeaderImage], + 'is_public' => 'required|bool', + 'gig_category_id' => ['required', 'int', 'exists:gig_categories,id'] + ]; + } +} diff --git a/src/app/Http/Resources/V1/GigadResource.php b/src/app/Http/Resources/V1/GigadResource.php new file mode 100644 index 00000000..21496f41 --- /dev/null +++ b/src/app/Http/Resources/V1/GigadResource.php @@ -0,0 +1,43 @@ + $this['uid'], + 'category' => [ + 'id' => $this->category->id, + 'name' => $this->category->name + ], + 'organization' => [ + 'uid' => $this->organization->uid, + 'name' => $this->organization->name + ], + 'link'=> $this['link'], + 'description' => $this['description'] + ]; + } + +} diff --git a/src/app/Orm/GigCategory.php b/src/app/Orm/GigCategory.php new file mode 100644 index 00000000..9cc370ed --- /dev/null +++ b/src/app/Orm/GigCategory.php @@ -0,0 +1,42 @@ +hasMany('App\Orm\Gigad', 'gig_category_id'); + } +} diff --git a/src/app/Orm/GigCategoryTranslation.php b/src/app/Orm/GigCategoryTranslation.php new file mode 100644 index 00000000..8d0a0be1 --- /dev/null +++ b/src/app/Orm/GigCategoryTranslation.php @@ -0,0 +1,32 @@ + 'boolean', + ]; + + public function setNameAttribute($value) + { + $this->attributes['name'] = trim($value); + } + +} diff --git a/src/app/Orm/Gigad.php b/src/app/Orm/Gigad.php new file mode 100644 index 00000000..0f0e8cbb --- /dev/null +++ b/src/app/Orm/Gigad.php @@ -0,0 +1,104 @@ + DT_Unique, 'size' => 16, 'special_chr' => false]; + + public $translatedAttributes = ['link', 'description']; + + protected $dates = [ + 'created_at', + 'updated_at', + 'deleted_at' + ]; + + protected $fillable = ['description', 'link', 'is_public']; + + protected $casts = [ + 'is_public' => 'boolean', + ]; + + /** + * Get the route key for the model. + * + * @return string + */ + public function getRouteKeyName() + { + return 'uid'; + } + + public function organization() + { + return $this->belongsTo('App\Orm\Organization'); + } + + public function category() + { + return $this->belongsTo(GigCategory::class, 'gig_category_id'); + } + + /** + * @param Builder $query + * @param bool $enabled + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeOnlyPublic(Builder $query, bool $enabled) + { + if (!$enabled) { + return $query; + } + return $query->where('is_public', 1); + } + + /** + * @param Builder $query + * @param bool $enabled + * @return Builder + */ + public function scopeOnlyMine(Builder $query, bool $enabled = false): Builder + { + + if (!$enabled || !Auth::user()) { + return $query; + } + + // This is not the most performant or elegant way to implement it. + // Leaving it in for the sake of working MVP, until performance becomes an issue + // (future me: sorry!) + $organizationIds = Auth::user()->organizations()->get()->pluck('id')->toArray(); + + return $query->whereIn('organization_id', $organizationIds); + } +} diff --git a/src/app/Orm/GigadTranslation.php b/src/app/Orm/GigadTranslation.php new file mode 100644 index 00000000..f9f63f6a --- /dev/null +++ b/src/app/Orm/GigadTranslation.php @@ -0,0 +1,26 @@ + 'boolean', + ]; + +} diff --git a/src/app/Orm/Organization.php b/src/app/Orm/Organization.php index 28532af3..3bc9eec7 100644 --- a/src/app/Orm/Organization.php +++ b/src/app/Orm/Organization.php @@ -22,6 +22,8 @@ * @property string $email * @property string $uid * @property string $facebook_url + * @property string $name + * @property string $description * @property string $homepage_url * @property int $is_public */ @@ -63,6 +65,14 @@ public function users() return $this->belongsToMany('App\User')->withPivot(['role']); } + /** + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function gigads() + { + return $this->hasMany('App\Orm\Gigad'); + } + public function admins() { return $this->users() diff --git a/src/app/Rules/ContainsMyOrganization.php b/src/app/Rules/ContainsMyOrganization.php index ac2ef5af..90d385c0 100644 --- a/src/app/Rules/ContainsMyOrganization.php +++ b/src/app/Rules/ContainsMyOrganization.php @@ -9,6 +9,9 @@ /** * Validates that a list of Organizations (by uid) contains at least one Organization that the current user belongs to * + * If input value is not an array, it's taken as a list of exactly one Organization UID-s; + * and the rule validates if the given Organization is in the list of user-belonging orgs. + * * Input $value = ['da1aFa', 'aj7Fa'] * * @package App\Rules @@ -19,12 +22,16 @@ class ContainsMyOrganization implements Rule /** * Determine if the validation rule passes. * - * @param string $attribute - * @param mixed $value + * @param string $attribute + * @param mixed $value * @return bool */ public function passes($attribute, $value): bool { + + if (is_string($value)) { + $value = [$value]; + } if (!$value || !is_array($value) || !count($value) || !Auth::check()) { return false; } @@ -32,7 +39,7 @@ public function passes($attribute, $value): bool $myOrganizations = $this->geMyOrganizations(Auth::user()); foreach ($value as $organization) { - if (in_array($organization,$myOrganizations)) { + if (in_array($organization, $myOrganizations)) { return true; } } @@ -57,7 +64,7 @@ public function message() * @param User $user * @return array */ - private function geMyOrganizations(User $user):array + private function geMyOrganizations(User $user): array { return $user->organizations() ->get() diff --git a/src/database/factories/Orm/GigCategoryFactory.php b/src/database/factories/Orm/GigCategoryFactory.php new file mode 100644 index 00000000..744ff666 --- /dev/null +++ b/src/database/factories/Orm/GigCategoryFactory.php @@ -0,0 +1,29 @@ + $this->faker->words(3, true), + 'description' => $this->faker->sentence(30, true) + ]; + } +} diff --git a/src/database/factories/Orm/GigadFactory.php b/src/database/factories/Orm/GigadFactory.php new file mode 100644 index 00000000..7bd45257 --- /dev/null +++ b/src/database/factories/Orm/GigadFactory.php @@ -0,0 +1,36 @@ + $this->faker->url, + 'description' => $this->faker->sentence(30), + 'organization_id' => Organization::inRandomOrder()->first()->id, + 'gig_category_id' => GigCategory::inRandomOrder()->first()->id, + 'uid' => (string) Str::uuid(), + 'is_public'=>true + ]; + } +} diff --git a/src/database/migrations/2020_10_17_142151_create_gig_category_table.php b/src/database/migrations/2020_10_17_142151_create_gig_category_table.php new file mode 100644 index 00000000..e532b124 --- /dev/null +++ b/src/database/migrations/2020_10_17_142151_create_gig_category_table.php @@ -0,0 +1,45 @@ +id(); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('gig_category_translations', function (Blueprint $table) { + $table->increments('id'); + $table->bigInteger('gig_category_id')->unsigned(); + $table->string('name', 255); + $table->text('description')->nullable()->default(null); + $table->char('locale', 2)->index(); + $table->boolean('auto_translated')->default(false)->comment('If true, indicates this translation was made by a machine'); + $table->unique(['gig_category_id', 'locale']); + $table->foreign('gig_category_id')->references('id')->on('gig_categories')->onDelete('cascade'); + }); + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('gig_categories'); + Schema::dropIfExists('gig_category_translations'); + } +} diff --git a/src/database/migrations/2020_10_17_142610_create_gig_ads_table.php b/src/database/migrations/2020_10_17_142610_create_gig_ads_table.php new file mode 100644 index 00000000..ec4ced08 --- /dev/null +++ b/src/database/migrations/2020_10_17_142610_create_gig_ads_table.php @@ -0,0 +1,56 @@ +id(); + $table->string('uid', 64); + $table->bigInteger('gig_category_id')->unsigned(); + $table->boolean('is_public')->default(false); + $table->integer('organization_id')->unsigned(); + + $table->timestamps(); + $table->softDeletes(); + + $table->unique(['uid']); + $table->foreign('organization_id')->references('id') + ->on('organizations')->onDelete('cascade'); + $table->foreign('gig_category_id')->references('id') + ->on('gig_categories')->onDelete('cascade'); + }); + + Schema::create('gigad_translations', function (Blueprint $table) { + $table->increments('id'); + $table->bigInteger('gigad_id')->unsigned(); + $table->string('link', 255)->nullable()->default(null); + $table->text('description')->nullable()->default(null); + $table->char('locale', 2)->index(); + $table->boolean('auto_translated')->default(false)->comment('If true, indicates this translation was made by a machine'); + + $table->unique(['gigad_id', 'locale']); + $table->foreign('gigad_id')->references('id')->on('gigads')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('gigad_translations'); + Schema::dropIfExists('gigads'); + } +} diff --git a/src/database/seeders/DatabaseSeeder.php b/src/database/seeders/DatabaseSeeder.php index 7e71e91a..5bc46065 100644 --- a/src/database/seeders/DatabaseSeeder.php +++ b/src/database/seeders/DatabaseSeeder.php @@ -17,5 +17,8 @@ public function run() $this->call(OrganizationsSeeder::class); $this->call(ProductionsSeeder::class); + $this->call(GigCategorySeeder::class); + $this->call(GigadSeeder::class); + } } diff --git a/src/database/seeders/GigCategorySeeder.php b/src/database/seeders/GigCategorySeeder.php new file mode 100644 index 00000000..9fa46692 --- /dev/null +++ b/src/database/seeders/GigCategorySeeder.php @@ -0,0 +1,47 @@ + 'Etendused üritustele', + 'description' => 'TODO' + ],[ + 'name' => 'Töötoad', + 'description' => 'TODO' + ],[ + 'name' => 'Õhtujuhtimine', + 'description' => 'TODO' + ],[ + 'name' => 'Meeskonnatöö', + 'description' => 'TODO' + ],[ + 'name' => 'Impronäitleja', + 'description' => 'TODO' + ], + ]; + + foreach ($categories as $input) { + $category = new GigCategory; + $category->save(); + + $category->name = $input['name']; + $category->description = $input['description']; + $category->save(); + } + } +} diff --git a/src/database/seeders/GigadSeeder.php b/src/database/seeders/GigadSeeder.php new file mode 100644 index 00000000..c15c858d --- /dev/null +++ b/src/database/seeders/GigadSeeder.php @@ -0,0 +1,25 @@ +count(6)->make(); + foreach ($ads as $ad) { + $ad->organization_id = Organization::inRandomOrder()->first()->id; + $ad->save(); + } + } +} diff --git a/src/database/seeders/ProductionsSeeder.php b/src/database/seeders/ProductionsSeeder.php index 4a108079..5c7063d0 100644 --- a/src/database/seeders/ProductionsSeeder.php +++ b/src/database/seeders/ProductionsSeeder.php @@ -17,9 +17,9 @@ public function run() for ($i = 0; $i < 2; $i++) { - $production = factory(Production::class)->create(); + $production = Production::factory()->create(); - $event = factory(Event::class, rand(1, 4))->create(['production_id' => $production->id]); + $event = Event::factory()->count(rand(1, 4))->create(['production_id' => $production->id]); } } diff --git a/src/database/seeders/UserSeeder.php b/src/database/seeders/UserSeeder.php index 3367bbec..25b0465b 100644 --- a/src/database/seeders/UserSeeder.php +++ b/src/database/seeders/UserSeeder.php @@ -13,11 +13,11 @@ class UserSeeder extends Seeder */ public function run() { - factory(User::class ,2)->create(); + User::factory()->count(2)->create(); // Test user // Not present in prod - factory(User::class)->make([ + User::factory()->make([ 'username'=>'admin', 'password'=> Hash::make('Ajutine123') ])->save(); diff --git a/src/resources/js/public/App.vue b/src/resources/js/public/App.vue index 8219dec2..728d82ff 100644 --- a/src/resources/js/public/App.vue +++ b/src/resources/js/public/App.vue @@ -26,6 +26,11 @@ exact :to="{ name: 'organizations' }"> {{ $t("nav.organizations") }} + + + {{ $t("nav.gigs") }} +
+ + +

Etendused

+ + + + + +
+ + +
+ Card image cap + +
+
+
+ + +
+ + +

Alison Belmont

+ +
Graffiti Artist
+ +

Sed ut perspiciatis unde omnis iste natus sit voluptatem accusantium doloremque + laudantium, totam rem aperiam.

+ + + + + + + + +
+ +
+ + + + + +

Töötoad

+ +

Õhtujuht

+ +

Meeskonnakoolitus

+ +

Impronäitleja

+
+ + + diff --git a/src/resources/js/public/views/organizations/List.vue b/src/resources/js/public/views/organizations/List.vue index 83a97a53..720d8060 100644 --- a/src/resources/js/public/views/organizations/List.vue +++ b/src/resources/js/public/views/organizations/List.vue @@ -22,8 +22,7 @@ export default { }, data() { return { - organizations: [], - newOrganizationName: '', + organizations: [] }; }, mounted() { diff --git a/src/resources/lang/en/public.json b/src/resources/lang/en/public.json index cee991a7..faedf5e6 100644 --- a/src/resources/lang/en/public.json +++ b/src/resources/lang/en/public.json @@ -5,7 +5,8 @@ "home": "Home", "about_improv":"About improv", "what_is_improv":"What is improvised theater?", - "improv_history": "History in Estonia" + "improv_history": "History in Estonia", + "gigs": "Paid performances" }, "production": { "attr": { @@ -14,4 +15,4 @@ "description": "Description" } } -} \ No newline at end of file +} diff --git a/src/resources/lang/et/public.json b/src/resources/lang/et/public.json index ec0d3042..21cdc130 100644 --- a/src/resources/lang/et/public.json +++ b/src/resources/lang/et/public.json @@ -5,7 +5,8 @@ "about_improv": "Improvisatsioon", "what_is_improv":"Mis on improviseeritud teater?", "improv_history": "Impro ajalugu", - "local_improv": "Impro Eestis" + "local_improv": "Impro Eestis", + "gigs": "Tellimine" }, "production": { "attr": { @@ -14,4 +15,4 @@ "description": "Kirjeldus" } } -} \ No newline at end of file +} diff --git a/src/routes/api.php b/src/routes/api.php index 4e99fd74..80f513d1 100644 --- a/src/routes/api.php +++ b/src/routes/api.php @@ -42,7 +42,7 @@ Route::apiResource('users', 'UserController') ->only(['show']); - + Route::apiResource('gigads', 'GigadController')->only(['index', 'show']); }); Route::apiResource('images', 'ImageController', ['as' => 'api']) @@ -56,6 +56,8 @@ Route::apiResource('productions', 'ProductionController', ['as' => 'api'])->only(['store', 'destroy', 'update']); + Route::apiResource('gigads', 'GigadController', ['as' => 'api'])->only(['store', 'destroy', 'update']); + Route::apiResource('organizations', 'OrganizationController', ['as' => 'api'])->only(['store', 'destroy', 'update']); Route::apiResource('events', 'EventController', ['as' => 'api'])->only(['store', 'destroy', 'update']); diff --git a/src/tests/Feature/Api/GigadTest.php b/src/tests/Feature/Api/GigadTest.php new file mode 100644 index 00000000..5072e4ff --- /dev/null +++ b/src/tests/Feature/Api/GigadTest.php @@ -0,0 +1,131 @@ +create(); + Organization::factory()->create(); + $this->gigad = Gigad::factory()->create(); + $this->validGigadInput = [ + 'link' => 'http://sqroot.eu/test', + 'description' => 'Lorem ipsum', + 'gig_category_id' => GigCategory::first()->id, + 'is_public' => false + ]; + } + + use DatabaseMigrations; + + + public function testGigadInfoIsReturned() + { + + $adDescription = 'Making audience laugh'; + $this->gigad->description = $adDescription; + $this->gigad->save(); + + $response = $this->get($this->getApiUrl() . '/gigads/' . $this->gigad->uid); + + $response->assertStatus(200) + ->assertJson(['data' => [ + 'uid' => $this->gigad->uid, + 'description' => $adDescription, + 'link' => $this->gigad->link, + 'organization' => [ + 'uid' => $this->gigad->organization->uid, + 'name' => $this->gigad->organization->name + ], + 'category' => [ + 'name' => $this->gigad->category->name, + 'id' => $this->gigad->category->id + ] + ]]); + } + + public function testGigadInfoIsListed() + { + Gigad::factory()->count(2)->create(); + + $response = $this->get($this->getApiUrl() . '/gigads'); + + $response->assertStatus(200) + ->assertJsonFragment([ + 'uid' => $this->gigad->uid, + 'description' => $this->gigad->description + ]) + ->assertJsonCount(3); + } + + public function testOnlyMyAdsAreListedWhenRequested() + { + Gigad::factory()->count(2)->create(); + + $user = $this->actingAsOrganizationMember(); + + $this->gigad->organization_id = $user->organizations()->first()->id; + $this->gigad->save(); + + $response = $this->get($this->getApiUrl() . '/gigads?onlyMine=1'); + $response->assertStatus(200) + ->assertJsonFragment([ + 'uid' => $this->gigad->uid, + ]) + ->assertJsonCount(1, 'data'); + } + + public function testOnlyPublicAdsAreListedWhenRequested() + { + Gigad::factory()->create(['is_public' => false]); + + $response = $this->get($this->getApiUrl() . '/gigads?filter[is_public]=1'); + $response->assertStatus(200) + ->assertJsonFragment([ + 'uid' => $this->gigad->uid, + ]) + ->assertJsonCount(1, 'data'); + } + + + public function testGigadCanBeCreated() + { + $user = $this->actingAsOrganizationMember(); + + $this->validGigadInput['organization_uid'] = $user->organizations()->first()->uid; + + $response = $this->post($this->getApiUrl() . '/gigads/', $this->validGigadInput); + + $response->assertStatus(201) + ->assertJson(['data' => ['link' => $this->validGigadInput['link']]]); + + $this->assertDatabaseHas('gigad_translations', ['link' => $this->validGigadInput['link']]); + } + + public function testGigadCanBeEdited() + { + $user = $this->actingAsOrganizationMember(); + + $this->gigad->organization_id = $user->organizations()->first()->id; + $this->gigad->save(); + $this->validGigadInput['organization_uid'] = $user->organizations()->first()->uid; + + $response = $this->put($this->getApiUrl() . '/gigads/' . $this->gigad->uid, $this->validGigadInput); + $response->assertStatus(200) + ->assertJson(['data' => ['link' => $this->validGigadInput['link']]]); + } +} diff --git a/src/tests/Unit/Rules/ContainsMyOrganizationTest.php b/src/tests/Unit/Rules/ContainsMyOrganizationTest.php index 1f2f5580..9b416f86 100644 --- a/src/tests/Unit/Rules/ContainsMyOrganizationTest.php +++ b/src/tests/Unit/Rules/ContainsMyOrganizationTest.php @@ -67,8 +67,10 @@ public function testMyOrganizationNotInListFails() ])); } - public function testReturnsFalseOnStringInput() + public function testTakesStringInputAsListOfOneOrgs() { - $this->assertFalse($this->validator->passes('organizations', 'Kaladin')); + $user = $this->actingAsOrganizationMember(); + + $this->assertTrue($this->validator->passes('organizations', $user->organizations()->first()->uid)); } } From 4c59ce5794dbd9dbae1c90d794059b0ed6cc731d Mon Sep 17 00:00:00 2001 From: Ando Roots Date: Mon, 19 Oct 2020 19:40:32 +0300 Subject: [PATCH 2/6] Add initial public frontend view for listing gig ads --- .../Api/V1/GigCategoryController.php | 30 +++++++ .../Controllers/Api/V1/GigadController.php | 2 +- .../Resources/V1/Gig/CategoryResource.php | 31 +++++++ src/app/Orm/GigCategory.php | 3 +- .../factories/Orm/GigCategoryFactory.php | 3 +- ...10_17_142151_create_gig_category_table.php | 1 + src/database/seeders/GigCategorySeeder.php | 16 ++-- src/resources/js/public/routes.js | 4 +- .../js/public/views/gigs/CategoryAds.vue | 64 ++++++++++++++ src/resources/js/public/views/gigs/Form.vue | 83 ------------------- src/resources/js/public/views/gigs/List.vue | 53 ++++++++++++ src/resources/lang/en/public.json | 5 +- src/resources/lang/et/public.json | 5 +- src/routes/api.php | 2 + src/tests/Feature/Api/Gigad/CategoryTest.php | 51 ++++++++++++ src/tests/Feature/Api/GigadTest.php | 4 +- 16 files changed, 260 insertions(+), 97 deletions(-) create mode 100644 src/app/Http/Controllers/Api/V1/GigCategoryController.php create mode 100644 src/app/Http/Resources/V1/Gig/CategoryResource.php create mode 100644 src/resources/js/public/views/gigs/CategoryAds.vue delete mode 100644 src/resources/js/public/views/gigs/Form.vue create mode 100644 src/resources/js/public/views/gigs/List.vue create mode 100644 src/tests/Feature/Api/Gigad/CategoryTest.php diff --git a/src/app/Http/Controllers/Api/V1/GigCategoryController.php b/src/app/Http/Controllers/Api/V1/GigCategoryController.php new file mode 100644 index 00000000..d97d1313 --- /dev/null +++ b/src/app/Http/Controllers/Api/V1/GigCategoryController.php @@ -0,0 +1,30 @@ +orderBy('order', 'asc') + ->paginate(15); + + return CategoryResource::collection($categories); + } +} diff --git a/src/app/Http/Controllers/Api/V1/GigadController.php b/src/app/Http/Controllers/Api/V1/GigadController.php index c4902b70..2fc1b6cb 100644 --- a/src/app/Http/Controllers/Api/V1/GigadController.php +++ b/src/app/Http/Controllers/Api/V1/GigadController.php @@ -42,7 +42,7 @@ public function index(Request $request) { $gigads = QueryBuilder::for(Gigad::class) - ->allowedFilters(AllowedFilter::exact('is_public')) + ->allowedFilters([AllowedFilter::exact('is_public'), AllowedFilter::exact('gig_category_id')]) ->orderBy('id', 'asc') ->onlyMine($request->input('onlyMine', false)) ->paginate(15); diff --git a/src/app/Http/Resources/V1/Gig/CategoryResource.php b/src/app/Http/Resources/V1/Gig/CategoryResource.php new file mode 100644 index 00000000..e9e0b45e --- /dev/null +++ b/src/app/Http/Resources/V1/Gig/CategoryResource.php @@ -0,0 +1,31 @@ + $this['id'], + 'name' => $this['name'], + 'description' => $this['description'], + 'order'=> $this['order'], + 'ads' => $this->gigads->count() + ]; + } + +} diff --git a/src/app/Orm/GigCategory.php b/src/app/Orm/GigCategory.php index 9cc370ed..5690d82a 100644 --- a/src/app/Orm/GigCategory.php +++ b/src/app/Orm/GigCategory.php @@ -15,6 +15,7 @@ * @property string $name * @property int $id * @property string $description + * @property int $order * @package App\Orm */ class GigCategory extends Model @@ -31,7 +32,7 @@ class GigCategory extends Model 'deleted_at' ]; - protected $fillable = ['description', 'name','locale']; + protected $fillable = ['description', 'name','order']; /** * @return \Illuminate\Database\Eloquent\Relations\HasMany */ diff --git a/src/database/factories/Orm/GigCategoryFactory.php b/src/database/factories/Orm/GigCategoryFactory.php index 744ff666..ea517634 100644 --- a/src/database/factories/Orm/GigCategoryFactory.php +++ b/src/database/factories/Orm/GigCategoryFactory.php @@ -23,7 +23,8 @@ public function definition() { return [ 'name' => $this->faker->words(3, true), - 'description' => $this->faker->sentence(30, true) + 'description' => $this->faker->sentence(30, true), + 'order' => rand(-3, 10) ]; } } diff --git a/src/database/migrations/2020_10_17_142151_create_gig_category_table.php b/src/database/migrations/2020_10_17_142151_create_gig_category_table.php index e532b124..df446bb7 100644 --- a/src/database/migrations/2020_10_17_142151_create_gig_category_table.php +++ b/src/database/migrations/2020_10_17_142151_create_gig_category_table.php @@ -15,6 +15,7 @@ public function up() { Schema::create('gig_categories', function (Blueprint $table) { $table->id(); + $table->tinyInteger('order')->default(0); $table->timestamps(); $table->softDeletes(); }); diff --git a/src/database/seeders/GigCategorySeeder.php b/src/database/seeders/GigCategorySeeder.php index 9fa46692..09ea3724 100644 --- a/src/database/seeders/GigCategorySeeder.php +++ b/src/database/seeders/GigCategorySeeder.php @@ -19,19 +19,24 @@ public function run() $categories = [ [ 'name' => 'Etendused üritustele', - 'description' => 'TODO' + 'description' => 'TODO', + 'order' => 10 ],[ 'name' => 'Töötoad', - 'description' => 'TODO' + 'description' => 'TODO', + 'order' => 20 ],[ 'name' => 'Õhtujuhtimine', - 'description' => 'TODO' + 'description' => 'TODO', + 'order'=>30 ],[ 'name' => 'Meeskonnatöö', - 'description' => 'TODO' + 'description' => 'TODO', + 'order'=> 40 ],[ 'name' => 'Impronäitleja', - 'description' => 'TODO' + 'description' => 'TODO', + 'order'=> 50 ], ]; @@ -41,6 +46,7 @@ public function run() $category->name = $input['name']; $category->description = $input['description']; + $category->order = $input['order']; $category->save(); } } diff --git a/src/resources/js/public/routes.js b/src/resources/js/public/routes.js index a741839f..f3f3fe58 100644 --- a/src/resources/js/public/routes.js +++ b/src/resources/js/public/routes.js @@ -6,7 +6,7 @@ import OrganizationDetails from './views/organizations/Details'; import Contact from './views/Contact'; import MarkdownView from '../components/MarkdownView'; import ProductionDetails from './views/productions/Details'; -import GigsForm from './views/gigs/Form'; +import GigsList from './views/gigs/List'; export function getRoutes(i18n){ return [ @@ -33,7 +33,7 @@ export function getRoutes(i18n){ { path: '/gigs', name: 'gigs', - component: GigsForm, + component: GigsList, }, { path: '/organizations/:uid', diff --git a/src/resources/js/public/views/gigs/CategoryAds.vue b/src/resources/js/public/views/gigs/CategoryAds.vue new file mode 100644 index 00000000..45d5cf0e --- /dev/null +++ b/src/resources/js/public/views/gigs/CategoryAds.vue @@ -0,0 +1,64 @@ + + + + diff --git a/src/resources/js/public/views/gigs/Form.vue b/src/resources/js/public/views/gigs/Form.vue deleted file mode 100644 index 03acf18f..00000000 --- a/src/resources/js/public/views/gigs/Form.vue +++ /dev/null @@ -1,83 +0,0 @@ - - - diff --git a/src/resources/js/public/views/gigs/List.vue b/src/resources/js/public/views/gigs/List.vue new file mode 100644 index 00000000..6a7c2a75 --- /dev/null +++ b/src/resources/js/public/views/gigs/List.vue @@ -0,0 +1,53 @@ + + + diff --git a/src/resources/lang/en/public.json b/src/resources/lang/en/public.json index faedf5e6..f20ebc64 100644 --- a/src/resources/lang/en/public.json +++ b/src/resources/lang/en/public.json @@ -14,5 +14,8 @@ "excerpt": "Excerpt", "description": "Description" } - } + }, + "gig": { + "visit_advertiser": "Read more" + } } diff --git a/src/resources/lang/et/public.json b/src/resources/lang/et/public.json index 21cdc130..c85724c4 100644 --- a/src/resources/lang/et/public.json +++ b/src/resources/lang/et/public.json @@ -14,5 +14,8 @@ "excerpt": "Lühikirjeldus", "description": "Kirjeldus" } - } + }, + "gig": { + "visit_advertiser": "Loe lähemalt" + } } diff --git a/src/routes/api.php b/src/routes/api.php index 80f513d1..ccf19573 100644 --- a/src/routes/api.php +++ b/src/routes/api.php @@ -42,6 +42,8 @@ Route::apiResource('users', 'UserController') ->only(['show']); + Route::apiResource('gigcategories', 'GigCategoryController')->only(['index']); + Route::apiResource('gigads', 'GigadController')->only(['index', 'show']); }); diff --git a/src/tests/Feature/Api/Gigad/CategoryTest.php b/src/tests/Feature/Api/Gigad/CategoryTest.php new file mode 100644 index 00000000..c7920207 --- /dev/null +++ b/src/tests/Feature/Api/Gigad/CategoryTest.php @@ -0,0 +1,51 @@ +count(5)->create(); + $this->gigCategory = GigCategory::factory()->create(); + } + + public function testGigCategoryListCanBeFetched() + { + $response = $this->get($this->getApiUrl() . '/gigcategories'); + + $response->assertStatus(200) + ->assertJsonFragment([ + 'id' => $this->gigCategory->id, + 'description' => $this->gigCategory->description + ]) + ->assertJsonCount(6,'data'); + } +} diff --git a/src/tests/Feature/Api/GigadTest.php b/src/tests/Feature/Api/GigadTest.php index 5072e4ff..0f939053 100644 --- a/src/tests/Feature/Api/GigadTest.php +++ b/src/tests/Feature/Api/GigadTest.php @@ -13,6 +13,8 @@ */ class GigadTest extends ApiTestCase { + use DatabaseMigrations; + private Gigad $gigad; private array $validGigadInput; @@ -30,8 +32,6 @@ protected function setUp(): void ]; } - use DatabaseMigrations; - public function testGigadInfoIsReturned() { From 3b8e4234912a2261e9fb3bd302238f5d70dc8c5f Mon Sep 17 00:00:00 2001 From: Ando Roots Date: Tue, 20 Oct 2020 11:59:14 +0300 Subject: [PATCH 3/6] Add back-end form for editing gigads --- .../Controllers/Api/V1/GigadController.php | 26 ++- .../Requests/Gigad/UpdateGigadRequest.php | 4 +- src/app/Http/Resources/V1/GigadResource.php | 8 +- .../V1/Image/HeaderImageResource.php | 1 + src/app/Http/Services/GigadStorageService.php | 37 ++++ src/app/Orm/Gigad.php | 10 + src/resources/js/admin/routes.js | 8 +- .../js/admin/views/organizations/Details.vue | 125 ++++++------ .../admin/views/organizations/gigs/Edit.vue | 184 ++++++++++++++++++ .../views/organizations/gigs/GigTable.vue | 99 ++++++++++ src/resources/js/public/App.vue | 2 +- .../js/public/views/gigs/CategoryAds.vue | 68 ++++--- src/resources/js/public/views/gigs/List.vue | 31 +-- src/resources/lang/en/admin.json | 175 +++++++++-------- src/resources/lang/en/common.json | 7 +- src/resources/lang/en/public.json | 3 +- src/resources/lang/et/admin.json | 175 +++++++++-------- src/resources/lang/et/common.json | 7 +- src/resources/lang/et/public.json | 3 +- 19 files changed, 690 insertions(+), 283 deletions(-) create mode 100644 src/app/Http/Services/GigadStorageService.php create mode 100644 src/resources/js/admin/views/organizations/gigs/Edit.vue create mode 100644 src/resources/js/admin/views/organizations/gigs/GigTable.vue diff --git a/src/app/Http/Controllers/Api/V1/GigadController.php b/src/app/Http/Controllers/Api/V1/GigadController.php index 2fc1b6cb..74b80b41 100644 --- a/src/app/Http/Controllers/Api/V1/GigadController.php +++ b/src/app/Http/Controllers/Api/V1/GigadController.php @@ -7,6 +7,7 @@ use App\Http\Requests\Gigad\StoreGigadRequest; use App\Http\Requests\Gigad\UpdateGigadRequest; use App\Http\Resources\V1\GigadResource; +use App\Http\Services\GigadStorageService; use App\Orm\Gigad; use App\Orm\Organization; use Illuminate\Http\Request; @@ -19,6 +20,18 @@ */ class GigadController extends Controller { + /** + * @var GigadStorageService + */ + private GigadStorageService $gigadStorageService; + + /** + * @param GigadStorageService $gigadStorageService + */ + public function __construct(GigadStorageService $gigadStorageService) + { + $this->gigadStorageService = $gigadStorageService; + } /** @@ -66,12 +79,9 @@ public function store(StoreGigadRequest $request) { $gigad = new Gigad; - $gigad->link = $request->input('link'); - $gigad->description = $request->input('description'); - $gigad->organization_id = Organization::where('uid', $request->input('organization_uid'))->first()->id; - $gigad->gig_category_id = $request->input('gig_category_id'); $gigad->setToken(); - $gigad->save(); + + $this->gigadStorageService->update($gigad,$request); return new GigadResource($gigad); } @@ -91,12 +101,8 @@ public function store(StoreGigadRequest $request) */ public function update(Gigad $gigad, UpdateGigadRequest $request) { - $gigad->link = $request->input('link'); - $gigad->description = $request->input('description'); - $gigad->organization_id = Organization::where('uid', $request->input('organization_uid'))->first()->id; - $gigad->gig_category_id = $request->input('gig_category_id'); - $gigad->save(); + $this->gigadStorageService->update($gigad,$request); return new GigadResource($gigad); } diff --git a/src/app/Http/Requests/Gigad/UpdateGigadRequest.php b/src/app/Http/Requests/Gigad/UpdateGigadRequest.php index 3aa6c4f8..b818c57e 100644 --- a/src/app/Http/Requests/Gigad/UpdateGigadRequest.php +++ b/src/app/Http/Requests/Gigad/UpdateGigadRequest.php @@ -17,8 +17,8 @@ class UpdateGigadRequest extends DeleteGigadRequest public function rules() { return [ - 'link' => 'max:255|required|min:5|url', - 'description' => 'max:5000|required', + 'link' => 'max:255|nullable|min:5|url', + 'description' => 'max:5000|nullable', 'organization_uid' => ['required', 'exists:organizations,uid', new ContainsMyOrganization], 'images.header.content' => ['nullable', new Base64HeaderImage], 'is_public' => 'required|bool', diff --git a/src/app/Http/Resources/V1/GigadResource.php b/src/app/Http/Resources/V1/GigadResource.php index 21496f41..314ca84c 100644 --- a/src/app/Http/Resources/V1/GigadResource.php +++ b/src/app/Http/Resources/V1/GigadResource.php @@ -2,6 +2,7 @@ namespace App\Http\Resources\V1; +use App\Http\Resources\V1\Image\HeaderImageResource; use App\Orm\GigCategory; use App\Orm\Organization; use Illuminate\Http\Resources\Json\JsonResource; @@ -36,7 +37,12 @@ public function toArray($request) 'name' => $this->organization->name ], 'link'=> $this['link'], - 'description' => $this['description'] + 'description' => $this['description'], + 'times' => [ + 'updated_at'=>$this['updated_at']->toIso8601String() + ], + 'is_public' => $this['is_public'], + 'images' => new HeaderImageResource($this) ]; } diff --git a/src/app/Http/Resources/V1/Image/HeaderImageResource.php b/src/app/Http/Resources/V1/Image/HeaderImageResource.php index deaa4677..5f57ed87 100644 --- a/src/app/Http/Resources/V1/Image/HeaderImageResource.php +++ b/src/app/Http/Resources/V1/Image/HeaderImageResource.php @@ -7,6 +7,7 @@ use Carbon\Carbon; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\Facades\Cache; +use Spatie\MediaLibrary\HasMedia; /** * @property HasMedia $this diff --git a/src/app/Http/Services/GigadStorageService.php b/src/app/Http/Services/GigadStorageService.php new file mode 100644 index 00000000..655cf826 --- /dev/null +++ b/src/app/Http/Services/GigadStorageService.php @@ -0,0 +1,37 @@ +link = $request->input('link'); + $gigad->description = $request->input('description'); + $gigad->organization_id = Organization::where('uid', $request->input('organization_uid'))->first()->id; + $gigad->gig_category_id = $request->input('gig_category_id'); + $gigad->is_public = $request->input('is_public', false); + + DB::transaction(function () use ($gigad, $request) { + $gigad->save(); + + $this->syncMedia($request, $gigad); + }); + + + return $gigad; + } +} diff --git a/src/app/Orm/Gigad.php b/src/app/Orm/Gigad.php index 0f0e8cbb..1f0bb78e 100644 --- a/src/app/Orm/Gigad.php +++ b/src/app/Orm/Gigad.php @@ -12,6 +12,7 @@ use OwenIt\Auditing\Contracts\Auditable; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; +use Spatie\MediaLibrary\MediaCollections\Models\Media as SpatieMedia; /** * @property string $link @@ -101,4 +102,13 @@ public function scopeOnlyMine(Builder $query, bool $enabled = false): Builder return $query->whereIn('organization_id', $organizationIds); } + + /** + * @param SpatieMedia|null $media + * @throws \Spatie\Image\Exceptions\InvalidManipulation + */ + public function registerMediaConversions(SpatieMedia $media = null): void + { + $this->registerCoverImageConversion(); + } } diff --git a/src/resources/js/admin/routes.js b/src/resources/js/admin/routes.js index 71036b86..c549c730 100644 --- a/src/resources/js/admin/routes.js +++ b/src/resources/js/admin/routes.js @@ -13,6 +13,7 @@ import OrganizationsList from './views/organizations/List'; import OrganizationDetails from './views/organizations/Details'; import OrganizationEdit from './views/organizations/Edit'; import OrganizationPeopleDetails from './views/organizations/people/Details'; +import GigadEdit from './views/organizations/gigs/Edit'; export function getRoutes() { return [ @@ -86,6 +87,11 @@ export function getRoutes() { name: 'organizations.people.details', component: OrganizationPeopleDetails }, + { + path: '/gigads/:uid/edit', + name: 'gigads.edit', + component: GigadEdit, + }, {path: '*', component: PageNotFound} ]; -} \ No newline at end of file +} diff --git a/src/resources/js/admin/views/organizations/Details.vue b/src/resources/js/admin/views/organizations/Details.vue index 4c27916c..78a55a83 100644 --- a/src/resources/js/admin/views/organizations/Details.vue +++ b/src/resources/js/admin/views/organizations/Details.vue @@ -1,52 +1,57 @@ diff --git a/src/resources/js/admin/views/organizations/gigs/GigTable.vue b/src/resources/js/admin/views/organizations/gigs/GigTable.vue new file mode 100644 index 00000000..a2d706f2 --- /dev/null +++ b/src/resources/js/admin/views/organizations/gigs/GigTable.vue @@ -0,0 +1,99 @@ + + diff --git a/src/resources/js/public/App.vue b/src/resources/js/public/App.vue index 728d82ff..d4bd159b 100644 --- a/src/resources/js/public/App.vue +++ b/src/resources/js/public/App.vue @@ -27,7 +27,7 @@ :to="{ name: 'organizations' }"> {{ $t("nav.organizations") }} - {{ $t("nav.gigs") }} diff --git a/src/resources/js/public/views/gigs/CategoryAds.vue b/src/resources/js/public/views/gigs/CategoryAds.vue index 45d5cf0e..f516924d 100644 --- a/src/resources/js/public/views/gigs/CategoryAds.vue +++ b/src/resources/js/public/views/gigs/CategoryAds.vue @@ -1,38 +1,53 @@