paperless/
document.rs

1//! # Document
2//!
3//! A document is stored on the server. There are a lot of way to filter documents
4
5use crate::{asn, correspondent, document_type, saved_view, storage_path, tag};
6use chrono::{DateTime, NaiveDate, Utc};
7use reqwest::Url;
8use serde::Deserialize;
9
10#[derive(Debug, Deserialize, Copy, Clone)]
11pub struct Id(u64);
12
13impl From<u64> for Id {
14    fn from(value: u64) -> Self {
15        Self(value)
16    }
17}
18impl From<Id> for u64 {
19    fn from(value: Id) -> Self {
20        value.0
21    }
22}
23impl ToString for Id {
24    fn to_string(&self) -> String {
25        self.0.to_string()
26    }
27}
28
29#[derive(Debug, Deserialize)]
30pub struct Document {
31    pub id: Id,
32    pub correspondent: Option<correspondent::Id>,
33    pub document_type: Option<correspondent::Id>,
34    pub storage_path: Option<storage_path::Id>,
35    pub title: String,
36    pub content: String,
37    pub tags: Vec<tag::Id>,
38    pub created: DateTime<Utc>,
39    pub created_date: NaiveDate,
40    pub modified: DateTime<Utc>,
41    pub added: DateTime<Utc>,
42    pub archive_serial_number: Option<asn::ASN>,
43    pub original_file_name: Option<String>,
44    pub archived_file_name: Option<String>,
45}
46
47/// Filter used when searching for a document
48///
49/// Multiple values can be defined at the same time if needed
50#[derive(Debug, Default)]
51pub struct Filter {
52    /// Query is equivalent to "advanced search" in the interface
53    pub query: Option<String>,
54    pub title_content_contains: Option<String>,
55    pub is_in_inbox: Option<bool>,
56    pub title_starts_with: Option<String>,
57    pub title_ends_with: Option<String>,
58    pub title_contains: Option<String>,
59    pub title_is: Option<String>,
60    pub content_starts_with: Option<String>,
61    pub content_ends_with: Option<String>,
62    pub content_contains: Option<String>,
63    pub content_is: Option<String>,
64    pub archive_serial_number_is: Option<asn::ASN>,
65    pub archive_serial_numer_gt: Option<asn::ASN>,
66    pub archive_serial_number_gte: Option<asn::ASN>,
67    pub archive_serial_numer_lt: Option<asn::ASN>,
68    pub archive_serial_number_lte: Option<asn::ASN>,
69    pub archive_serial_number_isnull: Option<bool>,
70    pub created_year: Option<usize>,
71    pub created_month: Option<usize>,
72    pub created_day: Option<usize>,
73    pub created_date_gt: Option<NaiveDate>,
74    pub created_gt: Option<DateTime<Utc>>,
75    pub created_date_lt: Option<NaiveDate>,
76    pub created_lt: Option<DateTime<Utc>>,
77    pub added_year: Option<usize>,
78    pub added_month: Option<usize>,
79    pub added_day: Option<usize>,
80    pub added_date_gt: Option<NaiveDate>,
81    pub added_gt: Option<DateTime<Utc>>,
82    pub added_date_lt: Option<NaiveDate>,
83    pub added_lt: Option<DateTime<Utc>>,
84    pub modified_year: Option<usize>,
85    pub modified_month: Option<usize>,
86    pub modified_day: Option<usize>,
87    pub modified_date_gt: Option<NaiveDate>,
88    pub modified_gt: Option<DateTime<Utc>>,
89    pub modified_date_lt: Option<NaiveDate>,
90    pub modified_lt: Option<DateTime<Utc>>,
91    pub correspondent_isnull: Option<bool>,
92    pub correspondent_id_in: Option<Vec<correspondent::Id>>,
93    pub correspondent_id: Option<correspondent::Id>,
94    pub correspondent_name_starts_with: Option<String>,
95    pub correspondent_name_ends_with: Option<String>,
96    pub correspondent_name_contains: Option<String>,
97    pub correspondent_name_is: Option<String>,
98    pub is_tagged: Option<bool>,
99    /// The document must have all those tags
100    pub tag_id_all: Vec<tag::Id>,
101    /// The document must have none of those tags
102    pub tag_id_none: Vec<tag::Id>,
103    /// The document must have any of those tags
104    pub tag_id_in: Vec<tag::Id>,
105    /// The document must have at least this tag
106    pub tag_id: Option<tag::Id>,
107    pub tag_name_starts_with: Option<String>,
108    pub tag_name_ends_with: Option<String>,
109    pub tag_name_contains: Option<String>,
110    pub tag_name_is: Option<String>,
111    pub document_type_isnull: Option<bool>,
112    pub document_type_id_in: Vec<document_type::Id>,
113    pub document_type_id: Option<document_type::Id>,
114    pub document_type_name_starts_with: Option<String>,
115    pub document_type_name_ends_with: Option<String>,
116    pub document_type_name_contains: Option<String>,
117    pub document_type_name_is: Option<String>,
118    pub storage_path_isnull: Option<bool>,
119    pub storage_path_id_in: Vec<storage_path::Id>,
120    pub storage_path_id: Option<storage_path::Id>,
121    pub storage_path_name_starts_with: Option<String>,
122    pub storage_path_name_ends_with: Option<String>,
123    pub storage_path_name_contains: Option<String>,
124    pub storage_path_name_is: Option<String>,
125    pub more_like: Option<Id>,
126}
127
128impl Filter {
129    #[rustfmt::skip]
130    /// Insert query parameter in a url
131    pub(crate) fn insert_query(self, url: &mut Url) {
132        if let Some(more_like) = self.more_like {
133            url.query_pairs_mut().append_pair("more_like_id", &more_like.to_string());
134        }
135        if let Some(query) = self.query {
136            url.query_pairs_mut().append_pair("query", &query.to_string());
137        }
138        if let Some(is_tagged) = self.is_tagged {
139            url.query_pairs_mut().append_pair("is_tagged", &is_tagged.to_string());
140        }
141
142        url.query_pairs_mut()
143            .append_pair("title_content", &self.title_content_contains.unwrap_or_default())
144            .append_pair("is_in_inbox", &if let Some(is_in_inbox) = self.is_in_inbox { is_in_inbox.to_string() } else { String::default() })
145            .append_pair("title__istartswith", &self.title_starts_with.unwrap_or_default())
146            .append_pair("title__iendswith", &self.title_ends_with.unwrap_or_default())
147            .append_pair("title__icontains", &self.title_contains.unwrap_or_default())
148            .append_pair("title__iexact", &self.title_is.unwrap_or_default())
149            .append_pair("content__istartswith", &self.content_starts_with.unwrap_or_default())
150            .append_pair("content__iendswith", &self.content_ends_with.unwrap_or_default())
151            .append_pair("content__icontains", &self.content_contains.unwrap_or_default())
152            .append_pair("content__iexact", &self.content_is.unwrap_or_default())
153            .append_pair("archive_serial_number", &self.archive_serial_number_is.map(|asn| asn.to_string()).unwrap_or_default())
154            .append_pair("archive_serial_number__gt", &self.archive_serial_numer_gt.map(|asn| asn.to_string()).unwrap_or_default())
155            .append_pair("archive_serial_number__gte", &self.archive_serial_number_gte.map(|asn| asn.to_string()).unwrap_or_default())
156            .append_pair("archive_serial_number__lt", &self.archive_serial_numer_lt.map(|asn| asn.to_string()).unwrap_or_default())
157            .append_pair("archive_serial_number__lte", &self.archive_serial_number_lte.map(|asn| asn.to_string()).unwrap_or_default())
158            .append_pair("archive_serial_number__isnull", &if let Some(isnull) = self.archive_serial_number_isnull { isnull.to_string() } else { String::default() })
159            .append_pair("created__year", &self.created_year.map(|year| year.to_string()).unwrap_or_default())
160            .append_pair("created__month", &self.created_year.map(|month| month.to_string()).unwrap_or_default())
161            .append_pair("created__day", &self.created_year.map(|day| day.to_string()).unwrap_or_default())
162            .append_pair("created__date__gt", &self.created_date_gt.map(|d| d.format("%Y-%m-%dT%H:%M:%SZ").to_string()).unwrap_or_default())
163            .append_pair("created__gt", &self.created_gt.map(|d| d.format("%Y-%m-%dT%H:%M:%SZ").to_string()).unwrap_or_default())
164            .append_pair("created__date__lt", &self.created_date_lt.map(|d| d.format("%Y-%m-%dT%H:%M:%SZ").to_string()).unwrap_or_default())
165            .append_pair("created__lt", &self.created_lt.map(|d| d.format("%Y-%m-%dT%H:%M:%SZ").to_string()).unwrap_or_default())
166            .append_pair("added__year", &self.added_year.map(|year| year.to_string()).unwrap_or_default())
167            .append_pair("added__month", &self.added_year.map(|month| month.to_string()).unwrap_or_default())
168            .append_pair("added__day", &self.added_year.map(|day| day.to_string()).unwrap_or_default())
169            .append_pair("added__date__gt", &self.added_date_gt.map(|d| d.format("%Y-%m-%dT%H:%M:%SZ").to_string()).unwrap_or_default())
170            .append_pair("added__gt", &self.added_gt.map(|d| d.format("%Y-%m-%dT%H:%M:%SZ").to_string()).unwrap_or_default())
171            .append_pair("added__date__lt", &self.added_date_lt.map(|d| d.format("%Y-%m-%dT%H:%M:%SZ").to_string()).unwrap_or_default())
172            .append_pair("added__lt", &self.added_lt.map(|d| d.format("%Y-%m-%dT%H:%M:%SZ").to_string()).unwrap_or_default())
173            .append_pair("modified__year", &self.modified_year.map(|year| year.to_string()).unwrap_or_default())
174            .append_pair("modified__month", &self.modified_year.map(|month| month.to_string()).unwrap_or_default())
175            .append_pair("modified__day", &self.modified_year.map(|day| day.to_string()).unwrap_or_default())
176            .append_pair("modified__date__gt", &self.modified_date_gt.map(|d| d.format("%Y-%m-%dT%H:%M:%SZ").to_string()).unwrap_or_default())
177            .append_pair("modified__gt", &self.modified_gt.map(|d| d.format("%Y-%m-%dT%H:%M:%SZ").to_string()).unwrap_or_default())
178            .append_pair("modified__date__lt", &self.modified_date_lt.map(|d| d.format("%Y-%m-%dT%H:%M:%SZ").to_string()).unwrap_or_default())
179            .append_pair("modified__lt", &self.modified_lt.map(|d| d.format("%Y-%m-%dT%H:%M:%SZ").to_string()).unwrap_or_default())
180            .append_pair("correspondent__isnull",&if let Some(isnull) = self.correspondent_isnull { isnull.to_string() } else { String::default() })
181            .append_pair("correspondent__id__in", &self.correspondent_id_in.map(|ids| ids.iter().map(|id| id.to_string()).collect::<Vec<String>>().join(",")).unwrap_or_default())
182            .append_pair("correspondent__id", &self.correspondent_id.map(|id| id.to_string()).unwrap_or_default())
183            .append_pair("correspondent__name__istartswith", &self.correspondent_name_starts_with.unwrap_or_default())
184            .append_pair("correspondent__name__iendswith", &self.correspondent_name_ends_with.unwrap_or_default())
185            .append_pair("correspondent__name__icontains", &self.correspondent_name_contains.unwrap_or_default())
186            .append_pair("correspondent__name__iexact", &self.correspondent_name_is.unwrap_or_default())
187            .append_pair("tags__id__in", &self.tag_id_in.iter().map(|id| id.to_string()).collect::<Vec<String>>().join(","))
188            .append_pair("tags__id__all", &self.tag_id_all.iter().map(|id| id.to_string()).collect::<Vec<String>>().join(","))
189            .append_pair("tags__id__none", &self.tag_id_none.iter().map(|id| id.to_string()).collect::<Vec<String>>().join(","))
190            .append_pair("tags__id", &self.tag_id.map(|id| id.to_string()).unwrap_or_default())
191            .append_pair("tags__name__istartswith", &self.tag_name_starts_with.unwrap_or_default())
192            .append_pair("tags__name__iendswith", &self.tag_name_ends_with.unwrap_or_default())
193            .append_pair("tags__name__icontains", &self.tag_name_contains.unwrap_or_default())
194            .append_pair("tags__name__iexact", &self.tag_name_is.unwrap_or_default())
195            .append_pair("document_type__isnull",&if let Some(isnull) = self.document_type_isnull { isnull.to_string() } else { String::default() })
196            .append_pair("document_type__id__in", &self.document_type_id_in.iter().map(|id| id.to_string()).collect::<Vec<String>>().join(","))
197            .append_pair("document_type__id", &self.document_type_id.map(|id| id.to_string()).unwrap_or_default())
198            .append_pair("document_type__name__istartswith", &self.document_type_name_starts_with.unwrap_or_default())
199            .append_pair("document_type__name__iendswith", &self.document_type_name_ends_with.unwrap_or_default())
200            .append_pair("document_type__name__icontains", &self.document_type_name_contains.unwrap_or_default())
201            .append_pair("document_type__name__iexact", &self.document_type_name_is.unwrap_or_default())
202            .append_pair("storage_path__isnull",&if let Some(isnull) = self.storage_path_isnull { isnull.to_string() } else { String::default() })
203            .append_pair("storage_path__id__in", &self.storage_path_id_in.iter().map(|id| id.to_string()).collect::<Vec<String>>().join(","))
204            .append_pair("storage_path__id", &self.storage_path_id.map(|id| id.to_string()).unwrap_or_default())
205            .append_pair("storage_path__name__istartswith", &self.storage_path_name_starts_with.unwrap_or_default())
206            .append_pair("storage_path__name__iendswith", &self.storage_path_name_ends_with.unwrap_or_default())
207            .append_pair("storage_path__name__icontains", &self.storage_path_name_contains.unwrap_or_default())
208            .append_pair("storage_path__name__iexact", &self.storage_path_name_is.unwrap_or_default());
209    }
210
211    /// Create a filter from view rules
212    pub fn from_filter_rules(filter_rules: &[saved_view::FilterRule]) -> Self {
213        let mut filter = Self::default();
214        for rule in filter_rules {
215            match rule {
216                saved_view::FilterRule::TitleContains(v) => {
217                    filter.title_contains = v.clone();
218                }
219                saved_view::FilterRule::ContentContains(v) => {
220                    filter.content_contains = v.clone();
221                }
222                saved_view::FilterRule::ASNIs(Some(v)) => {
223                    filter.archive_serial_number_is = Some(v.clone());
224                }
225                saved_view::FilterRule::ASNIs(None) => {
226                    filter.archive_serial_number_isnull = Some(true);
227                }
228                saved_view::FilterRule::CorrespondentIs(Some(v)) => {
229                    filter.correspondent_id = Some(v.clone());
230                }
231                saved_view::FilterRule::CorrespondentIs(None) => {
232                    filter.correspondent_isnull = Some(true);
233                }
234                saved_view::FilterRule::DocumentTypeIs(Some(v)) => {
235                    filter.document_type_id = Some(v.clone());
236                }
237                saved_view::FilterRule::DocumentTypeIs(None) => {
238                    filter.document_type_isnull = Some(true);
239                }
240                saved_view::FilterRule::IsInInbox(v) => {
241                    filter.is_in_inbox = v.clone();
242                }
243                saved_view::FilterRule::HasTag(Some(tag)) => {
244                    filter.tag_id_all.push(tag.clone());
245                }
246                saved_view::FilterRule::HasAnyTag(v) => {
247                    filter.is_tagged = v.clone();
248                }
249                saved_view::FilterRule::CreatedBefore(v) => {
250                    filter.created_lt = v.clone();
251                }
252                saved_view::FilterRule::CreatedAfter(v) => {
253                    filter.created_gt = v.clone();
254                }
255                saved_view::FilterRule::CreatedYearIs(v) => {
256                    filter.created_year = v.clone();
257                }
258                saved_view::FilterRule::CreatedMountIs(v) => {
259                    filter.created_month = v.clone();
260                }
261                saved_view::FilterRule::CreatedDayIs(v) => {
262                    filter.created_day = v.clone();
263                }
264                saved_view::FilterRule::AddedBefore(v) => {
265                    filter.added_lt = v.clone();
266                }
267                saved_view::FilterRule::AddedAfter(v) => {
268                    filter.added_gt = v.clone();
269                }
270                saved_view::FilterRule::ModifiedBefore(v) => {
271                    filter.modified_lt = v.clone();
272                }
273                saved_view::FilterRule::ModifiedAfter(v) => {
274                    filter.modified_gt = v.clone();
275                }
276                saved_view::FilterRule::DontHaveTag(Some(tag)) => {
277                    filter.tag_id_none.push(tag.clone());
278                }
279                saved_view::FilterRule::DontHaveASN(v) => {
280                    filter.archive_serial_number_isnull = v.clone();
281                }
282                saved_view::FilterRule::TitleOrContentContains(v) => {
283                    filter.title_content_contains = v.clone();
284                }
285                saved_view::FilterRule::FullTextQuery(v) => {
286                    filter.query = v.clone();
287                }
288                saved_view::FilterRule::MoreLikeThis(v) => {
289                    filter.more_like = v.clone();
290                }
291                saved_view::FilterRule::HasTagIn(Some(tag)) => {
292                    filter.tag_id_in.push(tag.clone());
293                }
294                saved_view::FilterRule::ASNGreaterThan(v) => {
295                    filter.archive_serial_numer_gt = v.clone();
296                }
297                saved_view::FilterRule::ASNLessThan(v) => {
298                    filter.archive_serial_numer_lt = v.clone();
299                }
300                saved_view::FilterRule::StoragePathIs(Some(v)) => {
301                    filter.storage_path_id = Some(v.clone());
302                }
303                saved_view::FilterRule::StoragePathIs(None) => {
304                    filter.storage_path_isnull = Some(true);
305                }
306                r => {
307                    println!("Ignore {:?}", r)
308                }
309            }
310        }
311        filter
312    }
313}
314
315//https://paperless.joel.rs/api/documents/?more_like_id=&query=&title_content=&is_in_inbox=&title__istartswith=&title__iendswith=&title__icontains=&title__iexact=&content__istartswith=&content__iendswith=&content__icontains=&content__iexact=&archive_serial_number=&archive_serial_number__gt=&archive_serial_number__gte=&archive_serial_number__lt=&archive_serial_number__lte=&archive_serial_number__isnull=&correspondent__isnull=&correspondent__id__in=&correspondent__id=5&correspondent__name__istartswith=&correspondent__name__iendswith=&correspondent__name__icontains=&correspondent__name__iexact=&is_tagged=&tags__id__in=&tags__id__all=&tags__id__none=&tags__id=&tags__name__istartswith=&tags__name__iendswith=&tags__name__icontains=&tags__name__iexact=&document_type__isnull=&document_type__id__in=&document_type__id=&document_type__name__istartswith=&document_type__name__iendswith=&document_type__name__icontains=&document_type__name__iexact=&storage_path__isnull=&storage_path__id__in=&storage_path__id=&storage_path__name__istartswith=&storage_path__name__iendswith=&storage_path__name__icontains=&storage_path__name__iexact=
316//https://paperless.joel.rs/api/documents/?title__istartswith=&title__iendswith=&title__icontains=&title__iexact=&content__istartswith=&content__iendswith=&content__icontains=&content__iexact=&archive_serial_number=&archive_serial_number__gt=&archive_serial_number__gte=&archive_serial_number__lt=&archive_serial_number__lte=&archive_serial_number__isnull=&created__year=&created__month=&created__day=&created__date__gt=&created__gt=&created__date__lt=&created__lt=&added__year=&added__month=&added__day=&added__date__gt=&added__gt=&added__date__lt=&added__lt=&modified__year=&modified__month=&modified__day=&modified__date__gt=&modified__gt=&modified__date__lt=&modified__lt=&correspondent__isnull=&correspondent__id__in=&correspondent__id=5&correspondent__name__istartswith=&correspondent__name__iendswith=&correspondent__name__icontains=&correspondent__name__iexact=&tags__id__in=&tags__id=&tags__name__istartswith=&tags__name__iendswith=&tags__name__icontains=&tags__name__iexact=&document_type__isnull=&document_type__id__in=&document_type__id=&document_type__name__istartswith=&document_type__name__iendswith=&document_type__name__icontains=&document_type__name__iexact=&storage_path__isnull=&storage_path__id__in=&storage_path__id=&storage_path__name__istartswith=&storage_path__name__iendswith=&storage_path__name__icontains=&storage_path__name__iexact=&is_tagged=&tags__id__all=&tags__id__none=&is_in_inbox=&title_content=