Skip to main content

actix_admin/routes/
list.rs

1use crate::prelude::*;
2use actix_web::http::header::ContentDisposition;
3use actix_web::{error, web, Error, HttpRequest, HttpResponse};
4use csv::WriterBuilder;
5use sea_orm::DatabaseConnection;
6use serde_derive::{Deserialize, Serialize};
7use std::fmt;
8use tera::Context;
9
10use super::helpers::{add_default_context_with_session, SearchParams};
11use super::{add_auth_context, render_template, validate_sort_by, ListQuery, RoutePrelude};
12use crate::admin_prelude;
13use crate::ActixAdminModel;
14use crate::ActixAdminNotification;
15use crate::ActixAdminViewModel;
16use crate::ActixAdminViewModelTrait;
17use actix_session::Session;
18
19#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
20pub enum SortOrder {
21    Asc,
22    Desc,
23}
24
25impl fmt::Display for SortOrder {
26    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
27        match self {
28            SortOrder::Asc => write!(f, "Asc"),
29            SortOrder::Desc => write!(f, "Desc"),
30        }
31    }
32}
33
34pub fn replace_regex(view_model: &ActixAdminViewModel, models: &mut [ActixAdminModel]) {
35    for field in view_model
36        .fields
37        .iter()
38        .filter(|f| f.list_regex_mask.is_some())
39    {
40        let regex = field.list_regex_mask.as_ref().unwrap();
41        for model in models.iter_mut() {
42            if let Some(value) = model.values.get_mut(&field.field_name) {
43                *value = regex.replace_all(value, "****").to_string();
44            }
45        }
46    }
47}
48
49pub async fn export_csv<E: ActixAdminViewModelTrait>(
50    session: Session,
51    req: HttpRequest,
52    data: web::Data<ActixAdmin>,
53    db: web::Data<DatabaseConnection>,
54) -> Result<HttpResponse, Error> {
55    let actix_admin = &data.into_inner();
56    let ctx = admin_prelude!(&session, &req, actix_admin, RoutePrelude::export(), E);
57
58    let query = ListQuery::from_query(req.query_string(), ctx.view_model);
59    validate_sort_by(ctx.view_model, &query.sort_by)?;
60
61    let params = query.to_view_model_params(ctx.tenant_ref, false);
62
63    let entities = match E::list(&db, &params).await {
64        Ok(res) => {
65            let mut entities = res.1;
66            replace_regex(ctx.view_model, &mut entities);
67            entities
68        }
69        Err(_) => Vec::new(),
70    };
71
72    let mut writer = WriterBuilder::new().from_writer(vec![]);
73    let mut fields = ctx
74        .view_model
75        .fields
76        .iter()
77        .map(|f| f.field_name.clone())
78        .collect::<Vec<_>>();
79    fields.insert(0, ctx.view_model.primary_key.clone());
80    writer
81        .write_record(&fields)
82        .map_err(error::ErrorInternalServerError)?;
83
84    for entity in entities {
85        let mut values = vec![entity.primary_key.unwrap_or_default()];
86        for field in ctx.view_model.fields {
87            let value = entity
88                .values
89                .get(&field.field_name)
90                .cloned()
91                .unwrap_or_default();
92            values.push(
93                entity
94                    .fk_values
95                    .get(&field.field_name)
96                    .cloned()
97                    .unwrap_or(value),
98            );
99        }
100        writer
101            .write_record(&values)
102            .map_err(error::ErrorInternalServerError)?;
103    }
104
105    let body = writer
106        .into_inner()
107        .map_err(error::ErrorInternalServerError)?;
108    Ok(HttpResponse::Ok()
109        .content_type("text/csv")
110        .insert_header(ContentDisposition::attachment("export.csv"))
111        .body(body))
112}
113
114pub async fn list<E: ActixAdminViewModelTrait>(
115    session: Session,
116    req: HttpRequest,
117    data: web::Data<ActixAdmin>,
118    db: web::Data<DatabaseConnection>,
119) -> Result<HttpResponse, Error> {
120    let actix_admin = &data.into_inner();
121    let route_ctx = admin_prelude!(&session, &req, actix_admin, RoutePrelude::view(), E);
122
123    let query = ListQuery::from_query(req.query_string(), route_ctx.view_model);
124    validate_sort_by(route_ctx.view_model, &query.sort_by)?;
125
126    let mut ctx = Context::new();
127    add_auth_context(&session, actix_admin, &mut ctx);
128
129    let vm_params = query.to_view_model_params(route_ctx.tenant_ref, true);
130    let search_params = SearchParams::from_list_query(&query);
131
132    let (num_pages, mut entities) = match E::list(&db, &vm_params).await {
133        Ok(res) => res,
134        Err(e) => {
135            ctx.insert("entities", &Vec::<ActixAdminModel>::new());
136            ctx.insert("num_pages", &0);
137            ctx.insert("min_show_page", &1);
138            ctx.insert("max_show_page", &1);
139            ctx.insert("page", &1);
140            ctx.insert("notifications", &[ActixAdminNotification::from(e)]);
141            return Ok(HttpResponse::InternalServerError()
142                .content_type("text/html")
143                .body(
144                    render_template(&actix_admin.tera, "list.html", &ctx)
145                        .map_err(error::ErrorInternalServerError)?,
146                ));
147        }
148    };
149
150    // Best-effort in-memory sort on the FK display column when the user
151    // requested to sort by a foreign-key field. This only re-orders within
152    // the current page (the primary query still orders on the FK id) but
153    // gives visually correct alphabetical order in the common case of
154    // browsing without paging past `entities_per_page`.
155    if let Some(field) = route_ctx
156        .view_model
157        .fields
158        .iter()
159        .find(|f| f.field_name == query.sort_by)
160    {
161        if !field.foreign_key.is_empty() {
162            let asc = matches!(query.sort_order, SortOrder::Asc);
163            entities.sort_by(|a, b| {
164                let av = a
165                    .fk_values
166                    .get(&field.field_name)
167                    .cloned()
168                    .unwrap_or_default();
169                let bv = b
170                    .fk_values
171                    .get(&field.field_name)
172                    .cloned()
173                    .unwrap_or_default();
174                if asc {
175                    av.cmp(&bv)
176                } else {
177                    bv.cmp(&av)
178                }
179            });
180        }
181    }
182
183    replace_regex(route_ctx.view_model, &mut entities);
184    let num_pages = num_pages.unwrap_or(1);
185    let page = query.page.min(num_pages);
186    let min_show_page = page.saturating_sub(4).max(1);
187    let max_show_page = (page + 4).min(num_pages);
188
189    add_default_context_with_session(
190        &mut ctx,
191        req,
192        route_ctx.view_model,
193        route_ctx.entity_name,
194        actix_admin,
195        Vec::new(),
196        &search_params,
197        Some(&session),
198    );
199
200    ctx.insert("entities", &entities);
201    ctx.insert("num_pages", &num_pages);
202    ctx.insert("min_show_page", &min_show_page);
203    ctx.insert("max_show_page", &max_show_page);
204    let mut viewmodel_filter = E::get_viewmodel_filter(&db).await;
205    // Round-trip the current query's filter values back into the view-model
206    // filter map so templates can pre-select them (and so re-submitting the
207    // filter form doesn't silently clear the current selection).
208    for f in &query.filters {
209        if let Some(entry) = viewmodel_filter.get_mut(&f.name) {
210            entry.value = f.value.clone();
211            entry.operator = f.operator.clone();
212        }
213    }
214    ctx.insert("viewmodel_filter", &viewmodel_filter);
215
216    Ok(HttpResponse::Ok().content_type("text/html").body(
217        render_template(&actix_admin.tera, "list.html", &ctx)
218            .map_err(|err| error::ErrorInternalServerError(format!("{err:?}")))?,
219    ))
220}