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}