actix_admin/
model.rs

1use crate::view_model::{ActixAdminViewModelFilter, ActixAdminViewModelParams};
2use crate::{ActixAdminError, ActixAdminViewModelField};
3use actix_multipart::{Multipart, MultipartError};
4use actix_web::web::Bytes;
5use async_trait::async_trait;
6use chrono::{NaiveDate, NaiveDateTime};
7use futures_util::stream::StreamExt as _;
8use sea_orm::{DatabaseConnection, EntityTrait};
9use serde_derive::Serialize;
10use std::collections::HashMap;
11use std::fs::File;
12use std::io::Write;
13use std::time::{SystemTime, UNIX_EPOCH};
14
15#[async_trait]
16pub trait ActixAdminModelTrait {
17    async fn list_model(
18        db: &DatabaseConnection,
19        params: &ActixAdminViewModelParams,
20        filter_values: HashMap<String, Option<String>>
21    ) -> Result<(Option<u64>, Vec<ActixAdminModel>), ActixAdminError>;
22    fn get_fields() -> &'static [ActixAdminViewModelField];
23    fn validate_model(model: &mut ActixAdminModel);
24    async fn load_foreign_keys(models: &mut [ActixAdminModel], db: &DatabaseConnection);
25}
26
27pub trait ActixAdminModelValidationTrait<T> {
28    fn validate(_model: &T) -> HashMap<String, String> {
29        return HashMap::new();
30    }
31}
32
33pub struct ActixAdminModelFilter<E: EntityTrait> {
34    pub name: String,
35    pub filter_type: ActixAdminModelFilterType,
36    pub filter: fn(sea_orm::Select<E>, Option<String>) -> sea_orm::Select<E>,
37    pub values: Option<Vec<(String, String)>>,
38    pub foreign_key: Option<String>
39}
40
41#[derive(Clone, Debug, Serialize)]
42pub enum ActixAdminModelFilterType {
43    Text,
44    SelectList,
45    Date,
46    DateTime,
47    Checkbox,
48    TomSelectSearch
49}
50
51#[async_trait]
52pub trait ActixAdminModelFilterTrait<E: EntityTrait> {
53    fn get_filter() -> Vec<ActixAdminModelFilter<E>> {
54        Vec::new()
55    }
56    async fn get_filter_values(_filter: &ActixAdminModelFilter<E>, _db: &DatabaseConnection)-> Option<Vec<(String, String)>> {
57        None
58    }
59}
60
61impl<T: EntityTrait> From<ActixAdminModelFilter<T>> for ActixAdminViewModelFilter {
62    fn from(filter: ActixAdminModelFilter<T>) -> Self {
63        ActixAdminViewModelFilter {
64            name: filter.name,
65            value: None,
66            values: None,
67            filter_type: Some(filter.filter_type),
68            foreign_key: None
69        }
70    }
71}
72
73#[derive(Clone, Debug, Serialize)]
74pub struct ActixAdminModel {
75    pub primary_key: Option<String>,
76    pub values: HashMap<String, String>,
77    pub fk_values: HashMap<String, String>,
78    pub errors: HashMap<String, String>,
79    pub custom_errors: HashMap<String, String>,
80    pub display_name: Option<String>,
81}
82
83impl ActixAdminModel {
84    pub fn create_empty() -> ActixAdminModel {
85        ActixAdminModel {
86            primary_key: None,
87            values: HashMap::new(),
88            errors: HashMap::new(),
89            custom_errors: HashMap::new(),
90            fk_values: HashMap::new(),
91            display_name: None
92        }
93    }
94
95    pub async fn create_from_payload(
96        id: Option<i32>,
97        mut payload: Multipart, file_upload_folder: &str
98    ) -> Result<ActixAdminModel, MultipartError> {
99        let mut hashmap = HashMap::<String, String>::new();
100
101        while let Some(item) = payload.next().await {
102            let mut field = item?;
103
104            let mut binary_data: Vec<Bytes> = Vec::new();
105            while let Some(chunk) = field.next().await {
106                binary_data.push(chunk.unwrap());
107                //println!("-- CHUNK: \n{:?}", String::from_utf8(chunk.map_or(Vec::new(), |c| c.to_vec())));
108                // let res_string = String::from_utf8(chunk.map_or(Vec::new(), |c| c.to_vec()));
109            }
110            let binary_data = binary_data.concat();
111            if field.content_disposition().expect("expected content disposition").get_filename().is_some() {
112                let mut filename = field
113                    .content_disposition()
114                    .expect("expected content disposition")
115                    .get_filename()
116                    .unwrap()
117                    .to_string();
118
119                let mut file_path = format!("{}/{}", file_upload_folder, filename);
120                let file_exists = std::path::Path::new(&file_path).exists();
121                // Avoid overwriting existing files
122                if file_exists {
123                    filename =  format!("{}_{}", SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), filename);
124                    file_path = format!("{}/{}", file_upload_folder, filename);
125                }
126
127                let file = File::create(file_path);
128                let _res = file.unwrap().write_all(&binary_data);
129
130                hashmap.insert(
131                    field.name().expect("expected file name").to_string(),
132                    filename.clone()
133                );
134            } else {
135                let res_string = String::from_utf8(binary_data);
136                if res_string.is_ok() {
137                    hashmap.insert(field.name().expect("expected file name").to_string(), res_string.unwrap());
138                }
139            }
140        }
141
142        Ok(ActixAdminModel {
143            primary_key: match id {
144                Some(id) => Some(id.to_string()),
145                None => None
146            },
147            values: hashmap,
148            errors: HashMap::new(),
149            custom_errors: HashMap::new(),
150            fk_values: HashMap::new(),
151            display_name: None
152        })
153    }
154
155    pub fn get_value<T: std::str::FromStr>(
156        &self,
157        key: &str,
158        is_option_or_string: bool,
159        is_allowed_to_be_empty: bool
160    ) -> Result<Option<T>, String> {
161        self.get_value_by_closure(key, is_option_or_string, is_allowed_to_be_empty, |val| val.parse::<T>())
162    }
163
164    pub fn get_datetime(
165        &self,
166        key: &str,
167        is_option_or_string: bool,
168        is_allowed_to_be_empty: bool
169    ) -> Result<Option<NaiveDateTime>, String> {
170        self.get_value_by_closure(key, is_option_or_string, is_allowed_to_be_empty, |val| {
171            NaiveDateTime::parse_from_str(val, "%Y-%m-%dT%H:%M")
172        })
173    }
174
175    pub fn get_date(
176        &self,
177        key: &str,
178        is_option_or_string: bool,
179        is_allowed_to_be_empty: bool
180    ) -> Result<Option<NaiveDate>, String> {
181        self.get_value_by_closure(key, is_option_or_string, is_allowed_to_be_empty, |val| {
182            NaiveDate::parse_from_str(val, "%Y-%m-%d")
183        })
184    }
185
186    pub fn get_bool(&self, key: &str, is_option_or_string: bool, is_allowed_to_be_empty: bool) -> Result<Option<bool>, String> {
187        let val = self.get_value_by_closure(key, is_option_or_string, is_allowed_to_be_empty ,|val| {
188            if !val.is_empty() && (val == "true" || val == "yes") {
189                Ok(true)
190            } else {
191                Ok(false)
192            }
193        });
194        // not selected bool field equals to false and not to missing
195        match val {
196            Ok(val) => Ok(val),
197            Err(_) => Ok(Some(false)),
198        }
199    }
200
201    fn get_value_by_closure<T: std::str::FromStr>(
202        &self,
203        key: &str,
204        is_option_or_string: bool,
205        is_allowed_to_be_empty: bool,
206        f: impl Fn(&String) -> Result<T, <T as std::str::FromStr>::Err>,
207    ) -> Result<Option<T>, String> {
208        let value = self.values.get(key);
209
210        let res: Result<Option<T>, String> = match value {
211            Some(val) => {
212                match (val.is_empty(), is_option_or_string, is_allowed_to_be_empty) {
213                    (true, true, true) => return Ok(None),
214                    (true, true, false) => return Err("Cannot be empty".to_string()),
215                    _ => {}
216                };
217
218                let parsed_val = f(val);
219
220                match parsed_val {
221                    Ok(val) => Ok(Some(val)),
222                    Err(_) => Err("Invalid Value".to_string()),
223                }
224            }
225            _ => {
226                match (is_option_or_string, is_allowed_to_be_empty) {
227                    (true, true) => Ok(None),
228                    (true, false) => Err("Cannot be empty".to_string()),
229                    (false, _) => Err("Invalid Value".to_string()), // a missing value in the form for a non-optional value
230                }
231            }
232        };
233
234        res
235    }
236
237    pub fn has_errors(&self) -> bool {
238        return (&self.errors.len() + &self.custom_errors.len()) != 0 as usize;
239    }
240}