Feed languages
This commit is contained in:
parent
ed93dc3630
commit
e986f7871b
19 changed files with 585 additions and 93 deletions
|
|
@ -19,7 +19,7 @@ public function index(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$perPage = min($request->get('per_page', 15), 100);
|
$perPage = min($request->get('per_page', 15), 100);
|
||||||
|
|
||||||
$feeds = Feed::with(['language'])
|
$feeds = Feed::with(['languages'])
|
||||||
->withCount('articles')
|
->withCount('articles')
|
||||||
->orderBy('is_active', 'desc')
|
->orderBy('is_active', 'desc')
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
|
|
@ -57,8 +57,28 @@ public function store(StoreFeedRequest $request): JsonResponse
|
||||||
$validated['url'] = $adapter->getHomepageUrl();
|
$validated['url'] = $adapter->getHomepageUrl();
|
||||||
$validated['type'] = 'website';
|
$validated['type'] = 'website';
|
||||||
|
|
||||||
|
// Extract language-related data
|
||||||
|
$languageIds = $validated['language_ids'] ?? [];
|
||||||
|
$primaryLanguageId = $validated['primary_language_id'] ?? $languageIds[0] ?? null;
|
||||||
|
$languageUrls = $validated['language_urls'] ?? [];
|
||||||
|
|
||||||
|
// Remove language fields from feed data
|
||||||
|
unset($validated['language_ids'], $validated['primary_language_id'], $validated['language_urls']);
|
||||||
|
|
||||||
$feed = Feed::create($validated);
|
$feed = Feed::create($validated);
|
||||||
|
|
||||||
|
// Attach languages to the feed
|
||||||
|
foreach ($languageIds as $index => $languageId) {
|
||||||
|
$pivotData = [
|
||||||
|
'url' => $languageUrls[$languageId] ?? $validated['url'],
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => $languageId == $primaryLanguageId,
|
||||||
|
];
|
||||||
|
$feed->languages()->attach($languageId, $pivotData);
|
||||||
|
}
|
||||||
|
|
||||||
|
$feed->load('languages');
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new FeedResource($feed),
|
new FeedResource($feed),
|
||||||
'Feed created successfully!',
|
'Feed created successfully!',
|
||||||
|
|
@ -76,6 +96,8 @@ public function store(StoreFeedRequest $request): JsonResponse
|
||||||
*/
|
*/
|
||||||
public function show(Feed $feed): JsonResponse
|
public function show(Feed $feed): JsonResponse
|
||||||
{
|
{
|
||||||
|
$feed->load('languages');
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new FeedResource($feed),
|
new FeedResource($feed),
|
||||||
'Feed retrieved successfully.'
|
'Feed retrieved successfully.'
|
||||||
|
|
@ -91,10 +113,34 @@ public function update(UpdateFeedRequest $request, Feed $feed): JsonResponse
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
$validated['is_active'] = $validated['is_active'] ?? $feed->is_active;
|
$validated['is_active'] = $validated['is_active'] ?? $feed->is_active;
|
||||||
|
|
||||||
|
// Extract language-related data
|
||||||
|
$languageIds = $validated['language_ids'] ?? null;
|
||||||
|
$primaryLanguageId = $validated['primary_language_id'] ?? null;
|
||||||
|
$languageUrls = $validated['language_urls'] ?? [];
|
||||||
|
|
||||||
|
// Remove language fields from feed data
|
||||||
|
unset($validated['language_ids'], $validated['primary_language_id'], $validated['language_urls']);
|
||||||
|
|
||||||
$feed->update($validated);
|
$feed->update($validated);
|
||||||
|
|
||||||
|
// Update languages if provided
|
||||||
|
if ($languageIds !== null) {
|
||||||
|
// Sync languages with the feed
|
||||||
|
$syncData = [];
|
||||||
|
foreach ($languageIds as $index => $languageId) {
|
||||||
|
$syncData[$languageId] = [
|
||||||
|
'url' => $languageUrls[$languageId] ?? $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => $primaryLanguageId ? $languageId == $primaryLanguageId : $index === 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$feed->languages()->sync($syncData);
|
||||||
|
}
|
||||||
|
|
||||||
|
$feed->load('languages');
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new FeedResource($feed->fresh()),
|
new FeedResource($feed),
|
||||||
'Feed updated successfully!'
|
'Feed updated successfully!'
|
||||||
);
|
);
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,9 @@ public function createFeed(Request $request): JsonResponse
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'provider' => 'required|in:belga,vrt',
|
'provider' => 'required|in:belga,vrt',
|
||||||
'language_id' => 'required|exists:languages,id',
|
'language_ids' => 'required|array|min:1',
|
||||||
|
'language_ids.*' => 'exists:languages,id',
|
||||||
|
'primary_language_id' => 'nullable|exists:languages,id',
|
||||||
'description' => 'nullable|string|max:1000',
|
'description' => 'nullable|string|max:1000',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -224,20 +226,34 @@ public function createFeed(Request $request): JsonResponse
|
||||||
$url = 'https://www.belganewsagency.eu/';
|
$url = 'https://www.belganewsagency.eu/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract language-related data
|
||||||
|
$languageIds = $validated['language_ids'];
|
||||||
|
$primaryLanguageId = $validated['primary_language_id'] ?? $languageIds[0];
|
||||||
|
|
||||||
$feed = Feed::firstOrCreate(
|
$feed = Feed::firstOrCreate(
|
||||||
['url' => $url],
|
['url' => $url],
|
||||||
[
|
[
|
||||||
'name' => $validated['name'],
|
'name' => $validated['name'],
|
||||||
'type' => $type,
|
'type' => $type,
|
||||||
'provider' => $provider,
|
'provider' => $provider,
|
||||||
'language_id' => $validated['language_id'],
|
|
||||||
'description' => $validated['description'] ?? null,
|
'description' => $validated['description'] ?? null,
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Sync languages with the feed
|
||||||
|
$syncData = [];
|
||||||
|
foreach ($languageIds as $languageId) {
|
||||||
|
$syncData[$languageId] = [
|
||||||
|
'url' => $url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => $languageId == $primaryLanguageId,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$feed->languages()->sync($syncData);
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
new FeedResource($feed->load('language')),
|
new FeedResource($feed->load('languages')),
|
||||||
'Feed created successfully.'
|
'Feed created successfully.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,16 @@
|
||||||
'description' => 'Belgian public broadcaster news',
|
'description' => 'Belgian public broadcaster news',
|
||||||
'type' => 'website',
|
'type' => 'website',
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
|
'supported_languages' => [
|
||||||
|
'en' => [
|
||||||
|
'url' => 'https://www.vrt.be/vrtnws/en/',
|
||||||
|
'name' => 'English',
|
||||||
|
],
|
||||||
|
'nl' => [
|
||||||
|
'url' => 'https://www.vrt.be/vrtnws/nl/',
|
||||||
|
'name' => 'Dutch',
|
||||||
|
],
|
||||||
|
],
|
||||||
'parsers' => [
|
'parsers' => [
|
||||||
'homepage' => \Domains\Article\Parsers\Vrt\VrtHomepageParserAdapter::class,
|
'homepage' => \Domains\Article\Parsers\Vrt\VrtHomepageParserAdapter::class,
|
||||||
'article' => \Domains\Article\Parsers\Vrt\VrtArticleParser::class,
|
'article' => \Domains\Article\Parsers\Vrt\VrtArticleParser::class,
|
||||||
|
|
@ -31,6 +41,12 @@
|
||||||
'description' => 'Belgian national news agency',
|
'description' => 'Belgian national news agency',
|
||||||
'type' => 'rss',
|
'type' => 'rss',
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
|
'supported_languages' => [
|
||||||
|
'en' => [
|
||||||
|
'url' => 'https://www.belganewsagency.eu/',
|
||||||
|
'name' => 'English',
|
||||||
|
],
|
||||||
|
],
|
||||||
'parsers' => [
|
'parsers' => [
|
||||||
'homepage' => \Domains\Article\Parsers\Belga\BelgaHomepageParserAdapter::class,
|
'homepage' => \Domains\Article\Parsers\Belga\BelgaHomepageParserAdapter::class,
|
||||||
'article' => \Domains\Article\Parsers\Belga\BelgaArticleParser::class,
|
'article' => \Domains\Article\Parsers\Belga\BelgaArticleParser::class,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Create feed_languages pivot table for many-to-many relationship
|
||||||
|
Schema::create('feed_languages', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('feed_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('language_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->string('url')->nullable(); // Optional: specific URL for this feed-language combination
|
||||||
|
$table->json('settings')->nullable(); // Optional: language-specific settings
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->boolean('is_primary')->default(false); // To indicate primary language for the feed
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Ensure unique combination of feed and language
|
||||||
|
$table->unique(['feed_id', 'language_id']);
|
||||||
|
|
||||||
|
// Index for performance
|
||||||
|
$table->index(['feed_id', 'is_active']);
|
||||||
|
$table->index(['language_id', 'is_active']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Migrate existing data from feeds.language_id to feed_languages
|
||||||
|
// This will preserve existing language associations
|
||||||
|
DB::table('feeds')
|
||||||
|
->whereNotNull('language_id')
|
||||||
|
->orderBy('id')
|
||||||
|
->chunk(100, function ($feeds) {
|
||||||
|
foreach ($feeds as $feed) {
|
||||||
|
DB::table('feed_languages')->insert([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'language_id' => $feed->language_id,
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true, // Mark existing languages as primary
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('feed_languages');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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::table('feeds', function (Blueprint $table) {
|
||||||
|
// Drop foreign key constraint first
|
||||||
|
$table->dropForeign(['language_id']);
|
||||||
|
// Then drop the column
|
||||||
|
$table->dropColumn('language_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('feeds', function (Blueprint $table) {
|
||||||
|
// Re-add the language_id column
|
||||||
|
$table->foreignId('language_id')->nullable()->after('provider');
|
||||||
|
$table->foreign('language_id')->references('id')->on('languages');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -20,7 +20,6 @@ public function definition(): array
|
||||||
'url' => $this->faker->url(),
|
'url' => $this->faker->url(),
|
||||||
'type' => $this->faker->randomElement(['website', 'rss']),
|
'type' => $this->faker->randomElement(['website', 'rss']),
|
||||||
'provider' => $this->faker->randomElement(['vrt', 'belga']),
|
'provider' => $this->faker->randomElement(['vrt', 'belga']),
|
||||||
'language_id' => null,
|
|
||||||
'description' => $this->faker->optional()->sentence(),
|
'description' => $this->faker->optional()->sentence(),
|
||||||
'settings' => [],
|
'settings' => [],
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
|
|
@ -56,12 +55,31 @@ public function recentlyFetched(): static
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function language(Language $language): static
|
/**
|
||||||
|
* Attach languages to the feed after creation
|
||||||
|
*/
|
||||||
|
public function withLanguages(array $languageIds = [], bool $usePrimary = true): static
|
||||||
{
|
{
|
||||||
return $this->state(fn (array $attributes) => [
|
return $this->afterCreating(function (Feed $feed) use ($languageIds, $usePrimary) {
|
||||||
'language_id' => $language->id,
|
if (empty($languageIds)) {
|
||||||
|
// Default to English if no languages specified
|
||||||
|
$language = Language::where('short_code', 'en')->first();
|
||||||
|
if ($language) {
|
||||||
|
$languageIds = [$language->id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($languageIds as $index => $languageId) {
|
||||||
|
$feed->languages()->attach($languageId, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => $usePrimary && $index === 0,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public function vrt(): static
|
public function vrt(): static
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,6 @@
|
||||||
* @property string $url
|
* @property string $url
|
||||||
* @property string $type
|
* @property string $type
|
||||||
* @property string $provider
|
* @property string $provider
|
||||||
* @property int $language_id
|
|
||||||
* @property Language|null $language
|
|
||||||
* @property string $description
|
* @property string $description
|
||||||
* @property array<string, mixed> $settings
|
* @property array<string, mixed> $settings
|
||||||
* @property bool $is_active
|
* @property bool $is_active
|
||||||
|
|
@ -48,7 +46,6 @@ protected static function newFactory()
|
||||||
'url',
|
'url',
|
||||||
'type',
|
'type',
|
||||||
'provider',
|
'provider',
|
||||||
'language_id',
|
|
||||||
'description',
|
'description',
|
||||||
'settings',
|
'settings',
|
||||||
'is_active',
|
'is_active',
|
||||||
|
|
@ -120,10 +117,35 @@ public function articles(): HasMany
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<Language, $this>
|
* @return BelongsToMany<Language, $this>
|
||||||
*/
|
*/
|
||||||
public function language(): BelongsTo
|
public function languages(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Language::class);
|
return $this->belongsToMany(Language::class, 'feed_languages')
|
||||||
|
->withPivot(['url', 'settings', 'is_active', 'is_primary'])
|
||||||
|
->withTimestamps()
|
||||||
|
->wherePivot('is_active', true)
|
||||||
|
->orderByPivot('is_primary', 'desc');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the primary language for this feed
|
||||||
|
* @return Language|null
|
||||||
|
*/
|
||||||
|
public function getPrimaryLanguageAttribute(): ?Language
|
||||||
|
{
|
||||||
|
return $this->languages()->wherePivot('is_primary', true)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all languages including inactive ones
|
||||||
|
* @return BelongsToMany<Language, $this>
|
||||||
|
*/
|
||||||
|
public function allLanguages(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Language::class, 'feed_languages')
|
||||||
|
->withPivot(['url', 'settings', 'is_active', 'is_primary'])
|
||||||
|
->withTimestamps()
|
||||||
|
->orderByPivot('is_primary', 'desc');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,26 @@ public function rules(): array
|
||||||
return [
|
return [
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'provider' => 'required|in:vrt,belga',
|
'provider' => 'required|in:vrt,belga',
|
||||||
'language_id' => 'required|exists:languages,id',
|
'language_ids' => 'required|array|min:1',
|
||||||
|
'language_ids.*' => 'exists:languages,id',
|
||||||
|
'primary_language_id' => 'nullable|exists:languages,id',
|
||||||
|
'language_urls' => 'nullable|array',
|
||||||
|
'language_urls.*' => 'nullable|url',
|
||||||
'description' => 'nullable|string',
|
'description' => 'nullable|string',
|
||||||
'is_active' => 'boolean'
|
'is_active' => 'boolean'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'language_ids.required' => 'At least one language must be selected.',
|
||||||
|
'language_ids.*.exists' => 'The selected language is invalid.',
|
||||||
|
'primary_language_id.exists' => 'The selected primary language is invalid.',
|
||||||
|
'language_urls.*.url' => 'Each language URL must be a valid URL.',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -21,9 +21,26 @@ public function rules(): array
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'url' => 'required|url|unique:feeds,url,' . ($this->route('feed') instanceof Feed ? (string)$this->route('feed')->id : (string)$this->route('feed')),
|
'url' => 'required|url|unique:feeds,url,' . ($this->route('feed') instanceof Feed ? (string)$this->route('feed')->id : (string)$this->route('feed')),
|
||||||
'type' => 'required|in:website,rss',
|
'type' => 'required|in:website,rss',
|
||||||
'language_id' => 'required|exists:languages,id',
|
'language_ids' => 'required|array|min:1',
|
||||||
|
'language_ids.*' => 'exists:languages,id',
|
||||||
|
'primary_language_id' => 'nullable|exists:languages,id',
|
||||||
|
'language_urls' => 'nullable|array',
|
||||||
|
'language_urls.*' => 'nullable|url',
|
||||||
'description' => 'nullable|string',
|
'description' => 'nullable|string',
|
||||||
'is_active' => 'boolean'
|
'is_active' => 'boolean'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'language_ids.required' => 'At least one language must be selected.',
|
||||||
|
'language_ids.*.exists' => 'The selected language is invalid.',
|
||||||
|
'primary_language_id.exists' => 'The selected primary language is invalid.',
|
||||||
|
'language_urls.*.url' => 'Each language URL must be a valid URL.',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -20,7 +20,29 @@ public function toArray(Request $request): array
|
||||||
'url' => $this->url,
|
'url' => $this->url,
|
||||||
'type' => $this->type,
|
'type' => $this->type,
|
||||||
'provider' => $this->provider,
|
'provider' => $this->provider,
|
||||||
'language_id' => $this->language_id,
|
'language_ids' => $this->whenLoaded('languages', function () {
|
||||||
|
return $this->languages->pluck('id');
|
||||||
|
}),
|
||||||
|
'languages' => $this->whenLoaded('languages', function () {
|
||||||
|
return $this->languages->map(function ($language) {
|
||||||
|
return [
|
||||||
|
'id' => $language->id,
|
||||||
|
'short_code' => $language->short_code,
|
||||||
|
'name' => $language->name,
|
||||||
|
'url' => $language->pivot->url,
|
||||||
|
'is_active' => $language->pivot->is_active,
|
||||||
|
'is_primary' => $language->pivot->is_primary,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
'primary_language' => $this->whenLoaded('languages', function () {
|
||||||
|
$primary = $this->languages->where('pivot.is_primary', true)->first();
|
||||||
|
return $primary ? [
|
||||||
|
'id' => $primary->id,
|
||||||
|
'short_code' => $primary->short_code,
|
||||||
|
'name' => $primary->name,
|
||||||
|
] : null;
|
||||||
|
}),
|
||||||
'is_active' => $this->is_active,
|
'is_active' => $this->is_active,
|
||||||
'description' => $this->description,
|
'description' => $this->description,
|
||||||
'created_at' => $this->created_at->toISOString(),
|
'created_at' => $this->created_at->toISOString(),
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,12 @@ public function platformChannels(): HasMany
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<Feed, $this>
|
* @return BelongsToMany<Feed, $this>
|
||||||
*/
|
*/
|
||||||
public function feeds(): HasMany
|
public function feeds(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Feed::class);
|
return $this->belongsToMany(Feed::class, 'feed_languages')
|
||||||
|
->withPivot(['url', 'settings', 'is_active', 'is_primary'])
|
||||||
|
->withTimestamps();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,20 +108,31 @@ public function test_feed_model_creates_successfully(): void
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create();
|
||||||
|
|
||||||
$feed = Feed::factory()->create([
|
$feed = Feed::factory()->create([
|
||||||
'language_id' => $language->id,
|
|
||||||
'name' => 'Test Feed',
|
'name' => 'Test Feed',
|
||||||
'url' => 'https://example.com/feed.rss',
|
'url' => 'https://example.com/feed.rss',
|
||||||
'is_active' => true
|
'is_active' => true
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Attach language to feed
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('feeds', [
|
$this->assertDatabaseHas('feeds', [
|
||||||
'language_id' => $language->id,
|
|
||||||
'name' => 'Test Feed',
|
'name' => 'Test Feed',
|
||||||
'url' => 'https://example.com/feed.rss',
|
'url' => 'https://example.com/feed.rss',
|
||||||
'is_active' => true
|
'is_active' => true
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals($language->id, $feed->language->id);
|
$this->assertDatabaseHas('feed_languages', [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertTrue($feed->languages->contains('id', $language->id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_article_model_creates_successfully(): void
|
public function test_article_model_creates_successfully(): void
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,8 @@ public function test_store_creates_vrt_feed_successfully(): void
|
||||||
$feedData = [
|
$feedData = [
|
||||||
'name' => 'VRT Test Feed',
|
'name' => 'VRT Test Feed',
|
||||||
'provider' => 'vrt',
|
'provider' => 'vrt',
|
||||||
'language_id' => $language->id,
|
'language_ids' => [$language->id],
|
||||||
|
'primary_language_id' => $language->id,
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -86,7 +87,8 @@ public function test_store_creates_belga_feed_successfully(): void
|
||||||
$feedData = [
|
$feedData = [
|
||||||
'name' => 'Belga Test Feed',
|
'name' => 'Belga Test Feed',
|
||||||
'provider' => 'belga',
|
'provider' => 'belga',
|
||||||
'language_id' => $language->id,
|
'language_ids' => [$language->id],
|
||||||
|
'primary_language_id' => $language->id,
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -118,7 +120,8 @@ public function test_store_sets_default_active_status(): void
|
||||||
$feedData = [
|
$feedData = [
|
||||||
'name' => 'Test Feed',
|
'name' => 'Test Feed',
|
||||||
'provider' => 'vrt',
|
'provider' => 'vrt',
|
||||||
'language_id' => $language->id,
|
'language_ids' => [$language->id],
|
||||||
|
'primary_language_id' => $language->id,
|
||||||
// Not setting is_active
|
// Not setting is_active
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -137,7 +140,7 @@ public function test_store_validates_required_fields(): void
|
||||||
$response = $this->postJson('/api/v1/feeds', []);
|
$response = $this->postJson('/api/v1/feeds', []);
|
||||||
|
|
||||||
$response->assertStatus(422)
|
$response->assertStatus(422)
|
||||||
->assertJsonValidationErrors(['name', 'provider', 'language_id']);
|
->assertJsonValidationErrors(['name', 'provider', 'language_ids']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_store_rejects_invalid_provider(): void
|
public function test_store_rejects_invalid_provider(): void
|
||||||
|
|
@ -147,7 +150,8 @@ public function test_store_rejects_invalid_provider(): void
|
||||||
$feedData = [
|
$feedData = [
|
||||||
'name' => 'Invalid Feed',
|
'name' => 'Invalid Feed',
|
||||||
'provider' => 'invalid',
|
'provider' => 'invalid',
|
||||||
'language_id' => $language->id,
|
'language_ids' => [$language->id],
|
||||||
|
'primary_language_id' => $language->id,
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->postJson('/api/v1/feeds', $feedData);
|
$response = $this->postJson('/api/v1/feeds', $feedData);
|
||||||
|
|
@ -183,13 +187,14 @@ public function test_show_returns_404_for_nonexistent_feed(): void
|
||||||
public function test_update_modifies_feed_successfully(): void
|
public function test_update_modifies_feed_successfully(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create();
|
||||||
$feed = Feed::factory()->language($language)->create(['name' => 'Original Name']);
|
$feed = Feed::factory()->withLanguages([$language->id])->create(['name' => 'Original Name']);
|
||||||
|
|
||||||
$updateData = [
|
$updateData = [
|
||||||
'name' => 'Updated Name',
|
'name' => 'Updated Name',
|
||||||
'url' => $feed->url,
|
'url' => $feed->url,
|
||||||
'type' => $feed->type,
|
'type' => $feed->type,
|
||||||
'language_id' => $feed->language_id,
|
'language_ids' => [$language->id],
|
||||||
|
'primary_language_id' => $language->id,
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->putJson("/api/v1/feeds/{$feed->id}", $updateData);
|
$response = $this->putJson("/api/v1/feeds/{$feed->id}", $updateData);
|
||||||
|
|
@ -212,13 +217,14 @@ public function test_update_modifies_feed_successfully(): void
|
||||||
public function test_update_preserves_active_status_when_not_provided(): void
|
public function test_update_preserves_active_status_when_not_provided(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create();
|
||||||
$feed = Feed::factory()->language($language)->create(['is_active' => false]);
|
$feed = Feed::factory()->withLanguages([$language->id])->create(['is_active' => false]);
|
||||||
|
|
||||||
$updateData = [
|
$updateData = [
|
||||||
'name' => $feed->name,
|
'name' => $feed->name,
|
||||||
'url' => $feed->url,
|
'url' => $feed->url,
|
||||||
'type' => $feed->type,
|
'type' => $feed->type,
|
||||||
'language_id' => $feed->language_id,
|
'language_ids' => [$language->id],
|
||||||
|
'primary_language_id' => $language->id,
|
||||||
// Not providing is_active
|
// Not providing is_active
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ public function test_status_shows_channel_step_when_platform_account_and_feed_ex
|
||||||
{
|
{
|
||||||
$language = Language::first();
|
$language = Language::first();
|
||||||
PlatformAccount::factory()->create(['is_active' => true]);
|
PlatformAccount::factory()->create(['is_active' => true]);
|
||||||
Feed::factory()->language($language)->create(['is_active' => true]);
|
Feed::factory()->withLanguages([$language->id])->create(['is_active' => true]);
|
||||||
|
|
||||||
$response = $this->getJson('/api/v1/onboarding/status');
|
$response = $this->getJson('/api/v1/onboarding/status');
|
||||||
|
|
||||||
|
|
@ -96,7 +96,7 @@ public function test_status_shows_route_step_when_platform_account_feed_and_chan
|
||||||
{
|
{
|
||||||
$language = Language::first();
|
$language = Language::first();
|
||||||
PlatformAccount::factory()->create(['is_active' => true]);
|
PlatformAccount::factory()->create(['is_active' => true]);
|
||||||
Feed::factory()->language($language)->create(['is_active' => true]);
|
Feed::factory()->withLanguages([$language->id])->create(['is_active' => true]);
|
||||||
PlatformChannel::factory()->create(['is_active' => true]);
|
PlatformChannel::factory()->create(['is_active' => true]);
|
||||||
|
|
||||||
$response = $this->getJson('/api/v1/onboarding/status');
|
$response = $this->getJson('/api/v1/onboarding/status');
|
||||||
|
|
@ -119,7 +119,7 @@ public function test_status_shows_no_onboarding_needed_when_all_components_exist
|
||||||
{
|
{
|
||||||
$language = Language::first();
|
$language = Language::first();
|
||||||
PlatformAccount::factory()->create(['is_active' => true]);
|
PlatformAccount::factory()->create(['is_active' => true]);
|
||||||
Feed::factory()->language($language)->create(['is_active' => true]);
|
Feed::factory()->withLanguages([$language->id])->create(['is_active' => true]);
|
||||||
PlatformChannel::factory()->create(['is_active' => true]);
|
PlatformChannel::factory()->create(['is_active' => true]);
|
||||||
Route::factory()->create(['is_active' => true]);
|
Route::factory()->create(['is_active' => true]);
|
||||||
|
|
||||||
|
|
@ -194,7 +194,7 @@ public function test_create_feed_validates_required_fields()
|
||||||
$response = $this->postJson('/api/v1/onboarding/feed', []);
|
$response = $this->postJson('/api/v1/onboarding/feed', []);
|
||||||
|
|
||||||
$response->assertStatus(422)
|
$response->assertStatus(422)
|
||||||
->assertJsonValidationErrors(['name', 'provider', 'language_id']);
|
->assertJsonValidationErrors(['name', 'provider', 'language_ids']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_create_feed_creates_vrt_feed_successfully()
|
public function test_create_feed_creates_vrt_feed_successfully()
|
||||||
|
|
@ -202,7 +202,8 @@ public function test_create_feed_creates_vrt_feed_successfully()
|
||||||
$feedData = [
|
$feedData = [
|
||||||
'name' => 'VRT Test Feed',
|
'name' => 'VRT Test Feed',
|
||||||
'provider' => 'vrt',
|
'provider' => 'vrt',
|
||||||
'language_id' => 1,
|
'language_ids' => [1],
|
||||||
|
'primary_language_id' => 1,
|
||||||
'description' => 'Test description',
|
'description' => 'Test description',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -223,9 +224,13 @@ public function test_create_feed_creates_vrt_feed_successfully()
|
||||||
'name' => 'VRT Test Feed',
|
'name' => 'VRT Test Feed',
|
||||||
'url' => 'https://www.vrt.be/vrtnws/en/',
|
'url' => 'https://www.vrt.be/vrtnws/en/',
|
||||||
'type' => 'website',
|
'type' => 'website',
|
||||||
'language_id' => 1,
|
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('feed_languages', [
|
||||||
|
'language_id' => 1,
|
||||||
|
'is_primary' => true,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_create_feed_creates_belga_feed_successfully()
|
public function test_create_feed_creates_belga_feed_successfully()
|
||||||
|
|
@ -233,7 +238,8 @@ public function test_create_feed_creates_belga_feed_successfully()
|
||||||
$feedData = [
|
$feedData = [
|
||||||
'name' => 'Belga Test Feed',
|
'name' => 'Belga Test Feed',
|
||||||
'provider' => 'belga',
|
'provider' => 'belga',
|
||||||
'language_id' => 1,
|
'language_ids' => [1],
|
||||||
|
'primary_language_id' => 1,
|
||||||
'description' => 'Test description',
|
'description' => 'Test description',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -254,9 +260,13 @@ public function test_create_feed_creates_belga_feed_successfully()
|
||||||
'name' => 'Belga Test Feed',
|
'name' => 'Belga Test Feed',
|
||||||
'url' => 'https://www.belganewsagency.eu/',
|
'url' => 'https://www.belganewsagency.eu/',
|
||||||
'type' => 'website',
|
'type' => 'website',
|
||||||
'language_id' => 1,
|
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('feed_languages', [
|
||||||
|
'language_id' => 1,
|
||||||
|
'is_primary' => true,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_create_feed_rejects_invalid_provider()
|
public function test_create_feed_rejects_invalid_provider()
|
||||||
|
|
@ -264,7 +274,8 @@ public function test_create_feed_rejects_invalid_provider()
|
||||||
$feedData = [
|
$feedData = [
|
||||||
'name' => 'Invalid Feed',
|
'name' => 'Invalid Feed',
|
||||||
'provider' => 'invalid',
|
'provider' => 'invalid',
|
||||||
'language_id' => 1,
|
'language_ids' => [1],
|
||||||
|
'primary_language_id' => 1,
|
||||||
'description' => 'Test description',
|
'description' => 'Test description',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -333,7 +344,7 @@ public function test_create_route_validates_required_fields()
|
||||||
public function test_create_route_creates_route_successfully()
|
public function test_create_route_creates_route_successfully()
|
||||||
{
|
{
|
||||||
$language = Language::first();
|
$language = Language::first();
|
||||||
$feed = Feed::factory()->language($language)->create();
|
$feed = Feed::factory()->withLanguages([$language->id])->create();
|
||||||
$platformChannel = PlatformChannel::factory()->create();
|
$platformChannel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
$routeData = [
|
$routeData = [
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,15 @@ public function test_index_returns_successful_response(): void
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
|
|
||||||
// Create unique feeds and channels for this test
|
// Create unique feeds and channels for this test
|
||||||
$feeds = Feed::factory()->count(3)->create(['language_id' => $language->id]);
|
$feeds = Feed::factory()->count(3)->create();
|
||||||
|
// Attach language to each feed
|
||||||
|
foreach ($feeds as $feed) {
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
}
|
||||||
$channels = PlatformChannel::factory()->count(3)->create([
|
$channels = PlatformChannel::factory()->count(3)->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id
|
||||||
|
|
@ -60,7 +68,12 @@ public function test_store_creates_routing_configuration_successfully(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create();
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create();
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id
|
||||||
|
|
@ -132,7 +145,12 @@ public function test_store_validates_feed_exists(): void
|
||||||
public function test_store_validates_platform_channel_exists(): void
|
public function test_store_validates_platform_channel_exists(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create();
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create();
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'feed_id' => $feed->id,
|
'feed_id' => $feed->id,
|
||||||
|
|
@ -149,7 +167,12 @@ public function test_show_returns_routing_configuration_successfully(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create();
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create();
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id
|
||||||
|
|
@ -185,7 +208,12 @@ public function test_show_returns_404_for_nonexistent_routing(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create();
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create();
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id
|
||||||
|
|
@ -204,7 +232,12 @@ public function test_update_modifies_routing_configuration_successfully(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create();
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create();
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id
|
||||||
|
|
@ -242,7 +275,12 @@ public function test_update_returns_404_for_nonexistent_routing(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create();
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create();
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id
|
||||||
|
|
@ -263,7 +301,12 @@ public function test_destroy_deletes_routing_configuration_successfully(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create();
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create();
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id
|
||||||
|
|
@ -292,7 +335,12 @@ public function test_destroy_returns_404_for_nonexistent_routing(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create();
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create();
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id
|
||||||
|
|
@ -311,7 +359,12 @@ public function test_toggle_activates_inactive_routing(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create();
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create();
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id
|
||||||
|
|
@ -342,7 +395,12 @@ public function test_toggle_deactivates_active_routing(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create();
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create();
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id
|
||||||
|
|
@ -373,7 +431,12 @@ public function test_toggle_returns_404_for_nonexistent_routing(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create();
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create();
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
$channel = PlatformChannel::factory()->create([
|
$channel = PlatformChannel::factory()->create([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'language_id' => $language->id
|
'language_id' => $language->id
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class FeedTest extends TestCase
|
||||||
|
|
||||||
public function test_fillable_fields(): void
|
public function test_fillable_fields(): void
|
||||||
{
|
{
|
||||||
$fillableFields = ['name', 'url', 'type', 'provider', 'language_id', 'description', 'settings', 'is_active', 'last_fetched_at'];
|
$fillableFields = ['name', 'url', 'type', 'provider', 'description', 'settings', 'is_active', 'last_fetched_at'];
|
||||||
$feed = new Feed();
|
$feed = new Feed();
|
||||||
|
|
||||||
$this->assertEquals($fillableFields, $feed->getFillable());
|
$this->assertEquals($fillableFields, $feed->getFillable());
|
||||||
|
|
@ -113,14 +113,33 @@ public function test_status_attribute_fetched_days_ago(): void
|
||||||
$this->assertStringContainsString('ago', $feed->status);
|
$this->assertStringContainsString('ago', $feed->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_belongs_to_language_relationship(): void
|
public function test_belongs_to_many_languages_relationship(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
$feed = Feed::factory()->create();
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$language1 = Language::factory()->create();
|
||||||
|
$language2 = Language::factory()->create();
|
||||||
|
|
||||||
$this->assertInstanceOf(Language::class, $feed->language);
|
$feed->languages()->attach($language1->id, [
|
||||||
$this->assertEquals($language->id, $feed->language->id);
|
'url' => 'https://example.com/en',
|
||||||
$this->assertEquals($language->name, $feed->language->name);
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
$feed->languages()->attach($language2->id, [
|
||||||
|
'url' => 'https://example.com/nl',
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => false
|
||||||
|
]);
|
||||||
|
|
||||||
|
$languages = $feed->languages;
|
||||||
|
|
||||||
|
$this->assertCount(2, $languages);
|
||||||
|
$this->assertTrue($languages->contains('id', $language1->id));
|
||||||
|
$this->assertTrue($languages->contains('id', $language2->id));
|
||||||
|
|
||||||
|
// Test primary language
|
||||||
|
$primaryLanguage = $feed->primary_language;
|
||||||
|
$this->assertNotNull($primaryLanguage);
|
||||||
|
$this->assertEquals($language1->id, $primaryLanguage->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_has_many_articles_relationship(): void
|
public function test_has_many_articles_relationship(): void
|
||||||
|
|
@ -241,7 +260,6 @@ public function test_feed_creation_with_explicit_values(): void
|
||||||
'url' => 'https://example.com/feed',
|
'url' => 'https://example.com/feed',
|
||||||
'type' => 'rss',
|
'type' => 'rss',
|
||||||
'provider' => 'vrt',
|
'provider' => 'vrt',
|
||||||
'language_id' => $language->id,
|
|
||||||
'description' => 'Test description',
|
'description' => 'Test description',
|
||||||
'settings' => $settings,
|
'settings' => $settings,
|
||||||
'is_active' => false
|
'is_active' => false
|
||||||
|
|
@ -250,7 +268,14 @@ public function test_feed_creation_with_explicit_values(): void
|
||||||
$this->assertEquals('Test Feed', $feed->name);
|
$this->assertEquals('Test Feed', $feed->name);
|
||||||
$this->assertEquals('https://example.com/feed', $feed->url);
|
$this->assertEquals('https://example.com/feed', $feed->url);
|
||||||
$this->assertEquals('rss', $feed->type);
|
$this->assertEquals('rss', $feed->type);
|
||||||
$this->assertEquals($language->id, $feed->language_id);
|
// Attach language to feed
|
||||||
|
$feed->languages()->attach($language->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertTrue($feed->languages->contains('id', $language->id));
|
||||||
$this->assertEquals('Test description', $feed->description);
|
$this->assertEquals('Test description', $feed->description);
|
||||||
$this->assertEquals($settings, $feed->settings);
|
$this->assertEquals($settings, $feed->settings);
|
||||||
$this->assertFalse($feed->is_active);
|
$this->assertFalse($feed->is_active);
|
||||||
|
|
|
||||||
|
|
@ -80,22 +80,40 @@ public function test_has_many_platform_channels_relationship(): void
|
||||||
$this->assertInstanceOf(PlatformChannel::class, $channels->first());
|
$this->assertInstanceOf(PlatformChannel::class, $channels->first());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_has_many_feeds_relationship(): void
|
public function test_belongs_to_many_feeds_relationship(): void
|
||||||
{
|
{
|
||||||
$language = Language::factory()->create();
|
$language = Language::factory()->create();
|
||||||
|
|
||||||
$feed1 = Feed::factory()->create(['language_id' => $language->id]);
|
$feed1 = Feed::factory()->create();
|
||||||
$feed2 = Feed::factory()->create(['language_id' => $language->id]);
|
$feed2 = Feed::factory()->create();
|
||||||
|
|
||||||
|
// Attach feeds to this language
|
||||||
|
$language->feeds()->attach($feed1->id, [
|
||||||
|
'url' => $feed1->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
$language->feeds()->attach($feed2->id, [
|
||||||
|
'url' => $feed2->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => false
|
||||||
|
]);
|
||||||
|
|
||||||
// Create feed for different language
|
// Create feed for different language
|
||||||
$otherLanguage = Language::factory()->create();
|
$otherLanguage = Language::factory()->create();
|
||||||
Feed::factory()->create(['language_id' => $otherLanguage->id]);
|
$feed3 = Feed::factory()->create();
|
||||||
|
$otherLanguage->feeds()->attach($feed3->id, [
|
||||||
|
'url' => $feed3->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
$feeds = $language->feeds;
|
$feeds = $language->feeds;
|
||||||
|
|
||||||
$this->assertCount(2, $feeds);
|
$this->assertCount(2, $feeds);
|
||||||
$this->assertTrue($feeds->contains('id', $feed1->id));
|
$this->assertTrue($feeds->contains('id', $feed1->id));
|
||||||
$this->assertTrue($feeds->contains('id', $feed2->id));
|
$this->assertTrue($feeds->contains('id', $feed2->id));
|
||||||
|
$this->assertFalse($feeds->contains('id', $feed3->id));
|
||||||
$this->assertInstanceOf(Feed::class, $feeds->first());
|
$this->assertInstanceOf(Feed::class, $feeds->first());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -295,7 +313,12 @@ public function test_language_relationships_maintain_referential_integrity(): vo
|
||||||
// Create related models
|
// Create related models
|
||||||
$instance = PlatformInstance::factory()->create();
|
$instance = PlatformInstance::factory()->create();
|
||||||
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
|
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
|
||||||
$feed = Feed::factory()->create(['language_id' => $language->id]);
|
$feed = Feed::factory()->create();
|
||||||
|
$language->feeds()->attach($feed->id, [
|
||||||
|
'url' => $feed->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_primary' => true
|
||||||
|
]);
|
||||||
|
|
||||||
// Attach instance
|
// Attach instance
|
||||||
$language->platformInstances()->attach($instance->id, [
|
$language->platformInstances()->attach($instance->id, [
|
||||||
|
|
@ -309,7 +332,7 @@ public function test_language_relationships_maintain_referential_integrity(): vo
|
||||||
$this->assertCount(1, $language->feeds);
|
$this->assertCount(1, $language->feeds);
|
||||||
|
|
||||||
$this->assertEquals($language->id, $channel->language_id);
|
$this->assertEquals($language->id, $channel->language_id);
|
||||||
$this->assertEquals($language->id, $feed->language_id);
|
$this->assertTrue($language->feeds->contains('id', $feed->id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_language_factory_unique_constraints(): void
|
public function test_language_factory_unique_constraints(): void
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,15 @@ export interface Article {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feed types
|
// Feed types
|
||||||
|
export interface FeedLanguage {
|
||||||
|
id: number;
|
||||||
|
short_code: string;
|
||||||
|
name: string;
|
||||||
|
url?: string;
|
||||||
|
is_active: boolean;
|
||||||
|
is_primary: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Feed {
|
export interface Feed {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -56,7 +65,13 @@ export interface Feed {
|
||||||
type: 'website' | 'rss';
|
type: 'website' | 'rss';
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
language_id?: number;
|
language_ids?: number[];
|
||||||
|
languages?: FeedLanguage[];
|
||||||
|
primary_language?: {
|
||||||
|
id: number;
|
||||||
|
short_code: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
provider?: string;
|
provider?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
|
@ -164,6 +179,12 @@ export interface OnboardingStatus {
|
||||||
export interface FeedProvider {
|
export interface FeedProvider {
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
supported_languages?: {
|
||||||
|
[key: string]: {
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OnboardingOptions {
|
export interface OnboardingOptions {
|
||||||
|
|
@ -184,7 +205,9 @@ export interface PlatformAccountRequest {
|
||||||
export interface FeedRequest {
|
export interface FeedRequest {
|
||||||
name: string;
|
name: string;
|
||||||
provider: 'vrt' | 'belga';
|
provider: 'vrt' | 'belga';
|
||||||
language_id: number;
|
language_ids: number[];
|
||||||
|
primary_language_id?: number;
|
||||||
|
language_urls?: { [key: number]: string };
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,11 @@ const FeedStep: React.FC = () => {
|
||||||
const [formData, setFormData] = useState<FeedRequest>({
|
const [formData, setFormData] = useState<FeedRequest>({
|
||||||
name: '',
|
name: '',
|
||||||
provider: 'vrt',
|
provider: 'vrt',
|
||||||
language_id: 0,
|
language_ids: [],
|
||||||
|
primary_language_id: undefined,
|
||||||
description: ''
|
description: ''
|
||||||
});
|
});
|
||||||
|
const [selectedProvider, setSelectedProvider] = useState<FeedProvider | null>(null);
|
||||||
const [errors, setErrors] = useState<Record<string, string[]>>({});
|
const [errors, setErrors] = useState<Record<string, string[]>>({});
|
||||||
|
|
||||||
// Get onboarding options (languages)
|
// Get onboarding options (languages)
|
||||||
|
|
@ -33,12 +35,21 @@ const FeedStep: React.FC = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: firstFeed.name || '',
|
name: firstFeed.name || '',
|
||||||
provider: (firstFeed.provider === 'vrt' || firstFeed.provider === 'belga') ? firstFeed.provider : 'vrt',
|
provider: (firstFeed.provider === 'vrt' || firstFeed.provider === 'belga') ? firstFeed.provider : 'vrt',
|
||||||
language_id: firstFeed.language_id ?? 0,
|
language_ids: firstFeed.language_ids || [],
|
||||||
|
primary_language_id: firstFeed.primary_language?.id,
|
||||||
description: firstFeed.description || ''
|
description: firstFeed.description || ''
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [feeds]);
|
}, [feeds]);
|
||||||
|
|
||||||
|
// Update selected provider when provider changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (options?.feed_providers && formData.provider) {
|
||||||
|
const provider = options.feed_providers.find(p => p.code === formData.provider);
|
||||||
|
setSelectedProvider(provider || null);
|
||||||
|
}
|
||||||
|
}, [formData.provider, options]);
|
||||||
|
|
||||||
const createFeedMutation = useMutation({
|
const createFeedMutation = useMutation({
|
||||||
mutationFn: (data: FeedRequest) => apiClient.createFeedForOnboarding(data),
|
mutationFn: (data: FeedRequest) => apiClient.createFeedForOnboarding(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
@ -135,25 +146,74 @@ const FeedStep: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="language_id" className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Language
|
Languages
|
||||||
|
{selectedProvider?.supported_languages && (
|
||||||
|
<span className="text-xs text-gray-500 ml-2">
|
||||||
|
(Provider supports: {Object.values(selectedProvider.supported_languages).map(l => l.name).join(', ')})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<div className="space-y-2">
|
||||||
id="language_id"
|
{options?.languages.map((language: Language) => {
|
||||||
value={formData.language_id || ''}
|
const isSupported = !selectedProvider?.supported_languages ||
|
||||||
onChange={(e) => handleChange('language_id', e.target.value ? parseInt(e.target.value) : 0)}
|
Object.keys(selectedProvider.supported_languages).includes(language.short_code);
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
const isSelected = formData.language_ids.includes(language.id);
|
||||||
required
|
const isPrimary = formData.primary_language_id === language.id;
|
||||||
>
|
|
||||||
<option value="">Select language</option>
|
return (
|
||||||
{options?.languages.map((language: Language) => (
|
<div key={language.id} className={`flex items-center space-x-3 p-2 rounded ${!isSupported ? 'opacity-50' : ''}`}>
|
||||||
<option key={language.id} value={language.id}>
|
<input
|
||||||
{language.name}
|
type="checkbox"
|
||||||
</option>
|
id={`language-${language.id}`}
|
||||||
))}
|
checked={isSelected}
|
||||||
</select>
|
disabled={!isSupported}
|
||||||
{errors.language_id && (
|
onChange={(e) => {
|
||||||
<p className="text-red-600 text-sm mt-1">{errors.language_id[0]}</p>
|
if (e.target.checked) {
|
||||||
|
const newLanguageIds = [...formData.language_ids, language.id];
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
language_ids: newLanguageIds,
|
||||||
|
primary_language_id: newLanguageIds.length === 1 ? language.id : prev.primary_language_id
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
const newLanguageIds = formData.language_ids.filter(id => id !== language.id);
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
language_ids: newLanguageIds,
|
||||||
|
primary_language_id: prev.primary_language_id === language.id
|
||||||
|
? (newLanguageIds[0] || undefined)
|
||||||
|
: prev.primary_language_id
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<label htmlFor={`language-${language.id}`} className="flex-1 cursor-pointer">
|
||||||
|
{language.name} ({language.short_code})
|
||||||
|
{!isSupported && <span className="text-xs text-gray-500 ml-2">(Not supported by provider)</span>}
|
||||||
|
</label>
|
||||||
|
{isSelected && formData.language_ids.length > 1 && (
|
||||||
|
<label className="flex items-center space-x-1">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="primary-language"
|
||||||
|
checked={isPrimary}
|
||||||
|
onChange={() => setFormData(prev => ({ ...prev, primary_language_id: language.id }))}
|
||||||
|
className="h-3 w-3 text-blue-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-600">Primary</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{formData.language_ids.length === 0 && (
|
||||||
|
<p className="text-red-600 text-sm mt-1">Please select at least one language</p>
|
||||||
|
)}
|
||||||
|
{errors.language_ids && (
|
||||||
|
<p className="text-red-600 text-sm mt-1">{errors.language_ids[0]}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue