iqps_backend/
qp.rs

1//! Utils for parsing question paper details
2
3use color_eyre::eyre::eyre;
4use duplicate::duplicate_item;
5use serde::Deserialize;
6use serde::Serialize;
7
8use crate::env::EnvVars;
9
10/// Represents a semester.
11///
12/// It can be parsed from a [`String`] using the `.try_from()` function. An error will be returned if the given string has an invalid value.
13///
14/// This value can be converted back into a [`String`] using the [`From`] trait implementation.
15pub enum Semester {
16    /// Autumn semester, parsed from `autumn`
17    Autumn,
18    /// Spring semester, parsed from `spring`
19    Spring,
20    /// Unknown/wildcard semester, parsed from an empty string.
21    ///
22    /// Note that this is different from an invalid value and is used to represent papers for which the semester is not known. An invalid value would be `puppy` or `Hippopotomonstrosesquippedaliophobia` for example.
23    Unknown,
24}
25
26impl TryFrom<&str> for Semester {
27    type Error = color_eyre::eyre::Error;
28
29    fn try_from(value: &str) -> Result<Self, Self::Error> {
30        if value == "autumn" {
31            Ok(Semester::Autumn)
32        } else if value == "spring" {
33            Ok(Semester::Spring)
34        } else if value.is_empty() {
35            Ok(Semester::Unknown)
36        } else {
37            Err(eyre!("Error parsing semester: Invalid value."))
38        }
39    }
40}
41
42impl From<&Semester> for String {
43    fn from(value: &Semester) -> Self {
44        match value {
45            Semester::Autumn => "autumn".into(),
46            Semester::Spring => "spring".into(),
47            Semester::Unknown => "".into(),
48        }
49    }
50}
51
52/// Represents the exam type of the paper.
53///
54/// Can be converted to and parsed from a String using the [`From`] and [`TryFrom`] trait implementations.
55pub enum Exam {
56    /// Mid-semester examination, parsed from `midsem`
57    Midsem,
58    /// End-semester examination, parsed from `endsem`
59    Endsem,
60    /// Class test, parsed from either `ct` or `ct` followed by a number (eg: `ct1` or `ct10`).
61    ///
62    /// The optional number represents the number of the class test (eg: class test 1 or class test 21). This will be None if the number is not known, parsed from `ct`.
63    CT(Option<usize>),
64    /// Unknown class test, parsed from an empty string.
65    ///
66    /// Note that this is different from an invalid value and is used to represent papers for which the exam is not known. An invalid value would be `catto` or `metakgp` for example.
67    Unknown,
68}
69
70impl TryFrom<&str> for Exam {
71    type Error = color_eyre::eyre::Error;
72
73    fn try_from(value: &str) -> Result<Self, Self::Error> {
74        if value == "midsem" {
75            Ok(Exam::Midsem)
76        } else if value == "endsem" {
77            Ok(Exam::Endsem)
78        } else if let Some(stripped) = value.strip_prefix("ct") {
79            if stripped.is_empty() {
80                Ok(Exam::CT(None))
81            } else if let Ok(i) = stripped.parse::<usize>() {
82                Ok(Exam::CT(Some(i)))
83            } else {
84                Err(eyre!("Error parsing exam: Invalid class test number."))
85            }
86        } else if value.is_empty() {
87            Ok(Exam::Unknown)
88        } else {
89            Err(eyre!("Error parsing exam: Unknown exam type."))
90        }
91    }
92}
93
94impl From<&Exam> for String {
95    fn from(value: &Exam) -> Self {
96        match value {
97            Exam::Midsem => "midsem".into(),
98            Exam::Endsem => "endsem".into(),
99            Exam::Unknown => "".into(),
100            Exam::CT(None) => "ct".into(),
101            Exam::CT(Some(i)) => format!("ct{}", i),
102        }
103    }
104}
105
106#[duplicate_item(
107    Serializable;
108    [ Exam ];
109    [ Semester ];
110)]
111impl Serialize for Serializable {
112    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
113    where
114        S: serde::Serializer,
115    {
116        serializer.serialize_str(String::from(self).as_str())
117    }
118}
119
120pub trait WithUrl: Sized {
121    /// Returns the question paper with the full static files URL in the `filelink` field instead of just the slug. See the [`crate::pathutils`] module for what a slug is.
122    fn with_url(self, env_vars: &EnvVars) -> Result<Self, color_eyre::eyre::Error>;
123}
124
125#[derive(Deserialize)]
126/// The details for a question paper in the library
127pub struct LibraryQP {
128    pub course_code: String,
129    pub course_name: String,
130    pub year: i32,
131    pub exam: String,
132    pub semester: String,
133    pub filename: String,
134    pub approve_status: bool,
135}
136
137#[derive(Serialize, sqlx::FromRow)]
138/// The fields of a question paper sent from the search endpoint
139pub struct BaseQP {
140    pub id: i32,
141    pub filelink: String,
142    pub from_library: bool,
143    pub course_code: String,
144    pub course_name: String,
145    pub year: i32,
146    pub semester: Semester,
147    pub exam: Exam,
148    pub note: String,
149}
150
151
152#[derive(Serialize, sqlx::FromRow)]
153/// The fields of a question paper sent from the admin dashboard endpoints.
154///
155/// This includes fields such as `approve_status` and `upload_timestamp` that would only be relevant to the dashboard.
156pub struct AdminDashboardQP {
157    #[serde(flatten)]
158    #[sqlx(flatten)]
159    /// The basic question paper fields, inherited from the `BaseQP` type. This field is flattened
160    /// for JSON serialization.
161    pub qp: BaseQP,
162    pub upload_timestamp: chrono::NaiveDateTime,
163    pub approve_status: bool,
164}
165
166impl WithUrl for BaseQP {
167    fn with_url(self, env_vars: &EnvVars) -> Result<Self, color_eyre::eyre::Error> {
168        Ok(Self {
169            filelink: env_vars.paths.get_url_from_slug(&self.filelink)?,
170            ..self
171        })
172    }
173}
174
175impl WithUrl for AdminDashboardQP {
176    fn with_url(self, env_vars: &EnvVars) -> Result<Self, color_eyre::eyre::Error> {
177        Ok(Self {
178            qp: self.qp.with_url(env_vars)?,
179            ..self
180        })
181    }
182}