1use crate::error::{OptimError, Result};
7use chrono::{DateTime, Datelike, TimeZone, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ConferenceManager {
14 pub conferences: HashMap<String, Conference>,
16 pub submissions: Vec<Submission>,
18 pub alerts: Vec<DeadlineAlert>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct Conference {
25 pub id: String,
27 pub name: String,
29 pub abbreviation: String,
31 pub description: String,
33 pub url: String,
35 pub ranking: ConferenceRanking,
37 pub research_areas: Vec<String>,
39 pub annual: bool,
41 pub series_info: SeriesInfo,
43 pub dates: ConferenceDates,
45 pub requirements: SubmissionRequirements,
47 pub review_process: ReviewProcess,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53pub enum ConferenceRanking {
54 TopTier,
56 HighQuality,
58 Good,
60 Acceptable,
62 Emerging,
64 Workshop,
66 Unranked,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct SeriesInfo {
73 pub series_number: u32,
75 pub year: u32,
77 pub location: String,
79 pub country: String,
81 pub format: ConferenceFormat,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
87pub enum ConferenceFormat {
88 InPerson,
90 Virtual,
92 Hybrid,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct ConferenceDates {
99 pub abstract_deadline: Option<DateTime<Utc>>,
101 pub paper_deadline: DateTime<Utc>,
103 pub notification_date: DateTime<Utc>,
105 pub camera_ready_deadline: DateTime<Utc>,
107 pub conference_start: DateTime<Utc>,
109 pub conference_end: DateTime<Utc>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct SubmissionRequirements {
116 pub page_limit: u32,
118 pub word_limit: Option<u32>,
120 pub format: FormatRequirements,
122 pub required_sections: Vec<String>,
124 pub supplementary_allowed: bool,
126 pub anonymous_submission: bool,
128 pub double_blind: bool,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct FormatRequirements {
135 pub template: String,
137 pub font_size: u32,
139 pub line_spacing: f64,
141 pub margins: String,
143 pub citation_style: String,
145 pub file_format: Vec<String>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ReviewProcess {
152 pub reviewers_per_paper: u32,
154 pub review_criteria: Vec<String>,
156 pub rebuttal_allowed: bool,
158 pub acceptance_rate: Option<f64>,
160 pub review_format: ReviewFormat,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
166pub enum ReviewFormat {
167 NumericalScores,
169 WrittenOnly,
171 Mixed,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct Submission {
178 pub id: String,
180 pub conference_id: String,
182 pub paper_id: String,
184 pub status: SubmissionStatus,
186 pub submitted_at: DateTime<Utc>,
188 pub track: Option<String>,
190 pub materials: SubmissionMaterials,
192 pub reviews: Vec<Review>,
194 pub decision: Option<Decision>,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
200pub enum SubmissionStatus {
201 Draft,
203 Submitted,
205 UnderReview,
207 Rebuttal,
209 Decided,
211 CameraReady,
213 Withdrawn,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct SubmissionMaterials {
220 pub paper_file: String,
222 pub supplementary_files: Vec<String>,
224 pub abstracttext: String,
226 pub keywords: Vec<String>,
228 pub authors: Option<Vec<String>>,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct Review {
235 pub id: String,
237 pub reviewer: String,
239 pub overall_score: Option<f64>,
241 pub detailed_scores: HashMap<String, f64>,
243 pub reviewtext: String,
245 pub recommendation: ReviewRecommendation,
247 pub reviewed_at: DateTime<Utc>,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
253pub enum ReviewRecommendation {
254 StrongAccept,
256 Accept,
258 WeakAccept,
260 Borderline,
262 WeakReject,
264 Reject,
266 StrongReject,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct Decision {
273 pub outcome: DecisionOutcome,
275 pub decided_at: DateTime<Utc>,
277 pub editor_comments: Option<String>,
279 pub required_revisions: Vec<String>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
285pub enum DecisionOutcome {
286 Accept,
288 AcceptMinorRevisions,
290 AcceptMajorRevisions,
292 ConditionalAccept,
294 Reject,
296 RejectAndResubmit,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct DeadlineAlert {
303 pub id: String,
305 pub conference_id: String,
307 pub deadline_type: DeadlineType,
309 pub alert_date: DateTime<Utc>,
311 pub days_before: u32,
313 pub message: String,
315 pub sent: bool,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
321pub enum DeadlineType {
322 AbstractSubmission,
324 PaperSubmission,
326 Notification,
328 CameraReady,
330 ConferenceStart,
332}
333
334impl Default for ConferenceManager {
335 fn default() -> Self {
336 Self::new()
337 }
338}
339
340impl ConferenceManager {
341 pub fn new() -> Self {
343 Self {
344 conferences: HashMap::new(),
345 submissions: Vec::new(),
346 alerts: Vec::new(),
347 }
348 }
349
350 pub fn add_conference(&mut self, conference: Conference) {
352 self.conferences.insert(conference.id.clone(), conference);
353 }
354
355 pub fn submit_paper(
357 &mut self,
358 conference_id: &str,
359 paper_id: &str,
360 materials: SubmissionMaterials,
361 ) -> Result<String> {
362 if !self.conferences.contains_key(conference_id) {
363 return Err(OptimError::InvalidConfig(format!(
364 "Conference '{}' not found",
365 conference_id
366 )));
367 }
368
369 let submission_id = uuid::Uuid::new_v4().to_string();
370 let submission = Submission {
371 id: submission_id.clone(),
372 conference_id: conference_id.to_string(),
373 paper_id: paper_id.to_string(),
374 status: SubmissionStatus::Submitted,
375 submitted_at: Utc::now(),
376 track: None,
377 materials,
378 reviews: Vec::new(),
379 decision: None,
380 };
381
382 self.submissions.push(submission);
383 Ok(submission_id)
384 }
385
386 pub fn get_upcoming_deadlines(
388 &self,
389 days_ahead: u32,
390 ) -> Vec<(&Conference, DeadlineType, DateTime<Utc>)> {
391 let mut deadlines = Vec::new();
392 let now = Utc::now();
393 let future_limit = now + chrono::Duration::days(days_ahead as i64);
394
395 for conference in self.conferences.values() {
396 let dates = &conference.dates;
397
398 if let Some(abstract_deadline) = dates.abstract_deadline {
399 if abstract_deadline > now && abstract_deadline <= future_limit {
400 deadlines.push((
401 conference,
402 DeadlineType::AbstractSubmission,
403 abstract_deadline,
404 ));
405 }
406 }
407
408 if dates.paper_deadline > now && dates.paper_deadline <= future_limit {
409 deadlines.push((
410 conference,
411 DeadlineType::PaperSubmission,
412 dates.paper_deadline,
413 ));
414 }
415
416 if dates.notification_date > now && dates.notification_date <= future_limit {
417 deadlines.push((
418 conference,
419 DeadlineType::Notification,
420 dates.notification_date,
421 ));
422 }
423
424 if dates.camera_ready_deadline > now && dates.camera_ready_deadline <= future_limit {
425 deadlines.push((
426 conference,
427 DeadlineType::CameraReady,
428 dates.camera_ready_deadline,
429 ));
430 }
431
432 if dates.conference_start > now && dates.conference_start <= future_limit {
433 deadlines.push((
434 conference,
435 DeadlineType::ConferenceStart,
436 dates.conference_start,
437 ));
438 }
439 }
440
441 deadlines.sort_by_key(|a| a.2);
443 deadlines
444 }
445
446 pub fn generate_deadline_alerts(&mut self, days_before: &[u32]) -> Vec<DeadlineAlert> {
454 let now = Utc::now();
455
456 let mut deadline_entries: Vec<(String, DeadlineType, DateTime<Utc>)> = Vec::new();
459 for conference in self.conferences.values() {
460 let dates = &conference.dates;
461 if let Some(abstract_deadline) = dates.abstract_deadline {
462 deadline_entries.push((
463 conference.id.clone(),
464 DeadlineType::AbstractSubmission,
465 abstract_deadline,
466 ));
467 }
468 deadline_entries.push((
469 conference.id.clone(),
470 DeadlineType::PaperSubmission,
471 dates.paper_deadline,
472 ));
473 deadline_entries.push((
474 conference.id.clone(),
475 DeadlineType::Notification,
476 dates.notification_date,
477 ));
478 deadline_entries.push((
479 conference.id.clone(),
480 DeadlineType::CameraReady,
481 dates.camera_ready_deadline,
482 ));
483 deadline_entries.push((
484 conference.id.clone(),
485 DeadlineType::ConferenceStart,
486 dates.conference_start,
487 ));
488 }
489
490 let mut new_alerts = Vec::new();
491 for (conference_id, deadline_type, deadline) in deadline_entries {
492 if deadline <= now {
493 continue;
494 }
495 let days_remaining = (deadline - now).num_days().max(0) as u32;
496
497 for &threshold in days_before {
498 if days_remaining > threshold {
499 continue;
500 }
501
502 let already_exists = self.alerts.iter().any(|alert| {
503 alert.conference_id == conference_id
504 && alert.deadline_type == deadline_type
505 && alert.days_before == threshold
506 });
507 if already_exists {
508 continue;
509 }
510
511 let alert = DeadlineAlert {
512 id: uuid::Uuid::new_v4().to_string(),
513 conference_id: conference_id.clone(),
514 deadline_type: deadline_type.clone(),
515 alert_date: now,
516 days_before: threshold,
517 message: format!(
518 "{deadline_type:?} deadline for conference '{conference_id}' is in \
519 {days_remaining} day(s) ({deadline})"
520 ),
521 sent: false,
522 };
523 self.alerts.push(alert.clone());
524 new_alerts.push(alert);
525 }
526 }
527
528 new_alerts
529 }
530
531 pub fn pending_alerts(&self) -> Vec<&DeadlineAlert> {
533 self.alerts.iter().filter(|alert| !alert.sent).collect()
534 }
535
536 pub fn mark_alert_sent(&mut self, alert_id: &str) -> Result<()> {
538 let alert = self
539 .alerts
540 .iter_mut()
541 .find(|alert| alert.id == alert_id)
542 .ok_or_else(|| OptimError::InvalidConfig(format!("alert '{alert_id}' not found")))?;
543 alert.sent = true;
544 Ok(())
545 }
546
547 pub fn search_conferences(&self, research_area: &str) -> Vec<&Conference> {
549 self.conferences
550 .values()
551 .filter(|conf| {
552 conf.research_areas
553 .iter()
554 .any(|area| area.to_lowercase().contains(&research_area.to_lowercase()))
555 })
556 .collect()
557 }
558
559 pub fn get_conferences_by_ranking(&self, ranking: ConferenceRanking) -> Vec<&Conference> {
561 self.conferences
562 .values()
563 .filter(|conf| conf.ranking == ranking)
564 .collect()
565 }
566
567 pub fn load_standard_conferences(&mut self) {
572 self.add_conference(Self::next_upcoming_edition(Self::create_neurips_conference));
573 self.add_conference(Self::next_upcoming_edition(Self::create_icml_conference));
574 self.add_conference(Self::next_upcoming_edition(Self::create_iclr_conference));
575 self.add_conference(Self::next_upcoming_edition(Self::create_aaai_conference));
576 self.add_conference(Self::next_upcoming_edition(Self::create_ijcai_conference));
577 }
578
579 fn next_upcoming_edition(build: fn(i32) -> Conference) -> Conference {
589 let now = Utc::now();
590 let start_year = now.year();
591 let last_candidate = start_year + 3;
592 for candidate_year in start_year..=last_candidate {
593 let conference = build(candidate_year);
594 if conference.dates.paper_deadline > now {
595 return conference;
596 }
597 }
598 build(last_candidate)
603 }
604
605 fn create_neurips_conference(edition_year: i32) -> Conference {
606 Conference {
607 id: format!("neurips{edition_year}"),
608 name: "Conference on Neural Information Processing Systems".to_string(),
609 abbreviation: "NeurIPS".to_string(),
610 description: "Premier conference on neural information processing systems".to_string(),
611 url: "https://neurips.cc/".to_string(),
612 ranking: ConferenceRanking::TopTier,
613 research_areas: vec![
614 "Machine Learning".to_string(),
615 "Deep Learning".to_string(),
616 "Neural Networks".to_string(),
617 "Optimization".to_string(),
618 ],
619 annual: true,
620 series_info: SeriesInfo {
621 series_number: 38,
622 year: edition_year as u32,
623 location: "Vancouver".to_string(),
624 country: "Canada".to_string(),
625 format: ConferenceFormat::Hybrid,
626 },
627 dates: ConferenceDates {
628 abstract_deadline: Some(
629 chrono::Utc
630 .with_ymd_and_hms(edition_year, 5, 15, 23, 59, 59)
631 .single()
632 .expect("invalid datetime"),
633 ),
634 paper_deadline: chrono::Utc
635 .with_ymd_and_hms(edition_year, 5, 22, 23, 59, 59)
636 .single()
637 .expect("invalid datetime"),
638 notification_date: chrono::Utc
639 .with_ymd_and_hms(edition_year, 9, 25, 12, 0, 0)
640 .single()
641 .expect("invalid datetime"),
642 camera_ready_deadline: chrono::Utc
643 .with_ymd_and_hms(edition_year, 10, 30, 23, 59, 59)
644 .single()
645 .expect("invalid datetime"),
646 conference_start: chrono::Utc
647 .with_ymd_and_hms(edition_year, 12, 10, 9, 0, 0)
648 .single()
649 .expect("invalid datetime"),
650 conference_end: chrono::Utc
651 .with_ymd_and_hms(edition_year, 12, 16, 18, 0, 0)
652 .single()
653 .expect("invalid datetime"),
654 },
655 requirements: SubmissionRequirements {
656 page_limit: 9,
657 word_limit: None,
658 format: FormatRequirements {
659 template: format!("NeurIPS {edition_year} LaTeX template"),
660 font_size: 10,
661 line_spacing: 1.0,
662 margins: "1 inch".to_string(),
663 citation_style: "NeurIPS".to_string(),
664 file_format: vec!["PDF".to_string()],
665 },
666 required_sections: vec![
667 "Abstract".to_string(),
668 "Introduction".to_string(),
669 "Related Work".to_string(),
670 "Method".to_string(),
671 "Experiments".to_string(),
672 "Conclusion".to_string(),
673 ],
674 supplementary_allowed: true,
675 anonymous_submission: true,
676 double_blind: true,
677 },
678 review_process: ReviewProcess {
679 reviewers_per_paper: 3,
680 review_criteria: vec![
681 "Technical Quality".to_string(),
682 "Novelty".to_string(),
683 "Significance".to_string(),
684 "Clarity".to_string(),
685 ],
686 rebuttal_allowed: true,
687 acceptance_rate: Some(0.26), review_format: ReviewFormat::Mixed,
689 },
690 }
691 }
692
693 fn create_icml_conference(edition_year: i32) -> Conference {
694 Conference {
695 id: format!("icml{edition_year}"),
696 name: "International Conference on Machine Learning".to_string(),
697 abbreviation: "ICML".to_string(),
698 description: "Premier international conference on machine learning".to_string(),
699 url: "https://icml.cc/".to_string(),
700 ranking: ConferenceRanking::TopTier,
701 research_areas: vec![
702 "Machine Learning".to_string(),
703 "Optimization".to_string(),
704 "Statistical Learning".to_string(),
705 "Deep Learning".to_string(),
706 ],
707 annual: true,
708 series_info: SeriesInfo {
709 series_number: 41,
710 year: edition_year as u32,
711 location: "Vienna".to_string(),
712 country: "Austria".to_string(),
713 format: ConferenceFormat::Hybrid,
714 },
715 dates: ConferenceDates {
716 abstract_deadline: None,
717 paper_deadline: chrono::Utc
718 .with_ymd_and_hms(edition_year, 2, 1, 23, 59, 59)
719 .single()
720 .expect("invalid datetime"),
721 notification_date: chrono::Utc
722 .with_ymd_and_hms(edition_year, 5, 1, 12, 0, 0)
723 .single()
724 .expect("invalid datetime"),
725 camera_ready_deadline: chrono::Utc
726 .with_ymd_and_hms(edition_year, 6, 1, 23, 59, 59)
727 .single()
728 .expect("invalid datetime"),
729 conference_start: chrono::Utc
730 .with_ymd_and_hms(edition_year, 7, 21, 9, 0, 0)
731 .single()
732 .expect("invalid datetime"),
733 conference_end: chrono::Utc
734 .with_ymd_and_hms(edition_year, 7, 27, 18, 0, 0)
735 .single()
736 .expect("invalid datetime"),
737 },
738 requirements: SubmissionRequirements {
739 page_limit: 8,
740 word_limit: None,
741 format: FormatRequirements {
742 template: format!("ICML {edition_year} LaTeX template"),
743 font_size: 10,
744 line_spacing: 1.0,
745 margins: "1 inch".to_string(),
746 citation_style: "ICML".to_string(),
747 file_format: vec!["PDF".to_string()],
748 },
749 required_sections: vec![
750 "Abstract".to_string(),
751 "Introduction".to_string(),
752 "Methods".to_string(),
753 "Results".to_string(),
754 "Conclusion".to_string(),
755 ],
756 supplementary_allowed: true,
757 anonymous_submission: true,
758 double_blind: true,
759 },
760 review_process: ReviewProcess {
761 reviewers_per_paper: 3,
762 review_criteria: vec![
763 "Technical Quality".to_string(),
764 "Clarity".to_string(),
765 "Originality".to_string(),
766 "Significance".to_string(),
767 ],
768 rebuttal_allowed: true,
769 acceptance_rate: Some(0.23), review_format: ReviewFormat::Mixed,
771 },
772 }
773 }
774
775 fn create_iclr_conference(edition_year: i32) -> Conference {
776 let prior_year = edition_year - 1;
777 Conference {
778 id: format!("iclr{edition_year}"),
779 name: "International Conference on Learning Representations".to_string(),
780 abbreviation: "ICLR".to_string(),
781 description: "Conference focused on learning representations".to_string(),
782 url: "https://iclr.cc/".to_string(),
783 ranking: ConferenceRanking::TopTier,
784 research_areas: vec![
785 "Deep Learning".to_string(),
786 "Representation Learning".to_string(),
787 "Neural Networks".to_string(),
788 "Optimization".to_string(),
789 ],
790 annual: true,
791 series_info: SeriesInfo {
792 series_number: 12,
793 year: edition_year as u32,
794 location: "Vienna".to_string(),
795 country: "Austria".to_string(),
796 format: ConferenceFormat::Hybrid,
797 },
798 dates: ConferenceDates {
799 abstract_deadline: Some(
800 chrono::Utc
801 .with_ymd_and_hms(prior_year, 9, 28, 23, 59, 59)
802 .single()
803 .expect("invalid datetime"),
804 ),
805 paper_deadline: chrono::Utc
806 .with_ymd_and_hms(prior_year, 10, 2, 23, 59, 59)
807 .single()
808 .expect("invalid datetime"),
809 notification_date: chrono::Utc
810 .with_ymd_and_hms(edition_year, 1, 15, 12, 0, 0)
811 .single()
812 .expect("invalid datetime"),
813 camera_ready_deadline: chrono::Utc
817 .with_ymd_and_hms(edition_year, 2, 28, 23, 59, 59)
818 .single()
819 .expect("invalid datetime"),
820 conference_start: chrono::Utc
821 .with_ymd_and_hms(edition_year, 5, 7, 9, 0, 0)
822 .single()
823 .expect("invalid datetime"),
824 conference_end: chrono::Utc
825 .with_ymd_and_hms(edition_year, 5, 11, 18, 0, 0)
826 .single()
827 .expect("invalid datetime"),
828 },
829 requirements: SubmissionRequirements {
830 page_limit: 9,
831 word_limit: None,
832 format: FormatRequirements {
833 template: format!("ICLR {edition_year} LaTeX template"),
834 font_size: 10,
835 line_spacing: 1.0,
836 margins: "1 inch".to_string(),
837 citation_style: "ICLR".to_string(),
838 file_format: vec!["PDF".to_string()],
839 },
840 required_sections: vec![
841 "Abstract".to_string(),
842 "Introduction".to_string(),
843 "Related Work".to_string(),
844 "Method".to_string(),
845 "Experiments".to_string(),
846 "Conclusion".to_string(),
847 ],
848 supplementary_allowed: true,
849 anonymous_submission: true,
850 double_blind: true,
851 },
852 review_process: ReviewProcess {
853 reviewers_per_paper: 3,
854 review_criteria: vec![
855 "Technical Quality".to_string(),
856 "Clarity".to_string(),
857 "Originality".to_string(),
858 "Significance".to_string(),
859 ],
860 rebuttal_allowed: true,
861 acceptance_rate: Some(0.31), review_format: ReviewFormat::Mixed,
863 },
864 }
865 }
866
867 fn create_aaai_conference(edition_year: i32) -> Conference {
868 let prior_year = edition_year - 1;
869 Conference {
870 id: format!("aaai{edition_year}"),
871 name: "AAAI Conference on Artificial Intelligence".to_string(),
872 abbreviation: "AAAI".to_string(),
873 description: "Conference on artificial intelligence".to_string(),
874 url: "https://aaai.org/".to_string(),
875 ranking: ConferenceRanking::TopTier,
876 research_areas: vec![
877 "Artificial Intelligence".to_string(),
878 "Machine Learning".to_string(),
879 "Knowledge Representation".to_string(),
880 "Planning".to_string(),
881 ],
882 annual: true,
883 series_info: SeriesInfo {
884 series_number: 38,
885 year: edition_year as u32,
886 location: "Vancouver".to_string(),
887 country: "Canada".to_string(),
888 format: ConferenceFormat::Hybrid,
889 },
890 dates: ConferenceDates {
891 abstract_deadline: Some(
892 chrono::Utc
893 .with_ymd_and_hms(prior_year, 8, 15, 23, 59, 59)
894 .single()
895 .expect("invalid datetime"),
896 ),
897 paper_deadline: chrono::Utc
898 .with_ymd_and_hms(prior_year, 8, 19, 23, 59, 59)
899 .single()
900 .expect("invalid datetime"),
901 notification_date: chrono::Utc
902 .with_ymd_and_hms(prior_year, 12, 9, 12, 0, 0)
903 .single()
904 .expect("invalid datetime"),
905 camera_ready_deadline: chrono::Utc
906 .with_ymd_and_hms(edition_year, 1, 15, 23, 59, 59)
907 .single()
908 .expect("invalid datetime"),
909 conference_start: chrono::Utc
910 .with_ymd_and_hms(edition_year, 2, 20, 9, 0, 0)
911 .single()
912 .expect("invalid datetime"),
913 conference_end: chrono::Utc
914 .with_ymd_and_hms(edition_year, 2, 27, 18, 0, 0)
915 .single()
916 .expect("invalid datetime"),
917 },
918 requirements: SubmissionRequirements {
919 page_limit: 7,
920 word_limit: None,
921 format: FormatRequirements {
922 template: format!("AAAI {edition_year} LaTeX template"),
923 font_size: 10,
924 line_spacing: 1.0,
925 margins: "0.75 inch".to_string(),
926 citation_style: "AAAI".to_string(),
927 file_format: vec!["PDF".to_string()],
928 },
929 required_sections: vec![
930 "Abstract".to_string(),
931 "Introduction".to_string(),
932 "Related Work".to_string(),
933 "Approach".to_string(),
934 "Experiments".to_string(),
935 "Conclusion".to_string(),
936 ],
937 supplementary_allowed: false,
938 anonymous_submission: true,
939 double_blind: true,
940 },
941 review_process: ReviewProcess {
942 reviewers_per_paper: 3,
943 review_criteria: vec![
944 "Technical Quality".to_string(),
945 "Novelty".to_string(),
946 "Significance".to_string(),
947 "Clarity".to_string(),
948 ],
949 rebuttal_allowed: false,
950 acceptance_rate: Some(0.23), review_format: ReviewFormat::NumericalScores,
952 },
953 }
954 }
955
956 fn create_ijcai_conference(edition_year: i32) -> Conference {
957 Conference {
958 id: format!("ijcai{edition_year}"),
959 name: "International Joint Conference on Artificial Intelligence".to_string(),
960 abbreviation: "IJCAI".to_string(),
961 description: "International conference on artificial intelligence".to_string(),
962 url: "https://ijcai.org/".to_string(),
963 ranking: ConferenceRanking::TopTier,
964 research_areas: vec![
965 "Artificial Intelligence".to_string(),
966 "Machine Learning".to_string(),
967 "Automated Reasoning".to_string(),
968 "Multi-agent Systems".to_string(),
969 ],
970 annual: true,
971 series_info: SeriesInfo {
972 series_number: 33,
973 year: edition_year as u32,
974 location: "Jeju".to_string(),
975 country: "South Korea".to_string(),
976 format: ConferenceFormat::Hybrid,
977 },
978 dates: ConferenceDates {
979 abstract_deadline: Some(
980 chrono::Utc
981 .with_ymd_and_hms(edition_year, 1, 17, 23, 59, 59)
982 .single()
983 .expect("invalid datetime"),
984 ),
985 paper_deadline: chrono::Utc
986 .with_ymd_and_hms(edition_year, 1, 24, 23, 59, 59)
987 .single()
988 .expect("invalid datetime"),
989 notification_date: chrono::Utc
990 .with_ymd_and_hms(edition_year, 4, 16, 12, 0, 0)
991 .single()
992 .expect("invalid datetime"),
993 camera_ready_deadline: chrono::Utc
994 .with_ymd_and_hms(edition_year, 5, 15, 23, 59, 59)
995 .single()
996 .expect("invalid datetime"),
997 conference_start: chrono::Utc
998 .with_ymd_and_hms(edition_year, 8, 3, 9, 0, 0)
999 .single()
1000 .expect("invalid datetime"),
1001 conference_end: chrono::Utc
1002 .with_ymd_and_hms(edition_year, 8, 9, 18, 0, 0)
1003 .single()
1004 .expect("invalid datetime"),
1005 },
1006 requirements: SubmissionRequirements {
1007 page_limit: 7,
1008 word_limit: None,
1009 format: FormatRequirements {
1010 template: format!("IJCAI {edition_year} LaTeX template"),
1011 font_size: 10,
1012 line_spacing: 1.0,
1013 margins: "0.75 inch".to_string(),
1014 citation_style: "IJCAI".to_string(),
1015 file_format: vec!["PDF".to_string()],
1016 },
1017 required_sections: vec![
1018 "Abstract".to_string(),
1019 "Introduction".to_string(),
1020 "Background".to_string(),
1021 "Approach".to_string(),
1022 "Experiments".to_string(),
1023 "Conclusion".to_string(),
1024 ],
1025 supplementary_allowed: false,
1026 anonymous_submission: true,
1027 double_blind: true,
1028 },
1029 review_process: ReviewProcess {
1030 reviewers_per_paper: 3,
1031 review_criteria: vec![
1032 "Technical Quality".to_string(),
1033 "Novelty".to_string(),
1034 "Significance".to_string(),
1035 "Clarity".to_string(),
1036 ],
1037 rebuttal_allowed: true,
1038 acceptance_rate: Some(0.15), review_format: ReviewFormat::Mixed,
1040 },
1041 }
1042 }
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047 use super::*;
1048
1049 #[test]
1050 fn test_conference_manager_creation() {
1051 let manager = ConferenceManager::new();
1052 assert!(manager.conferences.is_empty());
1053 assert!(manager.submissions.is_empty());
1054 }
1055
1056 #[test]
1057 fn test_load_standard_conferences() {
1058 let mut manager = ConferenceManager::new();
1059 manager.load_standard_conferences();
1060
1061 assert_eq!(manager.conferences.len(), 5);
1062 let abbreviations: Vec<&str> = manager
1063 .conferences
1064 .values()
1065 .map(|c| c.abbreviation.as_str())
1066 .collect();
1067 for expected in ["NeurIPS", "ICML", "ICLR", "AAAI", "IJCAI"] {
1068 assert!(
1069 abbreviations.contains(&expected),
1070 "missing {expected} in {abbreviations:?}"
1071 );
1072 }
1073 }
1074
1075 #[test]
1082 fn test_load_standard_conferences_deadlines_are_in_the_future() {
1083 let mut manager = ConferenceManager::new();
1084 manager.load_standard_conferences();
1085 let now = Utc::now();
1086
1087 for conference in manager.conferences.values() {
1088 assert!(
1089 conference.dates.paper_deadline > now,
1090 "{}'s paper deadline {} is not in the future (now = {now})",
1091 conference.abbreviation,
1092 conference.dates.paper_deadline
1093 );
1094 }
1095
1096 let upcoming = manager.get_upcoming_deadlines(400);
1100 assert!(
1101 !upcoming.is_empty(),
1102 "expected at least one upcoming deadline within 400 days"
1103 );
1104 }
1105
1106 #[test]
1109 fn test_generate_deadline_alerts_populates_alerts() {
1110 let mut manager = ConferenceManager::new();
1111 manager.load_standard_conferences();
1112 assert!(manager.alerts.is_empty());
1113
1114 let created = manager.generate_deadline_alerts(&[400]);
1117 assert!(!created.is_empty());
1118 assert_eq!(manager.alerts.len(), created.len());
1119 assert!(manager.alerts.iter().all(|a| !a.sent));
1120 assert_eq!(manager.pending_alerts().len(), manager.alerts.len());
1121
1122 let created_again = manager.generate_deadline_alerts(&[400]);
1125 assert!(created_again.is_empty());
1126 assert_eq!(manager.alerts.len(), created.len());
1127
1128 let alert_id = manager.alerts[0].id.clone();
1129 manager
1130 .mark_alert_sent(&alert_id)
1131 .expect("mark should succeed");
1132 assert!(
1133 manager
1134 .alerts
1135 .iter()
1136 .find(|a| a.id == alert_id)
1137 .unwrap()
1138 .sent
1139 );
1140 assert_eq!(manager.pending_alerts().len(), manager.alerts.len() - 1);
1141 }
1142
1143 #[test]
1144 fn test_search_conferences() {
1145 let mut manager = ConferenceManager::new();
1146 manager.load_standard_conferences();
1147
1148 let ml_conferences = manager.search_conferences("Machine Learning");
1149 assert!(!ml_conferences.is_empty());
1150
1151 let top_tier = manager.get_conferences_by_ranking(ConferenceRanking::TopTier);
1152 assert!(!top_tier.is_empty());
1153 }
1154}