diff --git a/app/Http/Controllers/Api/TagsController.php b/app/Http/Controllers/Api/TagsController.php index 161f4ecb..bee40369 100644 --- a/app/Http/Controllers/Api/TagsController.php +++ b/app/Http/Controllers/Api/TagsController.php @@ -460,6 +460,16 @@ protected function notifyFollowing(?Tag $tag): RedirectResponse return back(); } + /** + * Return related tags for a specified tag. + */ + public function relatedTags(Tag $tag): JsonResponse + { + $relatedTags = $tag->relatedTags(); + + return response()->json($relatedTags); + } + /** * Update the specified resource in storage. */ diff --git a/bootstrap/cache/packages.php b/bootstrap/cache/packages.php index 301ab906..e265bfd1 100755 --- a/bootstrap/cache/packages.php +++ b/bootstrap/cache/packages.php @@ -10,24 +10,6 @@ 0 => 'Anhskohbo\\NoCaptcha\\NoCaptchaServiceProvider', ), ), - 'barryvdh/laravel-debugbar' => - array ( - 'aliases' => - array ( - 'Debugbar' => 'Barryvdh\\Debugbar\\Facades\\Debugbar', - ), - 'providers' => - array ( - 0 => 'Barryvdh\\Debugbar\\ServiceProvider', - ), - ), - 'barryvdh/laravel-ide-helper' => - array ( - 'providers' => - array ( - 0 => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider', - ), - ), 'bepsvpt/secure-headers' => array ( 'providers' => @@ -37,27 +19,20 @@ ), 'intervention/image' => array ( - 'providers' => - array ( - 0 => 'Intervention\\Image\\ImageServiceProvider', - ), 'aliases' => array ( 'Image' => 'Intervention\\Image\\Facades\\Image', ), - ), - 'laravel-notification-channels/twitter' => - array ( 'providers' => array ( - 0 => 'NotificationChannels\\Twitter\\TwitterServiceProvider', + 0 => 'Intervention\\Image\\ImageServiceProvider', ), ), - 'laravel/dusk' => + 'laravel-notification-channels/twitter' => array ( 'providers' => array ( - 0 => 'Laravel\\Dusk\\DuskServiceProvider', + 0 => 'NotificationChannels\\Twitter\\TwitterServiceProvider', ), ), 'laravel/sanctum' => diff --git a/bootstrap/cache/services.php b/bootstrap/cache/services.php index 360b2786..82872ae8 100755 --- a/bootstrap/cache/services.php +++ b/bootstrap/cache/services.php @@ -24,36 +24,33 @@ 20 => 'Illuminate\\Validation\\ValidationServiceProvider', 21 => 'Illuminate\\View\\ViewServiceProvider', 22 => 'Anhskohbo\\NoCaptcha\\NoCaptchaServiceProvider', - 23 => 'Barryvdh\\Debugbar\\ServiceProvider', - 24 => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider', - 25 => 'Bepsvpt\\SecureHeaders\\SecureHeadersServiceProvider', - 26 => 'Intervention\\Image\\ImageServiceProvider', - 27 => 'NotificationChannels\\Twitter\\TwitterServiceProvider', - 28 => 'Laravel\\Dusk\\DuskServiceProvider', - 29 => 'Laravel\\Sanctum\\SanctumServiceProvider', - 30 => 'Laravel\\Socialite\\SocialiteServiceProvider', - 31 => 'Laravel\\Tinker\\TinkerServiceProvider', - 32 => 'Laravel\\Ui\\UiServiceProvider', - 33 => 'Collective\\Html\\HtmlServiceProvider', - 34 => 'Carbon\\Laravel\\ServiceProvider', - 35 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', - 36 => 'Termwind\\Laravel\\TermwindServiceProvider', - 37 => 'Sentry\\Laravel\\ServiceProvider', - 38 => 'Sentry\\Laravel\\Tracing\\ServiceProvider', - 39 => 'Spatie\\LaravelIgnition\\IgnitionServiceProvider', - 40 => 'Spatie\\Sitemap\\SitemapServiceProvider', - 41 => 'Vinkla\\Shield\\ShieldServiceProvider', - 42 => 'Collective\\Html\\HtmlServiceProvider', - 43 => 'App\\Providers\\AppServiceProvider', - 44 => 'App\\Providers\\AuthServiceProvider', - 45 => 'App\\Providers\\BroadcastServiceProvider', - 46 => 'App\\Providers\\EventServiceProvider', - 47 => 'App\\Providers\\RouteServiceProvider', - 48 => 'App\\Providers\\ViewComposerServiceProvider', - 49 => 'Laravel\\Socialite\\SocialiteServiceProvider', - 50 => 'Intervention\\Image\\ImageServiceProvider', - 51 => 'Laravel\\Tinker\\TinkerServiceProvider', - 52 => 'NotificationChannels\\Twitter\\TwitterServiceProvider', + 23 => 'Bepsvpt\\SecureHeaders\\SecureHeadersServiceProvider', + 24 => 'Intervention\\Image\\ImageServiceProvider', + 25 => 'NotificationChannels\\Twitter\\TwitterServiceProvider', + 26 => 'Laravel\\Sanctum\\SanctumServiceProvider', + 27 => 'Laravel\\Socialite\\SocialiteServiceProvider', + 28 => 'Laravel\\Tinker\\TinkerServiceProvider', + 29 => 'Laravel\\Ui\\UiServiceProvider', + 30 => 'Collective\\Html\\HtmlServiceProvider', + 31 => 'Carbon\\Laravel\\ServiceProvider', + 32 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', + 33 => 'Termwind\\Laravel\\TermwindServiceProvider', + 34 => 'Sentry\\Laravel\\ServiceProvider', + 35 => 'Sentry\\Laravel\\Tracing\\ServiceProvider', + 36 => 'Spatie\\LaravelIgnition\\IgnitionServiceProvider', + 37 => 'Spatie\\Sitemap\\SitemapServiceProvider', + 38 => 'Vinkla\\Shield\\ShieldServiceProvider', + 39 => 'Collective\\Html\\HtmlServiceProvider', + 40 => 'App\\Providers\\AppServiceProvider', + 41 => 'App\\Providers\\AuthServiceProvider', + 42 => 'App\\Providers\\BroadcastServiceProvider', + 43 => 'App\\Providers\\EventServiceProvider', + 44 => 'App\\Providers\\RouteServiceProvider', + 45 => 'App\\Providers\\ViewComposerServiceProvider', + 46 => 'Laravel\\Socialite\\SocialiteServiceProvider', + 47 => 'Intervention\\Image\\ImageServiceProvider', + 48 => 'Laravel\\Tinker\\TinkerServiceProvider', + 49 => 'NotificationChannels\\Twitter\\TwitterServiceProvider', ), 'eager' => array ( @@ -68,29 +65,27 @@ 8 => 'Illuminate\\Session\\SessionServiceProvider', 9 => 'Illuminate\\View\\ViewServiceProvider', 10 => 'Anhskohbo\\NoCaptcha\\NoCaptchaServiceProvider', - 11 => 'Barryvdh\\Debugbar\\ServiceProvider', - 12 => 'Bepsvpt\\SecureHeaders\\SecureHeadersServiceProvider', - 13 => 'Intervention\\Image\\ImageServiceProvider', - 14 => 'NotificationChannels\\Twitter\\TwitterServiceProvider', - 15 => 'Laravel\\Dusk\\DuskServiceProvider', - 16 => 'Laravel\\Sanctum\\SanctumServiceProvider', - 17 => 'Laravel\\Ui\\UiServiceProvider', - 18 => 'Carbon\\Laravel\\ServiceProvider', - 19 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', - 20 => 'Termwind\\Laravel\\TermwindServiceProvider', - 21 => 'Sentry\\Laravel\\ServiceProvider', - 22 => 'Sentry\\Laravel\\Tracing\\ServiceProvider', - 23 => 'Spatie\\LaravelIgnition\\IgnitionServiceProvider', - 24 => 'Spatie\\Sitemap\\SitemapServiceProvider', - 25 => 'Vinkla\\Shield\\ShieldServiceProvider', - 26 => 'App\\Providers\\AppServiceProvider', - 27 => 'App\\Providers\\AuthServiceProvider', - 28 => 'App\\Providers\\BroadcastServiceProvider', - 29 => 'App\\Providers\\EventServiceProvider', - 30 => 'App\\Providers\\RouteServiceProvider', - 31 => 'App\\Providers\\ViewComposerServiceProvider', - 32 => 'Intervention\\Image\\ImageServiceProvider', - 33 => 'NotificationChannels\\Twitter\\TwitterServiceProvider', + 11 => 'Bepsvpt\\SecureHeaders\\SecureHeadersServiceProvider', + 12 => 'Intervention\\Image\\ImageServiceProvider', + 13 => 'NotificationChannels\\Twitter\\TwitterServiceProvider', + 14 => 'Laravel\\Sanctum\\SanctumServiceProvider', + 15 => 'Laravel\\Ui\\UiServiceProvider', + 16 => 'Carbon\\Laravel\\ServiceProvider', + 17 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', + 18 => 'Termwind\\Laravel\\TermwindServiceProvider', + 19 => 'Sentry\\Laravel\\ServiceProvider', + 20 => 'Sentry\\Laravel\\Tracing\\ServiceProvider', + 21 => 'Spatie\\LaravelIgnition\\IgnitionServiceProvider', + 22 => 'Spatie\\Sitemap\\SitemapServiceProvider', + 23 => 'Vinkla\\Shield\\ShieldServiceProvider', + 24 => 'App\\Providers\\AppServiceProvider', + 25 => 'App\\Providers\\AuthServiceProvider', + 26 => 'App\\Providers\\BroadcastServiceProvider', + 27 => 'App\\Providers\\EventServiceProvider', + 28 => 'App\\Providers\\RouteServiceProvider', + 29 => 'App\\Providers\\ViewComposerServiceProvider', + 30 => 'Intervention\\Image\\ImageServiceProvider', + 31 => 'NotificationChannels\\Twitter\\TwitterServiceProvider', ), 'deferred' => array ( @@ -233,8 +228,6 @@ 'validator' => 'Illuminate\\Validation\\ValidationServiceProvider', 'validation.presence' => 'Illuminate\\Validation\\ValidationServiceProvider', 'Illuminate\\Contracts\\Validation\\UncompromisedVerifier' => 'Illuminate\\Validation\\ValidationServiceProvider', - 'command.ide-helper.generate' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider', - 'command.ide-helper.models' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider', 'Laravel\\Socialite\\Contracts\\Factory' => 'Laravel\\Socialite\\SocialiteServiceProvider', 'command.tinker' => 'Laravel\\Tinker\\TinkerServiceProvider', 'html' => 'Collective\\Html\\HtmlServiceProvider', @@ -280,9 +273,6 @@ 'Illuminate\\Validation\\ValidationServiceProvider' => array ( ), - 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider' => - array ( - ), 'Laravel\\Socialite\\SocialiteServiceProvider' => array ( ), diff --git a/database/factories/EntityFactory.php b/database/factories/EntityFactory.php index 965a11f4..3828774a 100644 --- a/database/factories/EntityFactory.php +++ b/database/factories/EntityFactory.php @@ -26,7 +26,7 @@ public function definition() return [ 'name' => $this->faker->name, 'slug' => $this->faker->name, - 'short' => $this->faker->text(254), + 'short' => $this->faker->text(100), 'description' => $this->faker->text(254), 'entity_type_id' => function () { return EntityType::all()->random()->id; diff --git a/public/postman/schemas/api.yml b/public/postman/schemas/api.yml index 535cdeb3..6c0bf81a 100644 --- a/public/postman/schemas/api.yml +++ b/public/postman/schemas/api.yml @@ -5948,6 +5948,42 @@ paths: description: Successful response content: application/json: {} + /api/tags/{slug}/related-tags: + parameters: + - name: slug + description: The unique identifier of the tag + in: path + required: true + schema: + type: string + readOnly: true + example: "strobe" + get: + tags: + - tags + summary: Get Related Tags + operationId: getRelatedTags + description: Returns tags that are related to the specified tag based on co-occurrence in events + responses: + "200": + description: Successful response containing related tags with their occurrence counts + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + description: The number of times this tag appears with the specified tag + example: + "house": 5 + "electronic": 3 + "dance": 2 + "404": + description: Tag not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /api/tag-types: get: tags: diff --git a/routes/api.php b/routes/api.php index 6114749a..d02eddd0 100644 --- a/routes/api.php +++ b/routes/api.php @@ -158,6 +158,7 @@ Route::post('tags/{tag}/follow', 'Api\TagsController@followJson')->middleware('auth:sanctum'); Route::post('tags/{tag}/unfollow', 'Api\TagsController@unfollowJson')->middleware('auth:sanctum'); Route::delete('tags/{tag}', 'Api\TagsController@destroy'); + Route::get('tags/{tag}/related-tags', ['as' => 'tags.relatedTags', 'uses' => 'Api\TagsController@relatedTags']); Route::get('tags/popular', ['as' => 'tags.popular', 'uses' => 'Api\TagsController@popular']); Route::resource('tags', 'Api\TagsController')->except(['destroy']); Route::match(['get', 'post'], 'tag-types/filter', ['as' => 'tag-types.filter', 'uses' => 'Api\TagTypesController@filter']); diff --git a/tests/Feature/ApiTagsRelatedTagsTest.php b/tests/Feature/ApiTagsRelatedTagsTest.php new file mode 100644 index 00000000..3418827e --- /dev/null +++ b/tests/Feature/ApiTagsRelatedTagsTest.php @@ -0,0 +1,79 @@ +create(['user_status_id' => 1]); + $this->actingAs($user, 'sanctum'); + + // Create test tags + $mainTag = Tag::factory()->create(['name' => 'Main Tag', 'slug' => 'main-tag']); + $relatedTag1 = Tag::factory()->create(['name' => 'Related Tag 1', 'slug' => 'related-tag-1']); + $relatedTag2 = Tag::factory()->create(['name' => 'Related Tag 2', 'slug' => 'related-tag-2']); + + // Create events that link the tags together + $event1 = Event::factory()->create(['created_by' => $user->id]); + $event2 = Event::factory()->create(['created_by' => $user->id]); + + // Attach tags to events to create relationships + $event1->tags()->attach([$mainTag->id, $relatedTag1->id]); + $event2->tags()->attach([$mainTag->id, $relatedTag1->id, $relatedTag2->id]); + + $response = $this->getJson("/api/tags/{$mainTag->slug}/related-tags"); + + $response->assertStatus(200); + $data = $response->json(); + + // Check that related tags are returned + $this->assertIsArray($data); + $this->assertArrayHasKey('Related Tag 1', $data); + $this->assertEquals(2, $data['Related Tag 1']); // Should appear in 2 events + $this->assertArrayHasKey('Related Tag 2', $data); + $this->assertEquals(1, $data['Related Tag 2']); // Should appear in 1 event + } + + /** @test */ + public function it_returns_empty_array_for_tag_with_no_related_tags(): void + { + $user = User::factory()->create(['user_status_id' => 1]); + $this->actingAs($user, 'sanctum'); + + $tag = Tag::factory()->create(['name' => 'Isolated Tag', 'slug' => 'isolated-tag']); + + $response = $this->getJson("/api/tags/{$tag->slug}/related-tags"); + + $response->assertStatus(200); + $data = $response->json(); + + $this->assertIsArray($data); + $this->assertEmpty($data); + } + + /** @test */ + public function it_returns_404_for_non_existent_tag(): void + { + // Ensure exceptions are handled so the test can assert the 404 response + $this->withExceptionHandling(); + + $user = User::factory()->create(['user_status_id' => 1]); + $this->actingAs($user, 'sanctum'); + + $response = $this->getJson('/api/tags/non-existent-tag/related-tags'); + + $response->assertStatus(404); + } +} \ No newline at end of file