From 34b7627bb263fc0e42a87b222e7f4026a465a954 Mon Sep 17 00:00:00 2001 From: Vincent Auger Date: Fri, 3 Jan 2025 14:46:22 -0400 Subject: [PATCH] initial announcement system - closes #752 --- .env.example | 3 - .../CheckAnnouncementController.php | 30 +++++++ .../OhDear/CheckStatusPageController.php | 38 --------- app/Http/Resources/AnnouncementResource.php | 36 +++++++++ app/Models/Announcement.php | 35 ++++++++ app/Policies/AnnouncementPolicy.php | 50 ++++++++++++ config/osp.php | 5 -- database/factories/AnnouncementFactory.php | 28 +++++++ ...1_03_170952_create_announcements_table.php | 25 ++++++ resources/src/api/OhDearStatus.ts | 81 ------------------- resources/src/auto-imports.d.ts | 2 + .../src/components/OhDearStatusMonitor.vue | 74 ----------------- resources/src/layouts/MainLayout.vue | 8 +- .../src/models/Announcement/Announcement.ts | 27 +++++++ .../components/AnnouncementMonitor.vue | 73 +++++++++++++++++ resources/src/models/Expertise/Expertise.ts | 4 +- routes/api.php | 4 +- tests/Feature/Models/AnnouncementTest.php | 35 ++++++++ 18 files changed, 349 insertions(+), 209 deletions(-) create mode 100644 app/Http/Controllers/Announcement/CheckAnnouncementController.php delete mode 100644 app/Http/Controllers/OhDear/CheckStatusPageController.php create mode 100644 app/Http/Resources/AnnouncementResource.php create mode 100644 app/Models/Announcement.php create mode 100644 app/Policies/AnnouncementPolicy.php create mode 100644 database/factories/AnnouncementFactory.php create mode 100644 database/migrations/2025_01_03_170952_create_announcements_table.php delete mode 100644 resources/src/api/OhDearStatus.ts delete mode 100644 resources/src/components/OhDearStatusMonitor.vue create mode 100644 resources/src/models/Announcement/Announcement.ts create mode 100644 resources/src/models/Announcement/components/AnnouncementMonitor.vue create mode 100644 tests/Feature/Models/AnnouncementTest.php diff --git a/.env.example b/.env.example index d45578c6..393ebb96 100644 --- a/.env.example +++ b/.env.example @@ -68,9 +68,6 @@ USE_OLLAMA=false OLLAMA_MODEL=llama3.2 OLLAMA_URL= -OHDEAR_ENABLED=false -OHDEAR_URL="" - ORCID_USE_SANDBOX=false ORCID_REDIRECT_URI="${FRONTEND_URL}api/orcid/callback" ORCID_CLIENT_ID="" diff --git a/app/Http/Controllers/Announcement/CheckAnnouncementController.php b/app/Http/Controllers/Announcement/CheckAnnouncementController.php new file mode 100644 index 00000000..cb6c889e --- /dev/null +++ b/app/Http/Controllers/Announcement/CheckAnnouncementController.php @@ -0,0 +1,30 @@ +orderBy('start_at', 'desc')->get(); + + if ($announcment->isEmpty()) { + return response()->json([], 204); + } + + return AnnouncementResource::collection($announcment); + + } +} diff --git a/app/Http/Controllers/OhDear/CheckStatusPageController.php b/app/Http/Controllers/OhDear/CheckStatusPageController.php deleted file mode 100644 index 2bc83c52..00000000 --- a/app/Http/Controllers/OhDear/CheckStatusPageController.php +++ /dev/null @@ -1,38 +0,0 @@ -json([ - 'message' => 'Status Monitoring is not enabled', - ], 202); - } - - $locale = $this->getLocaleFromRequest($request); - $url = config('osp.ohdear.url').'/json?locale='.$locale; - $rememberFor = 60 * 10; - - $statusResponse = Cache::remember($url, $rememberFor, function () use ($url) { - return Http::get($url)->json(); - }); - - return response()->json($statusResponse); - - } -} diff --git a/app/Http/Resources/AnnouncementResource.php b/app/Http/Resources/AnnouncementResource.php new file mode 100644 index 00000000..b781c63b --- /dev/null +++ b/app/Http/Resources/AnnouncementResource.php @@ -0,0 +1,36 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'data' => [ + 'id' => $this->id, + 'title_en' => $this->title_en, + 'title_fr' => $this->title_fr, + 'text_en' => $this->text_en, + 'text_fr' => $this->text_fr, + 'start_at' => $this->start_at, + 'end_at' => $this->end_at, + ], + 'can' => [ + 'update' => false, + 'delete' => false, + ], + ]; + } +} diff --git a/app/Models/Announcement.php b/app/Models/Announcement.php new file mode 100644 index 00000000..9cfe4571 --- /dev/null +++ b/app/Models/Announcement.php @@ -0,0 +1,35 @@ + */ + use HasFactory; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + public function casts(): array + { + return [ + 'start_at' => 'datetime', + 'end_at' => 'datetime', + ]; + } + + /** + * Return active announcements + */ + public function scopeActive(Builder $query) + { + return $query->where('start_at', '<=', now()) + ->where('end_at', '>=', now()); + } +} diff --git a/app/Policies/AnnouncementPolicy.php b/app/Policies/AnnouncementPolicy.php new file mode 100644 index 00000000..b6626f14 --- /dev/null +++ b/app/Policies/AnnouncementPolicy.php @@ -0,0 +1,50 @@ +hasRole(UserRole::ADMIN); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, Announcement $announcement): bool + { + return $user->hasRole(UserRole::ADMIN); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->hasRole(UserRole::ADMIN); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Announcement $announcement): bool + { + return $user->hasRole(UserRole::ADMIN); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Announcement $announcement): bool + { + return $user->hasRole(UserRole::ADMIN); + } +} diff --git a/config/osp.php b/config/osp.php index 042132c2..f6322248 100644 --- a/config/osp.php +++ b/config/osp.php @@ -46,11 +46,6 @@ 'redirect_uri' => env('ORCID_REDIRECT_URI'), ], - 'ohdear' => [ - 'enabled' => env('OHDEAR_ENABLED', false), - 'url' => env('OHDEAR_URL'), - ], - 'ollama' => [ 'enabled' => env('USE_OLLAMA', false), 'url' => env('OLLAMA_URL'), diff --git a/database/factories/AnnouncementFactory.php b/database/factories/AnnouncementFactory.php new file mode 100644 index 00000000..453445a9 --- /dev/null +++ b/database/factories/AnnouncementFactory.php @@ -0,0 +1,28 @@ + + */ +class AnnouncementFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'title_en' => $this->faker->sentence, + 'title_fr' => $this->faker->sentence, + 'text_en' => $this->faker->paragraph, + 'text_fr' => $this->faker->paragraph, + 'start_at' => $this->faker->dateTimeBetween('-2 week', '-1 week'), + 'end_at' => $this->faker->dateTimeBetween('+1 week', '+2 week'), + ]; + } +} diff --git a/database/migrations/2025_01_03_170952_create_announcements_table.php b/database/migrations/2025_01_03_170952_create_announcements_table.php new file mode 100644 index 00000000..b33f1d6a --- /dev/null +++ b/database/migrations/2025_01_03_170952_create_announcements_table.php @@ -0,0 +1,25 @@ +id(); + $table->timestamps(); + $table->string('title_en', 255); + $table->string('title_fr', 255); + $table->string('text_en', 500); + $table->string('text_fr', 500); + $table->dateTime('start_at'); + $table->dateTime('end_at'); + }); + } +}; diff --git a/resources/src/api/OhDearStatus.ts b/resources/src/api/OhDearStatus.ts deleted file mode 100644 index 906055da..00000000 --- a/resources/src/api/OhDearStatus.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { Locale } from '@/stores/LocaleStore' -import { http } from '@/api/http' - -export interface OhDearStatusPage { - title: string - timezone: string - pinnedUpdate: Update | null - sites: Sites - updatesPerDay: { [key: string]: Update[] } - summarizedStatus: string -} - -export interface Update { - id: number - title: string - text: string - pinned: boolean - severity: 'info' | 'warning' | 'high' | 'resolved' | 'scheduled' - time: number -} - -export interface Sites { - [key: string]: Site[] -} - -export interface Site { - label: string - url: string - status: string -} - -export class OhDearStatus { - private readonly baseUrl: string - locale: Locale - OhDearStatusPage: OhDearStatusPage | null = null - - constructor(baseUrl: string, locale: Locale) { - this.baseUrl = baseUrl - this.locale = locale - } - - public async updateLocale(locale: Locale) { - this.locale = locale - return this.updateStatus() - } - - public async updateStatus() { - const response = await http.get( - `${this.baseUrl}?locale=${this.locale}`, - ) - - if (response.status !== 200) { - throw new Error(`OhDearStatus: ${response.statusText}`) - } - this.OhDearStatusPage = response.data - return this.OhDearStatusPage - } - - public hasPinnedUpdate(): boolean { - if (this.OhDearStatusPage === null) - return false - if (this.OhDearStatusPage.pinnedUpdate === null) - return false - return true - } - - public getPinnedUpdateDate(): Date | null { - if (this.OhDearStatusPage === null) - return null - if (this.OhDearStatusPage.pinnedUpdate?.time === undefined) - return null - - const secondsSinceEpoch = this.OhDearStatusPage.pinnedUpdate.time - const time = new Date(secondsSinceEpoch * 1000) - return time - } - - public getPinnedUpdateId(): number | null { - return this.OhDearStatusPage?.pinnedUpdate?.id ?? null - } -} diff --git a/resources/src/auto-imports.d.ts b/resources/src/auto-imports.d.ts index 3cb857ef..d685693d 100644 --- a/resources/src/auto-imports.d.ts +++ b/resources/src/auto-imports.d.ts @@ -71,6 +71,7 @@ declare global { const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] const onDeactivated: typeof import('vue')['onDeactivated'] + const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval'] const onErrorCaptured: typeof import('vue')['onErrorCaptured'] const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke'] const onLongPress: typeof import('@vueuse/core')['onLongPress'] @@ -409,6 +410,7 @@ declare module 'vue' { readonly onBeforeUpdate: UnwrapRef readonly onClickOutside: UnwrapRef readonly onDeactivated: UnwrapRef + readonly onElementRemoval: UnwrapRef readonly onErrorCaptured: UnwrapRef readonly onKeyStroke: UnwrapRef readonly onLongPress: UnwrapRef diff --git a/resources/src/components/OhDearStatusMonitor.vue b/resources/src/components/OhDearStatusMonitor.vue deleted file mode 100644 index fc214c2e..00000000 --- a/resources/src/components/OhDearStatusMonitor.vue +++ /dev/null @@ -1,74 +0,0 @@ - - - - - diff --git a/resources/src/layouts/MainLayout.vue b/resources/src/layouts/MainLayout.vue index 3ca1c468..c653d312 100644 --- a/resources/src/layouts/MainLayout.vue +++ b/resources/src/layouts/MainLayout.vue @@ -1,9 +1,9 @@ + + + + diff --git a/resources/src/models/Expertise/Expertise.ts b/resources/src/models/Expertise/Expertise.ts index 5aac1eb5..37ae6743 100644 --- a/resources/src/models/Expertise/Expertise.ts +++ b/resources/src/models/Expertise/Expertise.ts @@ -1,7 +1,7 @@ +import type { Locale } from '@/stores/LocaleStore' import type { Resource, ResourceList } from '../Resource' -import { SpatieQuery } from '@/api/SpatieQuery' import { http } from '@/api/http' -import type { Locale } from '@/stores/LocaleStore' +import { SpatieQuery } from '@/api/SpatieQuery' export interface Expertise { id: string // ulid diff --git a/routes/api.php b/routes/api.php index b04e2abf..846a4a02 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,5 +1,6 @@ get('/api/announcements'); + $response->assertStatus(204); + + Announcement::factory()->create([ + 'title_en' => 'Test Title', + 'title_fr' => 'Test Title', + 'text_en' => 'Test Text', + 'text_fr' => 'Test Text', + 'start_at' => now()->subDay(), + 'end_at' => now()->addDay(), + ]); + + // make anohther innactive announcement + Announcement::factory()->create([ + 'title_en' => 'Old Test Title', + 'title_fr' => 'Test Title', + 'text_en' => 'Test Text', + 'text_fr' => 'Test Text', + 'start_at' => now()->subDays(2), + 'end_at' => now()->subDay(), + ]); + + $response = $this->get('/api/announcements'); + $response->assertStatus(200); + + expect($response->json('data.0.data.title_en'))->toBe('Test Title'); + $response->assertJsonCount(1, 'data'); + +});