1use serde_derive::Deserialize;
17
18use crate::view_model::{
19 ActixAdminFilterOperator, ActixAdminViewModelFilter, ActixAdminViewModelParams,
20};
21use crate::{ActixAdminViewModel, SortOrder};
22
23use super::DEFAULT_ENTITIES_PER_PAGE;
24
25#[derive(Debug, Deserialize, Default, Clone)]
28pub struct Params {
29 pub(crate) page: Option<u64>,
30 pub(crate) entities_per_page: Option<u64>,
31 pub(crate) search: Option<String>,
32 pub(crate) sort_by: Option<String>,
33 pub(crate) sort_order: Option<SortOrder>,
34}
35
36impl Params {
37 pub fn from_query(qs: &str) -> Self {
39 serde_urlencoded::from_str::<Self>(qs).unwrap_or_default()
40 }
41}
42
43pub fn parse_filters_from_query(qs: &str) -> Vec<ActixAdminViewModelFilter> {
51 use std::collections::HashMap;
52
53 let mut values: Vec<(String, Option<String>)> = Vec::new();
54 let mut operators: HashMap<String, ActixAdminFilterOperator> = HashMap::new();
55
56 for (key, value) in form_urlencoded::parse(qs.as_bytes()) {
57 let Some(rest) = key.strip_prefix("filter_") else {
58 continue;
59 };
60 if let Some(name) = rest.strip_suffix("__op") {
61 if let Ok(op) = value.parse::<ActixAdminFilterOperator>() {
62 operators.insert(name.to_string(), op);
63 }
64 continue;
65 }
66 let v = if value.is_empty() {
67 None
68 } else {
69 Some(value.into_owned())
70 };
71 values.push((rest.to_string(), v));
72 }
73
74 values
75 .into_iter()
76 .map(|(name, value)| {
77 let operator = operators.remove(&name);
78 ActixAdminViewModelFilter {
79 name,
80 value,
81 values: None,
82 filter_type: None,
83 foreign_key: None,
84 operators: Vec::new(),
85 operator,
86 }
87 })
88 .collect()
89}
90
91#[derive(Debug, Clone)]
94pub struct ListQuery {
95 pub page: u64,
96 pub entities_per_page: u64,
97 pub search: String,
98 pub sort_by: String,
99 pub sort_order: SortOrder,
100 pub filters: Vec<ActixAdminViewModelFilter>,
101}
102
103impl ListQuery {
104 pub fn from_query(qs: &str, view_model: &ActixAdminViewModel) -> Self {
107 let params = Params::from_query(qs);
108 let filters = parse_filters_from_query(qs);
109 Self::from_params_and_filters(params, filters, view_model)
110 }
111
112 pub fn from_params_and_filters(
114 params: Params,
115 filters: Vec<ActixAdminViewModelFilter>,
116 view_model: &ActixAdminViewModel,
117 ) -> Self {
118 ListQuery {
119 page: params.page.unwrap_or(1).max(1),
123 entities_per_page: params
124 .entities_per_page
125 .unwrap_or(DEFAULT_ENTITIES_PER_PAGE)
126 .max(1),
127 search: params.search.unwrap_or_default(),
128 sort_by: params
129 .sort_by
130 .unwrap_or_else(|| view_model.primary_key.clone()),
131 sort_order: params.sort_order.unwrap_or(SortOrder::Asc),
132 filters,
133 }
134 }
135
136 pub fn from_form(form: &[(String, String)], view_model: &ActixAdminViewModel) -> Self {
140 let mut params = Params::default();
141 for (k, v) in form {
142 match k.as_str() {
143 "page" => params.page = v.parse().ok(),
144 "entities_per_page" => params.entities_per_page = v.parse().ok(),
145 "search" => params.search = Some(v.clone()),
146 "sort_by" => params.sort_by = Some(v.clone()),
147 "sort_order" => {
148 params.sort_order = match v.as_str() {
149 "Asc" => Some(SortOrder::Asc),
150 "Desc" => Some(SortOrder::Desc),
151 _ => None,
152 }
153 }
154 _ => {}
155 }
156 }
157 Self::from_params_and_filters(params, Vec::new(), view_model)
158 }
159
160 pub fn to_query_string(&self) -> String {
163 let pairs: Vec<(&str, String)> = vec![
164 ("page", self.page.to_string()),
165 ("entities_per_page", self.entities_per_page.to_string()),
166 ("search", self.search.clone()),
167 ("sort_by", self.sort_by.clone()),
168 ("sort_order", self.sort_order.to_string()),
169 ];
170 serde_urlencoded::to_string(&pairs).unwrap_or_default()
171 }
172
173 pub fn to_view_model_params(
177 &self,
178 tenant_ref: Option<i32>,
179 paginated: bool,
180 ) -> ActixAdminViewModelParams {
181 ActixAdminViewModelParams {
182 page: if paginated { Some(self.page) } else { None },
183 entities_per_page: if paginated {
184 Some(self.entities_per_page)
185 } else {
186 None
187 },
188 viewmodel_filter: self.filters.clone(),
189 search: self.search.clone(),
190 sort_by: self.sort_by.clone(),
191 sort_order: self.sort_order.clone(),
192 tenant_ref,
193 }
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn params_from_query_handles_garbage() {
203 let p = Params::from_query("page=nope&entities_per_page=xyz&random=1");
204 assert!(p.page.is_none());
205 assert!(p.entities_per_page.is_none());
206 }
207
208 #[test]
209 fn params_from_query_parses_valid_input() {
210 let p = Params::from_query(
211 "page=3&entities_per_page=20&search=foo&sort_by=name&sort_order=Desc",
212 );
213 assert_eq!(p.page, Some(3));
214 assert_eq!(p.entities_per_page, Some(20));
215 assert_eq!(p.search.as_deref(), Some("foo"));
216 assert_eq!(p.sort_by.as_deref(), Some("name"));
217 assert!(matches!(p.sort_order, Some(SortOrder::Desc)));
218 }
219
220 #[test]
221 fn filter_parser_extracts_filter_prefixed_pairs() {
222 let filters = parse_filters_from_query("page=1&filter_status=active&filter_owner=&other=x");
223 let names: Vec<_> = filters.iter().map(|f| f.name.as_str()).collect();
224 assert_eq!(names, vec!["status", "owner"]);
225 assert_eq!(filters[0].value.as_deref(), Some("active"));
226 assert!(filters[1].value.is_none());
227 }
228
229 #[test]
230 fn filter_parser_survives_odd_percent_encoding() {
231 let _ = parse_filters_from_query("filter_x=%ZZ");
232 let _ = parse_filters_from_query("&&filter_=v&filter_a");
233 }
234
235 #[test]
236 fn filter_parser_handles_equals_in_value() {
237 let filters = parse_filters_from_query("filter_query=a=b");
238 assert_eq!(filters.len(), 1);
239 assert_eq!(filters[0].value.as_deref(), Some("a=b"));
240 }
241
242 #[test]
243 fn filter_parser_decodes_plus_as_space_in_key_and_value() {
244 let filters = parse_filters_from_query("filter_Post+with+Tom+Select=hello+world");
245 assert_eq!(filters.len(), 1);
246 assert_eq!(filters[0].name, "Post with Tom Select");
247 assert_eq!(filters[0].value.as_deref(), Some("hello world"));
248 }
249
250 #[test]
251 fn filter_parser_decodes_percent_encoded_key_and_value() {
252 let filters = parse_filters_from_query("filter_Post%20with%20Tom%20Select=a%2Fb");
253 assert_eq!(filters.len(), 1);
254 assert_eq!(filters[0].name, "Post with Tom Select");
255 assert_eq!(filters[0].value.as_deref(), Some("a/b"));
256 }
257}