Skip to main content

shared/models/
announcement.rs

1use crate::{
2    models::{InsertQueryBuilder, UpdateQueryBuilder},
3    prelude::*,
4};
5use garde::Validate;
6use serde::{Deserialize, Serialize};
7use sqlx::{Row, postgres::PgRow, prelude::Type};
8use std::{
9    collections::BTreeMap,
10    hash::Hash,
11    sync::{Arc, LazyLock},
12};
13use utoipa::ToSchema;
14
15#[derive(ToSchema, Serialize, Deserialize, Type, PartialEq, Eq, Hash, Clone, Copy)]
16#[serde(rename_all = "snake_case")]
17#[sqlx(type_name = "announcement_type", rename_all = "SCREAMING_SNAKE_CASE")]
18pub enum AnnouncementType {
19    Info,
20    Success,
21    Warning,
22    Error,
23}
24
25pub fn validate_title_translations(
26    title_translations: &BTreeMap<compact_str::CompactString, compact_str::CompactString>,
27    _context: &(),
28) -> Result<(), garde::Error> {
29    if title_translations.len() > 512 {
30        return Err(garde::Error::new("cannot have more than 512 entries"));
31    }
32
33    for (lang, translation) in title_translations {
34        if lang.len() < 2 || lang.len() > 15 {
35            return Err(garde::Error::new(format!(
36                "language code '{}' must be between 2 and 15 characters",
37                lang
38            )));
39        }
40        if translation.is_empty() || translation.len() > 255 {
41            return Err(garde::Error::new(format!(
42                "translation for language '{}' must be between 1 and 255 characters",
43                lang
44            )));
45        }
46    }
47
48    Ok(())
49}
50
51pub fn validate_content_translations(
52    content_translations: &BTreeMap<compact_str::CompactString, compact_str::CompactString>,
53    _context: &(),
54) -> Result<(), garde::Error> {
55    if content_translations.len() > 512 {
56        return Err(garde::Error::new("cannot have more than 512 entries"));
57    }
58
59    for (lang, translation) in content_translations {
60        if lang.len() < 2 || lang.len() > 15 {
61            return Err(garde::Error::new(format!(
62                "language code '{}' must be between 2 and 15 characters",
63                lang
64            )));
65        }
66        if translation.is_empty() || translation.len() > 2048 {
67            return Err(garde::Error::new(format!(
68                "translation for language '{}' must be between 1 and 2048 characters",
69                lang
70            )));
71        }
72    }
73
74    Ok(())
75}
76
77#[derive(Serialize, Deserialize, Clone)]
78pub struct Announcement {
79    pub uuid: uuid::Uuid,
80
81    pub r#type: AnnouncementType,
82    pub enabled: bool,
83    pub enabled_start: Option<chrono::NaiveDateTime>,
84    pub enabled_end: Option<chrono::NaiveDateTime>,
85    pub dismissible: bool,
86    pub dismissible_end: Option<chrono::NaiveDateTime>,
87
88    pub title: compact_str::CompactString,
89    pub title_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
90    pub content: compact_str::CompactString,
91    pub content_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
92
93    pub locations: Vec<uuid::Uuid>,
94    pub nodes: Vec<uuid::Uuid>,
95    pub backup_configurations: Vec<uuid::Uuid>,
96    pub eggs: Vec<uuid::Uuid>,
97
98    pub created: chrono::NaiveDateTime,
99
100    extension_data: super::ModelExtensionData,
101}
102
103impl BaseModel for Announcement {
104    const NAME: &'static str = "announcement";
105
106    fn get_extension_list() -> &'static super::ModelExtensionList {
107        static EXTENSIONS: LazyLock<super::ModelExtensionList> =
108            LazyLock::new(|| std::sync::RwLock::new(Vec::new()));
109
110        &EXTENSIONS
111    }
112
113    fn get_extension_data(&self) -> &super::ModelExtensionData {
114        &self.extension_data
115    }
116
117    #[inline]
118    fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
119        let prefix = prefix.unwrap_or_default();
120
121        BTreeMap::from([
122            (
123                "announcements.uuid",
124                compact_str::format_compact!("{prefix}uuid"),
125            ),
126            (
127                "announcements.type",
128                compact_str::format_compact!("{prefix}type"),
129            ),
130            (
131                "announcements.enabled",
132                compact_str::format_compact!("{prefix}enabled"),
133            ),
134            (
135                "announcements.enabled_start",
136                compact_str::format_compact!("{prefix}enabled_start"),
137            ),
138            (
139                "announcements.enabled_end",
140                compact_str::format_compact!("{prefix}enabled_end"),
141            ),
142            (
143                "announcements.dismissible",
144                compact_str::format_compact!("{prefix}dismissible"),
145            ),
146            (
147                "announcements.dismissible_end",
148                compact_str::format_compact!("{prefix}dismissible_end"),
149            ),
150            (
151                "announcements.title",
152                compact_str::format_compact!("{prefix}title"),
153            ),
154            (
155                "announcements.title_translations",
156                compact_str::format_compact!("{prefix}title_translations"),
157            ),
158            (
159                "announcements.content",
160                compact_str::format_compact!("{prefix}content"),
161            ),
162            (
163                "announcements.content_translations",
164                compact_str::format_compact!("{prefix}content_translations"),
165            ),
166            (
167                "announcements.locations",
168                compact_str::format_compact!("{prefix}locations"),
169            ),
170            (
171                "announcements.nodes",
172                compact_str::format_compact!("{prefix}nodes"),
173            ),
174            (
175                "announcements.backup_configurations",
176                compact_str::format_compact!("{prefix}backup_configurations"),
177            ),
178            (
179                "announcements.eggs",
180                compact_str::format_compact!("{prefix}eggs"),
181            ),
182            (
183                "announcements.created",
184                compact_str::format_compact!("{prefix}created"),
185            ),
186        ])
187    }
188
189    #[inline]
190    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
191        let prefix = prefix.unwrap_or_default();
192
193        Ok(Self {
194            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
195            r#type: row.try_get(compact_str::format_compact!("{prefix}type").as_str())?,
196            enabled: row.try_get(compact_str::format_compact!("{prefix}enabled").as_str())?,
197            enabled_start: row
198                .try_get(compact_str::format_compact!("{prefix}enabled_start").as_str())?,
199            enabled_end: row
200                .try_get(compact_str::format_compact!("{prefix}enabled_end").as_str())?,
201            dismissible: row
202                .try_get(compact_str::format_compact!("{prefix}dismissible").as_str())?,
203            dismissible_end: row
204                .try_get(compact_str::format_compact!("{prefix}dismissible_end").as_str())?,
205            title: row.try_get(compact_str::format_compact!("{prefix}title").as_str())?,
206            title_translations: serde_json::from_value(
207                row.try_get(compact_str::format_compact!("{prefix}title_translations").as_str())?,
208            )?,
209            content: row.try_get(compact_str::format_compact!("{prefix}content").as_str())?,
210            content_translations: serde_json::from_value(
211                row.try_get(compact_str::format_compact!("{prefix}content_translations").as_str())?,
212            )?,
213            locations: row.try_get(compact_str::format_compact!("{prefix}locations").as_str())?,
214            nodes: row.try_get(compact_str::format_compact!("{prefix}nodes").as_str())?,
215            backup_configurations: row
216                .try_get(compact_str::format_compact!("{prefix}backup_configurations").as_str())?,
217            eggs: row.try_get(compact_str::format_compact!("{prefix}eggs").as_str())?,
218            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
219            extension_data: Self::map_extensions(prefix, row)?,
220        })
221    }
222}
223
224impl Announcement {
225    pub async fn all_with_pagination(
226        database: &crate::database::Database,
227        page: i64,
228        per_page: i64,
229        search: Option<&str>,
230    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
231        let offset = (page - 1) * per_page;
232
233        let rows = sqlx::query(&format!(
234            r#"
235            SELECT {}, COUNT(*) OVER() AS total_count
236            FROM announcements
237            WHERE ($1 IS NULL OR announcements.title ILIKE '%' || $1 || '%')
238            ORDER BY announcements.created
239            LIMIT $2 OFFSET $3
240            "#,
241            Self::columns_sql(None)
242        ))
243        .bind(search)
244        .bind(per_page)
245        .bind(offset)
246        .fetch_all(database.read())
247        .await?;
248
249        Ok(super::Pagination {
250            total: rows
251                .first()
252                .map_or(Ok(0), |row| row.try_get("total_count"))?,
253            per_page,
254            page,
255            data: rows
256                .into_iter()
257                .map(|row| Self::map(None, &row))
258                .try_collect_vec()?,
259        })
260    }
261
262    pub async fn all_by_active(
263        database: &crate::database::Database,
264    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
265        let rows = sqlx::query(&format!(
266            r#"
267            SELECT {}
268            FROM announcements
269            WHERE announcements.enabled = true
270                AND (announcements.enabled_start IS NULL OR announcements.enabled_start <= NOW())
271                AND (announcements.enabled_end IS NULL OR announcements.enabled_end >= NOW())
272                AND array_length(announcements.locations, 1) IS NULL
273                AND array_length(announcements.nodes, 1) IS NULL
274                AND array_length(announcements.backup_configurations, 1) IS NULL
275                AND array_length(announcements.eggs, 1) IS NULL
276            ORDER BY announcements.created
277            "#,
278            Self::columns_sql(None)
279        ))
280        .fetch_all(database.read())
281        .await?;
282
283        rows.into_iter()
284            .map(|row| Self::map(None, &row))
285            .try_collect_vec()
286    }
287
288    pub async fn all_by_active_server(
289        database: &crate::database::Database,
290        server: &super::server::Server,
291    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
292        let node = server.node.fetch_cached(database).await?;
293
294        let rows = sqlx::query(&format!(
295            r#"
296            SELECT {}
297            FROM announcements
298            WHERE announcements.enabled = true
299                AND (announcements.enabled_start IS NULL OR announcements.enabled_start <= NOW())
300                AND (announcements.enabled_end IS NULL OR announcements.enabled_end >= NOW())
301                AND (
302                    array_length(announcements.locations, 1) IS NOT NULL
303                    OR array_length(announcements.nodes, 1) IS NOT NULL
304                    OR array_length(announcements.backup_configurations, 1) IS NOT NULL
305                    OR array_length(announcements.eggs, 1) IS NOT NULL
306                )
307                AND (
308                    (announcements.locations IS NULL OR $1 = ANY(announcements.locations))
309                    OR (announcements.nodes IS NULL OR $2 = ANY(announcements.nodes))
310                    OR ($3 IS NOT NULL AND (announcements.backup_configurations IS NULL OR $3 = ANY(announcements.backup_configurations)))
311                    OR (announcements.eggs IS NULL OR $4 = ANY(announcements.eggs))
312                )
313            ORDER BY announcements.created
314            "#,
315            Self::columns_sql(None)
316        ))
317        .bind(node.location.uuid)
318        .bind(node.uuid)
319        .bind(server.backup_configuration.as_ref().map_or_else(
320            || {
321                node.backup_configuration.as_ref().map_or_else(
322                    || node.location.backup_configuration.as_ref().map(|b| b.uuid),
323                    |b| Some(b.uuid),
324                )
325            },
326            |b| Some(b.uuid),
327        ))
328        .bind(server.egg.uuid)
329        .fetch_all(database.read())
330        .await?;
331
332        rows.into_iter()
333            .map(|row| Self::map(None, &row))
334            .try_collect_vec()
335    }
336}
337
338#[async_trait::async_trait]
339impl IntoAdminApiObject for Announcement {
340    type AdminApiObject = AdminApiAnnouncement;
341    type ExtraArgs<'a> = ();
342
343    async fn into_admin_api_object<'a>(
344        mut self,
345        state: &crate::State,
346        _args: Self::ExtraArgs<'a>,
347    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
348        let api_object = AdminApiAnnouncement::init_hooks(&self, state).await?;
349
350        let api_object = finish_extendible!(
351            AdminApiAnnouncement {
352                uuid: self.uuid,
353                r#type: self.r#type,
354                enabled: self.enabled,
355                enabled_start: self.enabled_start.map(|dt| dt.and_utc()),
356                enabled_end: self.enabled_end.map(|dt| dt.and_utc()),
357                dismissible: self.dismissible,
358                dismissible_end: self.dismissible_end.map(|dt| dt.and_utc()),
359                title: self.title,
360                title_translations: self.title_translations,
361                content: self.content,
362                content_translations: self.content_translations,
363                locations: self.locations,
364                nodes: self.nodes,
365                backup_configurations: self.backup_configurations,
366                eggs: self.eggs,
367                created: self.created.and_utc(),
368            },
369            api_object,
370            state
371        )?;
372
373        Ok(api_object)
374    }
375}
376
377#[async_trait::async_trait]
378impl IntoApiObject for Announcement {
379    type ApiObject = ApiAnnouncement;
380    type ExtraArgs<'a> = ();
381
382    async fn into_api_object<'a>(
383        self,
384        state: &crate::State,
385        _args: Self::ExtraArgs<'a>,
386    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
387        let api_object = ApiAnnouncement::init_hooks(&self, state).await?;
388
389        let api_object = finish_extendible!(
390            ApiAnnouncement {
391                uuid: self.uuid,
392                r#type: self.r#type,
393                dismissible: self.dismissible,
394                dismissible_end: self.dismissible_end.map(|dt| dt.and_utc()),
395                title: self.title,
396                title_translations: self.title_translations,
397                content: self.content,
398                content_translations: self.content_translations,
399            },
400            api_object,
401            state
402        )?;
403
404        Ok(api_object)
405    }
406}
407
408#[async_trait::async_trait]
409impl ByUuid for Announcement {
410    async fn by_uuid(
411        database: &crate::database::Database,
412        uuid: uuid::Uuid,
413    ) -> Result<Self, crate::database::DatabaseError> {
414        let row = sqlx::query(&format!(
415            r#"
416            SELECT {}
417            FROM announcements
418            WHERE announcements.uuid = $1
419            "#,
420            Self::columns_sql(None)
421        ))
422        .bind(uuid)
423        .fetch_one(database.read())
424        .await?;
425
426        Self::map(None, &row)
427    }
428
429    async fn by_uuid_with_transaction(
430        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
431        uuid: uuid::Uuid,
432    ) -> Result<Self, crate::database::DatabaseError> {
433        let row = sqlx::query(&format!(
434            r#"
435            SELECT {}
436            FROM announcements
437            WHERE announcements.uuid = $1
438            "#,
439            Self::columns_sql(None)
440        ))
441        .bind(uuid)
442        .fetch_one(&mut **transaction)
443        .await?;
444
445        Self::map(None, &row)
446    }
447}
448
449#[derive(ToSchema, Deserialize, Validate)]
450pub struct CreateAnnouncementOptions {
451    #[garde(skip)]
452    pub r#type: AnnouncementType,
453
454    #[garde(skip)]
455    pub enabled: bool,
456    #[garde(skip)]
457    pub enabled_start: Option<chrono::DateTime<chrono::Utc>>,
458    #[garde(skip)]
459    pub enabled_end: Option<chrono::DateTime<chrono::Utc>>,
460    #[garde(skip)]
461    pub dismissible: bool,
462    #[garde(skip)]
463    pub dismissible_end: Option<chrono::DateTime<chrono::Utc>>,
464
465    #[garde(length(chars, min = 1, max = 255))]
466    #[schema(min_length = 1, max_length = 255)]
467    pub title: compact_str::CompactString,
468    #[garde(custom(validate_title_translations))]
469    pub title_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
470    #[garde(length(chars, min = 1, max = 2048))]
471    #[schema(min_length = 1, max_length = 2048)]
472    pub content: compact_str::CompactString,
473    #[garde(custom(validate_content_translations))]
474    pub content_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
475
476    #[garde(length(max = 100))]
477    #[schema(max_length = 100)]
478    pub locations: Vec<uuid::Uuid>,
479    #[garde(length(max = 100))]
480    #[schema(max_length = 100)]
481    pub nodes: Vec<uuid::Uuid>,
482    #[garde(length(max = 100))]
483    #[schema(max_length = 100)]
484    pub backup_configurations: Vec<uuid::Uuid>,
485    #[garde(length(max = 100))]
486    #[schema(max_length = 100)]
487    pub eggs: Vec<uuid::Uuid>,
488}
489
490#[async_trait::async_trait]
491impl CreatableModel for Announcement {
492    type CreateOptions<'a> = CreateAnnouncementOptions;
493    type CreateResult = Self;
494
495    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
496        static CREATE_LISTENERS: LazyLock<CreateListenerList<Announcement>> =
497            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
498
499        &CREATE_LISTENERS
500    }
501
502    async fn create_with_transaction(
503        state: &crate::State,
504        mut options: Self::CreateOptions<'_>,
505        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
506    ) -> Result<Self, crate::database::DatabaseError> {
507        options.validate()?;
508
509        let mut query_builder = InsertQueryBuilder::new("announcements");
510
511        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
512
513        query_builder
514            .set("type", options.r#type)
515            .set("enabled", options.enabled)
516            .set("enabled_start", options.enabled_start)
517            .set("enabled_end", options.enabled_end)
518            .set("dismissible", options.dismissible)
519            .set("dismissible_end", options.dismissible_end)
520            .set("title", &options.title)
521            .set(
522                "title_translations",
523                serde_json::to_value(&options.title_translations)?,
524            )
525            .set("content", &options.content)
526            .set(
527                "content_translations",
528                serde_json::to_value(&options.content_translations)?,
529            )
530            .set("locations", &options.locations)
531            .set("nodes", &options.nodes)
532            .set("backup_configurations", &options.backup_configurations)
533            .set("eggs", &options.eggs);
534
535        let row = query_builder
536            .returning(&Self::columns_sql(None))
537            .fetch_one(&mut **transaction)
538            .await?;
539        let mut announcement = Self::map(None, &row)?;
540
541        Self::run_after_create_handlers(&mut announcement, &options, state, transaction).await?;
542
543        Ok(announcement)
544    }
545}
546
547#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
548pub struct UpdateAnnouncementOptions {
549    #[garde(skip)]
550    pub r#type: Option<AnnouncementType>,
551
552    #[garde(skip)]
553    pub enabled: Option<bool>,
554    #[garde(skip)]
555    #[serde(default, with = "::serde_with::rust::double_option")]
556    pub enabled_start: Option<Option<chrono::DateTime<chrono::Utc>>>,
557    #[garde(skip)]
558    #[serde(default, with = "::serde_with::rust::double_option")]
559    pub enabled_end: Option<Option<chrono::DateTime<chrono::Utc>>>,
560    #[garde(skip)]
561    pub dismissible: Option<bool>,
562    #[garde(skip)]
563    #[serde(default, with = "::serde_with::rust::double_option")]
564    pub dismissible_end: Option<Option<chrono::DateTime<chrono::Utc>>>,
565
566    #[garde(length(chars, min = 1, max = 255))]
567    #[schema(min_length = 1, max_length = 255)]
568    pub title: Option<compact_str::CompactString>,
569    #[garde(inner(custom(validate_title_translations)))]
570    pub title_translations:
571        Option<BTreeMap<compact_str::CompactString, compact_str::CompactString>>,
572    #[garde(length(chars, min = 1, max = 2048))]
573    #[schema(min_length = 1, max_length = 2048)]
574    pub content: Option<compact_str::CompactString>,
575    #[garde(inner(custom(validate_content_translations)))]
576    pub content_translations:
577        Option<BTreeMap<compact_str::CompactString, compact_str::CompactString>>,
578
579    #[garde(length(max = 100))]
580    #[schema(max_length = 100)]
581    pub locations: Option<Vec<uuid::Uuid>>,
582    #[garde(length(max = 100))]
583    #[schema(max_length = 100)]
584    pub nodes: Option<Vec<uuid::Uuid>>,
585    #[garde(length(max = 100))]
586    #[schema(max_length = 100)]
587    pub backup_configurations: Option<Vec<uuid::Uuid>>,
588    #[garde(length(max = 100))]
589    #[schema(max_length = 100)]
590    pub eggs: Option<Vec<uuid::Uuid>>,
591}
592
593#[async_trait::async_trait]
594impl UpdatableModel for Announcement {
595    type UpdateOptions = UpdateAnnouncementOptions;
596
597    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
598        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<Announcement>> =
599            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
600
601        &UPDATE_LISTENERS
602    }
603
604    async fn update_with_transaction(
605        &mut self,
606        state: &crate::State,
607        mut options: Self::UpdateOptions,
608        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
609    ) -> Result<(), crate::database::DatabaseError> {
610        options.validate()?;
611
612        let mut query_builder = UpdateQueryBuilder::new("announcements");
613
614        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
615            .await?;
616
617        query_builder
618            .set("type", options.r#type)
619            .set("enabled", options.enabled)
620            .set(
621                "enabled_start",
622                options
623                    .enabled_start
624                    .as_ref()
625                    .map(|dt| dt.as_ref().map(|dt| dt.naive_utc())),
626            )
627            .set(
628                "enabled_end",
629                options
630                    .enabled_end
631                    .as_ref()
632                    .map(|dt| dt.as_ref().map(|dt| dt.naive_utc())),
633            )
634            .set("dismissible", options.dismissible)
635            .set(
636                "dismissible_end",
637                options
638                    .dismissible_end
639                    .as_ref()
640                    .map(|dt| dt.as_ref().map(|dt| dt.naive_utc())),
641            )
642            .set("title", options.title.as_ref())
643            .set(
644                "title_translations",
645                options
646                    .title_translations
647                    .as_ref()
648                    .map(serde_json::to_value)
649                    .transpose()?,
650            )
651            .set("content", options.content.as_ref())
652            .set(
653                "content_translations",
654                options
655                    .content_translations
656                    .as_ref()
657                    .map(serde_json::to_value)
658                    .transpose()?,
659            )
660            .set("locations", options.locations.as_ref())
661            .set("nodes", options.nodes.as_ref())
662            .set(
663                "backup_configurations",
664                options.backup_configurations.as_ref(),
665            )
666            .set("eggs", options.eggs.as_ref())
667            .where_eq("uuid", self.uuid);
668
669        query_builder.execute(&mut **transaction).await?;
670
671        if let Some(r#type) = options.r#type {
672            self.r#type = r#type;
673        }
674        if let Some(enabled) = options.enabled {
675            self.enabled = enabled;
676        }
677        if let Some(enabled_start) = options.enabled_start {
678            self.enabled_start = enabled_start.map(|dt| dt.naive_utc());
679        }
680        if let Some(enabled_end) = options.enabled_end {
681            self.enabled_end = enabled_end.map(|dt| dt.naive_utc());
682        }
683        if let Some(dismissible) = options.dismissible {
684            self.dismissible = dismissible;
685        }
686        if let Some(dismissible_end) = options.dismissible_end {
687            self.dismissible_end = dismissible_end.map(|dt| dt.naive_utc());
688        }
689        if let Some(title) = options.title {
690            self.title = title;
691        }
692        if let Some(title_translations) = options.title_translations {
693            self.title_translations = title_translations;
694        }
695        if let Some(content) = options.content {
696            self.content = content;
697        }
698        if let Some(content_translations) = options.content_translations {
699            self.content_translations = content_translations;
700        }
701        if let Some(locations) = options.locations {
702            self.locations = locations;
703        }
704        if let Some(nodes) = options.nodes {
705            self.nodes = nodes;
706        }
707        if let Some(backup_configurations) = options.backup_configurations {
708            self.backup_configurations = backup_configurations;
709        }
710        if let Some(eggs) = options.eggs {
711            self.eggs = eggs;
712        }
713
714        self.run_after_update_handlers(state, transaction).await?;
715
716        Ok(())
717    }
718}
719
720#[async_trait::async_trait]
721impl DeletableModel for Announcement {
722    type DeleteOptions = ();
723
724    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
725        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<Announcement>> =
726            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
727
728        &DELETE_LISTENERS
729    }
730
731    async fn delete_with_transaction(
732        &self,
733        state: &crate::State,
734        options: Self::DeleteOptions,
735        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
736    ) -> Result<(), anyhow::Error> {
737        self.run_delete_handlers(&options, state, transaction)
738            .await?;
739
740        sqlx::query(
741            r#"
742            DELETE FROM announcements
743            WHERE announcements.uuid = $1
744            "#,
745        )
746        .bind(self.uuid)
747        .execute(&mut **transaction)
748        .await?;
749
750        self.run_after_delete_handlers(&options, state, transaction)
751            .await?;
752
753        Ok(())
754    }
755}
756
757#[schema_extension_derive::extendible]
758#[init_args(Announcement, crate::State)]
759#[hook_args(crate::State)]
760#[derive(ToSchema, Serialize)]
761#[schema(title = "AdminAnnouncement")]
762pub struct AdminApiAnnouncement {
763    pub uuid: uuid::Uuid,
764
765    pub r#type: AnnouncementType,
766    pub enabled: bool,
767    pub enabled_start: Option<chrono::DateTime<chrono::Utc>>,
768    pub enabled_end: Option<chrono::DateTime<chrono::Utc>>,
769    pub dismissible: bool,
770    pub dismissible_end: Option<chrono::DateTime<chrono::Utc>>,
771
772    pub title: compact_str::CompactString,
773    pub title_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
774    pub content: compact_str::CompactString,
775    pub content_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
776
777    pub locations: Vec<uuid::Uuid>,
778    pub nodes: Vec<uuid::Uuid>,
779    pub backup_configurations: Vec<uuid::Uuid>,
780    pub eggs: Vec<uuid::Uuid>,
781
782    pub created: chrono::DateTime<chrono::Utc>,
783}
784
785#[schema_extension_derive::extendible]
786#[init_args(Announcement, crate::State)]
787#[hook_args(crate::State)]
788#[derive(ToSchema, Serialize)]
789#[schema(title = "ApiAnnouncement")]
790pub struct ApiAnnouncement {
791    pub uuid: uuid::Uuid,
792
793    pub r#type: AnnouncementType,
794    pub dismissible: bool,
795    pub dismissible_end: Option<chrono::DateTime<chrono::Utc>>,
796
797    pub title: compact_str::CompactString,
798    pub title_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
799    pub content: compact_str::CompactString,
800    pub content_translations: BTreeMap<compact_str::CompactString, compact_str::CompactString>,
801}