Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/Http/Controllers/Api/TagsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
31 changes: 3 additions & 28 deletions bootstrap/cache/packages.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' =>
Expand All @@ -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' =>
Expand Down
106 changes: 48 additions & 58 deletions bootstrap/cache/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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 (
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -280,9 +273,6 @@
'Illuminate\\Validation\\ValidationServiceProvider' =>
array (
),
'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider' =>
array (
),
'Laravel\\Socialite\\SocialiteServiceProvider' =>
array (
),
Expand Down
2 changes: 1 addition & 1 deletion database/factories/EntityFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
36 changes: 36 additions & 0 deletions public/postman/schemas/api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
Expand Down
79 changes: 79 additions & 0 deletions tests/Feature/ApiTagsRelatedTagsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace Tests\Feature;

use App\Models\Event;
use App\Models\Tag;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ApiTagsRelatedTagsTest extends TestCase
{
use RefreshDatabase;

protected $seed = true;

/** @test */
public function it_returns_related_tags_for_a_tag(): void
{
$user = User::factory()->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);
}
}