use crate::types::models::{
AdminAction, ColumnInfo, DateHierarchyInfo, Fieldset, FilterInfo, InlineFormInfo, ModelInfo,
PrepopulatedField, RelationOption,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
fn default_pk_field() -> String {
"id".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardResponse {
pub site_name: String,
pub site_header: String,
pub url_prefix: String,
pub login_url: String,
pub logout_url: String,
pub models: Vec<ModelInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub csrf_token: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListResponse {
pub model_name: String,
#[serde(default = "default_pk_field")]
pub pk_field: String,
pub count: u64,
pub page: u64,
pub page_size: u64,
pub total_pages: u64,
pub results: Vec<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub available_filters: Option<Vec<FilterInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub columns: Option<Vec<ColumnInfo>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DateHierarchyListResponse {
#[serde(flatten)]
pub response: ListResponse,
#[serde(skip_serializing_if = "Option::is_none")]
pub date_hierarchy: Option<DateHierarchyInfo>,
}
impl From<DateHierarchyListResponse> for ListResponse {
fn from(response: DateHierarchyListResponse) -> Self {
response.response
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RelationLookupResponse {
pub results: Vec<RelationOption>,
pub page: u64,
pub has_next: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListActionMetadataResponse {
pub pk_field: String,
pub actions: Vec<AdminAction>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetailResponse {
pub model_name: String,
pub data: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdminHistoryEntry {
pub id: i64,
pub actor: String,
pub timestamp: String,
pub action_name: String,
pub model_name: String,
pub object_id: String,
pub object_repr: String,
pub changed_fields: Vec<String>,
pub affected_count: u64,
pub success: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryResponse {
pub model_name: String,
pub object_id: String,
pub count: u64,
pub page: u64,
pub page_size: u64,
pub total_pages: u64,
pub results: Vec<AdminHistoryEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MutationResponse {
pub success: bool,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub affected: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InlineEditOutcome {
pub object_id: String,
pub changed_fields: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InlineEditError {
pub object_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub field: Option<String>,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InlineEditResponse {
pub updated: u64,
pub outcomes: Vec<InlineEditOutcome>,
pub errors: Vec<InlineEditError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BulkDeleteResponse {
pub success: bool,
pub deleted: u64,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportResponse {
pub success: bool,
pub imported: u64,
pub updated: u64,
pub skipped: u64,
pub failed: u64,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub errors: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ExportResponse {
#[serde(with = "serde_bytes")]
pub data: Vec<u8>,
pub filename: String,
pub content_type: String,
pub truncated: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_count: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoginResponse {
pub token: String,
pub username: String,
pub user_id: String,
pub is_staff: bool,
pub is_superuser: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldsResponse {
pub model_name: String,
pub fields: Vec<crate::types::models::FieldInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fieldsets: Option<Vec<Fieldset>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inlines: Vec<InlineFormInfo>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub prepopulated_fields: Vec<PrepopulatedField>,
#[serde(skip_serializing_if = "Option::is_none")]
pub values: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ManyToManyLookupResponse {
pub options: Vec<RelationOption>,
pub page: u64,
pub has_more: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::RelationOption;
use rstest::rstest;
use serde_json::json;
#[test]
fn date_hierarchy_response_keeps_legacy_fields_flat() {
let response = DateHierarchyListResponse {
response: ListResponse {
model_name: "Article".to_string(),
pk_field: "id".to_string(),
count: 1,
page: 1,
page_size: 25,
total_pages: 1,
results: vec![],
available_filters: None,
columns: None,
},
date_hierarchy: None,
};
let value = serde_json::to_value(response).expect("list response should serialize");
assert_eq!(value["model_name"], serde_json::json!("Article"));
assert!(value.get("response").is_none());
}
#[rstest]
fn history_response_serde_shape_round_trips_without_raw_values() {
let response = HistoryResponse {
model_name: "accounts.User".to_string(),
object_id: "42".to_string(),
count: 1,
page: 1,
page_size: 25,
total_pages: 1,
results: vec![AdminHistoryEntry {
id: 7,
actor: "admin-1".to_string(),
timestamp: "2026-08-09T12:34:56.123456Z".to_string(),
action_name: "UPDATE".to_string(),
model_name: "accounts.User".to_string(),
object_id: "42".to_string(),
object_repr: "accounts.User (42)".to_string(),
changed_fields: vec!["email".to_string()],
affected_count: 1,
success: true,
}],
};
let value = serde_json::to_value(&response).expect("history response must serialize");
let decoded: HistoryResponse =
serde_json::from_value(value.clone()).expect("history response must deserialize");
assert_eq!(
value,
serde_json::json!({
"model_name": "accounts.User",
"object_id": "42",
"count": 1,
"page": 1,
"page_size": 25,
"total_pages": 1,
"results": [{
"id": 7,
"actor": "admin-1",
"timestamp": "2026-08-09T12:34:56.123456Z",
"action_name": "UPDATE",
"model_name": "accounts.User",
"object_id": "42",
"object_repr": "accounts.User (42)",
"changed_fields": ["email"],
"affected_count": 1,
"success": true
}]
})
);
assert_eq!(decoded.results[0].changed_fields, ["email"]);
}
#[rstest]
fn fields_response_defaults_omitted_inlines_to_empty() {
let response: FieldsResponse =
serde_json::from_str(r#"{"model_name":"Parent","fields":[],"values":null}"#).unwrap();
assert!(response.inlines.is_empty());
assert!(response.prepopulated_fields.is_empty());
}
#[rstest]
fn relation_lookup_response_round_trips() {
let response = RelationLookupResponse {
results: vec![RelationOption {
id: "9".to_string(),
label: "Related object".to_string(),
}],
page: 3,
has_next: true,
};
let serialized =
serde_json::to_value(&response).expect("relation lookup response should serialize");
let deserialized: RelationLookupResponse = serde_json::from_value(serialized.clone())
.expect("relation lookup response should deserialize");
assert_eq!(
serialized,
json!({
"results": [{"id": "9", "label": "Related object"}],
"page": 3,
"has_next": true
})
);
assert_eq!(deserialized, response);
}
}