1use std::sync::Arc;
2
3use chrono::{DateTime, Utc};
4
5#[cfg(feature = "python")]
6use pyo3::{
7 exceptions::PyValueError,
8 prelude::*,
9 types::{PyDateTime, PyDict},
10};
11
12use serde::{Deserialize, Serialize};
13
14pub use crate::native::common::{
15 Category, Comment, Entry, Export, Field, Form, Reason, State, Value,
16};
17
18use quick_xml::events::BytesStart;
19
20use crate::native::deserializers::{checked_datetime, required_attribute, visit_attributes};
21
22#[cfg(feature = "python")]
23use crate::native::deserializers::to_py_datetime;
24
25#[cfg(not(feature = "python"))]
26#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
27#[serde(rename_all = "camelCase")]
28pub struct Site {
29 #[serde(alias = "@name")]
30 #[serde(alias = "name")]
31 pub name: String,
32 #[serde(rename = "uniqueId")]
33 #[serde(alias = "@uniqueId")]
34 #[serde(alias = "uniqueId")]
35 pub unique_id: String,
36 #[serde(rename = "numberOfPatients")]
37 #[serde(alias = "@numberOfPatients")]
38 #[serde(alias = "numberOfPatients")]
39 pub number_of_patients: usize,
40 #[serde(rename = "countOfRandomizedPatients")]
41 #[serde(alias = "@countOfRandomizedPatients")]
42 #[serde(alias = "countOfRandomizedPatients")]
43 pub count_of_randomized_patients: usize,
44 #[serde(rename = "whenCreated")]
45 #[serde(alias = "@whenCreated")]
46 #[serde(alias = "whenCreated")]
47 pub when_created: Option<DateTime<Utc>>,
48 #[serde(alias = "@creator")]
49 #[serde(alias = "creator")]
50 pub creator: String,
51 #[serde(rename = "numberOfForms")]
52 #[serde(alias = "@numberOfForms")]
53 #[serde(alias = "numberOfForms")]
54 pub number_of_forms: usize,
55
56 #[serde(rename = "form")]
57 #[serde(alias = "form")]
58 pub forms: Option<Arc<Vec<Form>>>,
59}
60
61#[cfg(not(feature = "python"))]
62impl Site {
63 pub(crate) fn from_attributes(e: &BytesStart<'_>) -> Result<Self, crate::errors::Error> {
64 let mut name: Option<&str> = None;
65 let mut unique_id: Option<&str> = None;
66 let mut number_of_patients = "";
67 let mut count_of_randomized_patients = "";
68 let mut when_created = "";
69 let mut creator: Option<&str> = None;
70 let mut number_of_forms = "";
71
72 visit_attributes(e, |key, attr| match key {
73 b"name" => name = Some(attr),
74 b"uniqueId" => unique_id = Some(attr),
75 b"numberOfPatients" => number_of_patients = attr,
76 b"countOfRandomizedPatients" => count_of_randomized_patients = attr,
77 b"whenCreated" => when_created = attr,
78 b"creator" => creator = Some(attr),
79 b"numberOfForms" => number_of_forms = attr,
80 _ => {}
81 })?;
82
83 Ok(Site {
84 name: required_attribute(name, "name")?,
85 unique_id: required_attribute(unique_id, "uniqueId")?,
86 number_of_patients: number_of_patients.parse().unwrap_or(0),
87 count_of_randomized_patients: count_of_randomized_patients.parse().unwrap_or(0),
88 when_created: checked_datetime(when_created)?,
89 creator: required_attribute(creator, "creator")?,
90 number_of_forms: number_of_forms.parse().unwrap_or(0),
91 forms: None,
92 })
93 }
94
95 pub(crate) fn set_forms(&mut self, forms: Vec<Form>) {
96 self.forms = if forms.is_empty() {
97 None
98 } else {
99 Some(Arc::new(forms))
100 };
101 }
102}
103
104#[cfg(feature = "python")]
105#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
106#[serde(rename_all = "camelCase")]
107#[pyclass(skip_from_py_object)]
108pub struct Site {
109 #[serde(alias = "@name")]
110 #[serde(alias = "name")]
111 pub name: String,
112 #[serde(rename = "uniqueId")]
113 #[serde(alias = "@uniqueId")]
114 #[serde(alias = "uniqueId")]
115 pub unique_id: String,
116 #[serde(rename = "numberOfPatients")]
117 #[serde(alias = "@numberOfPatients")]
118 #[serde(alias = "numberOfPatients")]
119 pub number_of_patients: usize,
120 #[serde(rename = "countOfRandomizedPatients")]
121 #[serde(alias = "@countOfRandomizedPatients")]
122 #[serde(alias = "countOfRandomizedPatients")]
123 pub count_of_randomized_patients: usize,
124 #[serde(rename = "whenCreated")]
125 #[serde(alias = "@whenCreated")]
126 #[serde(alias = "whenCreated")]
127 pub when_created: Option<DateTime<Utc>>,
128 #[serde(alias = "@creator")]
129 #[serde(alias = "creator")]
130 pub creator: String,
131 #[serde(rename = "numberOfForms")]
132 #[serde(alias = "@numberOfForms")]
133 #[serde(alias = "numberOfForms")]
134 pub number_of_forms: usize,
135
136 #[serde(rename = "form")]
137 #[serde(alias = "form")]
138 pub forms: Option<Arc<Vec<Form>>>,
139}
140
141#[cfg(feature = "python")]
142impl Site {
143 pub(crate) fn from_attributes(e: &BytesStart<'_>) -> Result<Self, crate::errors::Error> {
144 let mut name: Option<&str> = None;
145 let mut unique_id: Option<&str> = None;
146 let mut number_of_patients = "";
147 let mut count_of_randomized_patients = "";
148 let mut when_created = "";
149 let mut creator: Option<&str> = None;
150 let mut number_of_forms = "";
151
152 visit_attributes(e, |key, attr| match key {
153 b"name" => name = Some(attr),
154 b"uniqueId" => unique_id = Some(attr),
155 b"numberOfPatients" => number_of_patients = attr,
156 b"countOfRandomizedPatients" => count_of_randomized_patients = attr,
157 b"whenCreated" => when_created = attr,
158 b"creator" => creator = Some(attr),
159 b"numberOfForms" => number_of_forms = attr,
160 _ => {}
161 })?;
162
163 Ok(Site {
164 name: required_attribute(name, "name")?,
165 unique_id: required_attribute(unique_id, "uniqueId")?,
166 number_of_patients: number_of_patients.parse().unwrap_or(0),
167 count_of_randomized_patients: count_of_randomized_patients.parse().unwrap_or(0),
168 when_created: checked_datetime(when_created)?,
169 creator: required_attribute(creator, "creator")?,
170 number_of_forms: number_of_forms.parse().unwrap_or(0),
171 forms: None,
172 })
173 }
174
175 pub(crate) fn set_forms(&mut self, forms: Vec<Form>) {
176 self.forms = if forms.is_empty() {
177 None
178 } else {
179 Some(Arc::new(forms))
180 };
181 }
182}
183
184#[cfg(feature = "python")]
185#[pymethods]
186impl Site {
187 #[getter]
188 fn name(&self) -> PyResult<String> {
189 Ok(self.name.clone())
190 }
191
192 #[getter]
193 fn unique_id(&self) -> PyResult<String> {
194 Ok(self.unique_id.clone())
195 }
196
197 #[getter]
198 fn number_of_patients(&self) -> PyResult<usize> {
199 Ok(self.number_of_patients)
200 }
201
202 #[getter]
203 fn count_of_randomized_patients(&self) -> PyResult<usize> {
204 Ok(self.count_of_randomized_patients)
205 }
206
207 #[getter]
208 fn when_created<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
209 self.when_created
210 .as_ref()
211 .map(|dt| to_py_datetime(py, dt))
212 .transpose()
213 }
214
215 #[getter]
216 fn creator(&self) -> PyResult<String> {
217 Ok(self.creator.clone())
218 }
219
220 #[getter]
221 fn number_of_forms(&self) -> PyResult<usize> {
222 Ok(self.number_of_forms)
223 }
224
225 #[getter]
226 fn forms(&self) -> PyResult<Option<Vec<Form>>> {
227 Ok(self.forms.as_deref().cloned())
228 }
229
230 pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
231 let dict = PyDict::new(py);
232 dict.set_item("name", &self.name)?;
233 dict.set_item("unique_id", &self.unique_id)?;
234 dict.set_item("number_of_patients", self.number_of_patients)?;
235 dict.set_item(
236 "count_of_randomized_patients",
237 self.count_of_randomized_patients,
238 )?;
239 dict.set_item(
240 "when_created",
241 self.when_created
242 .as_ref()
243 .map(|dt| to_py_datetime(py, dt))
244 .transpose()?,
245 )?;
246 dict.set_item("creator", &self.creator)?;
247 dict.set_item("number_of_forms", self.number_of_forms)?;
248
249 let mut form_dicts = Vec::new();
250 if let Some(forms) = &self.forms {
251 for form in forms.iter() {
252 let form_dict = form.to_dict(py)?;
253 form_dicts.push(form_dict);
254 }
255 dict.set_item("forms", form_dicts)?;
256 } else {
257 dict.set_item("forms", py.None())?;
258 }
259
260 Ok(dict)
261 }
262}
263
264#[cfg(not(feature = "python"))]
265#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
267#[serde(rename_all = "camelCase")]
268pub struct SiteNative {
269 #[serde(default)]
270 pub export: Option<Export>,
271
272 #[serde(alias = "site")]
273 pub sites: Vec<Site>,
274}
275
276#[cfg(not(feature = "python"))]
277impl SiteNative {
278 pub fn to_json(&self) -> serde_json::Result<String> {
297 let json = serde_json::to_string(&self)?;
298
299 Ok(json)
300 }
301}
302
303#[cfg(feature = "python")]
304#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
306#[serde(rename_all = "camelCase")]
307#[pyclass(get_all, skip_from_py_object)]
308pub struct SiteNative {
309 #[serde(default)]
310 pub export: Option<Export>,
311
312 #[serde(alias = "site")]
313 pub sites: Vec<Site>,
314}
315
316#[cfg(feature = "python")]
317#[pymethods]
318impl SiteNative {
319 #[getter]
320 fn export(&self) -> PyResult<Option<Export>> {
321 Ok(self.export.clone())
322 }
323
324 #[getter]
325 fn sites(&self) -> PyResult<Vec<Site>> {
326 Ok(self.sites.clone())
327 }
328
329 fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
331 let dict = PyDict::new(py);
332 let mut site_dicts = Vec::new();
333 for site in &self.sites {
334 let site_dict = site.to_dict(py)?;
335 site_dicts.push(site_dict);
336 }
337 match &self.export {
338 Some(export) => dict.set_item("export", export.to_dict(py)?)?,
339 None => dict.set_item("export", py.None())?,
340 }
341 dict.set_item("sites", site_dicts)?;
342 Ok(dict)
343 }
344
345 fn to_json(&self) -> PyResult<String> {
347 serde_json::to_string(&self)
348 .map_err(|_| PyErr::new::<PyValueError, _>("Error converting to JSON"))
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use insta::assert_yaml_snapshot;
356
357 #[test]
358 fn deserialize_site_native_json() {
359 let json_str = r#"{
360 "sites": [
361 {
362 "name": "Some Site",
363 "uniqueId": "1681574834910",
364 "numberOfPatients": 4,
365 "countOfRandomizedPatients": 0,
366 "whenCreated": "2023-04-15T16:08:19Z",
367 "creator": "Paul Sanders",
368 "numberOfForms": 1,
369 "forms": [
370 {
371 "name": "demographic.form.name.site.demographics",
372 "lastModified": "2023-04-15T16:08:19Z",
373 "whoLastModifiedName": "Paul Sanders",
374 "whoLastModifiedRole": "Project Manager",
375 "whenCreated": 1681574834930,
376 "hasErrors": false,
377 "hasWarnings": false,
378 "locked": false,
379 "user": null,
380 "dateTimeChanged": null,
381 "formTitle": "Site Demographics",
382 "formIndex": 1,
383 "formGroup": "Demographic",
384 "formState": "In-Work",
385 "states": [
386 {
387 "value": "form.state.in.work",
388 "signer": "Paul Sanders - Project Manager",
389 "signerUniqueId": "1681162687395",
390 "dateSigned": "2023-04-15T16:08:19Z"
391 }
392 ],
393 "categories": [
394 {
395 "name": "Demographics",
396 "categoryType": "normal",
397 "highestIndex": 0,
398 "fields": [
399 {
400 "name": "address",
401 "fieldType": "text",
402 "dataType": "string",
403 "errorCode": "valid",
404 "whenCreated": "2023-04-15T16:07:14Z",
405 "keepHistory": true,
406 "entry": null
407 },
408 {
409 "name": "company",
410 "fieldType": "text",
411 "dataType": "string",
412 "errorCode": "valid",
413 "whenCreated": "2023-04-15T16:07:14Z",
414 "keepHistory": true,
415 "entries": [
416 {
417 "entryId": "1",
418 "value": {
419 "by": "Paul Sanders",
420 "byUniqueId": "1681162687395",
421 "role": "Project Manager",
422 "when": "2023-04-15T16:08:19Z",
423 "value": "Some Company"
424 },
425 "reason": null
426 }
427 ]
428 },
429 {
430 "name": "site_code_name",
431 "fieldType": "hidden",
432 "dataType": "string",
433 "errorCode": "valid",
434 "whenCreated": "2023-04-15T16:07:14Z",
435 "keepHistory": true,
436 "entry": [
437 {
438 "entryId": "1",
439 "value": {
440 "by": "set from calculation",
441 "byUniqueId": null,
442 "role": "System",
443 "when": "2023-04-15T16:08:19Z",
444 "value": "ABC-Some Site"
445 },
446 "reason": {
447 "by": "set from calculation",
448 "byUniqueId": null,
449 "role": "System",
450 "when": "2023-04-15T16:08:19Z",
451 "value": "calculated value"
452 }
453 },
454 {
455 "entryId": "2",
456 "value": {
457 "by": "set from calculation",
458 "byUniqueId": null,
459 "role": "System",
460 "when": "2023-04-15T16:07:24Z",
461 "value": "Some Site"
462 },
463 "reason": {
464 "by": "set from calculation",
465 "byUniqueId": null,
466 "role": "System",
467 "when": "2023-04-15T16:07:24Z",
468 "value": "calculated value"
469 }
470 }
471 ]
472 }
473 ]
474 },
475 {
476 "name": "Enrollment",
477 "categoryType": "normal",
478 "highestIndex": 0,
479 "field": [
480 {
481 "name": "enrollment_closed_date",
482 "fieldType": "popUpCalendar",
483 "dataType": "date",
484 "errorCode": "valid",
485 "whenCreated": "2023-04-15T16:07:14Z",
486 "keepHistory": true,
487 "entry": null
488 },
489 {
490 "name": "enrollment_open",
491 "fieldType": "radio",
492 "dataType": "string",
493 "errorCode": "valid",
494 "whenCreated": "2023-04-15T16:07:14Z",
495 "keepHistory": true,
496 "entry": [
497 {
498 "entryId": "1",
499 "value": {
500 "by": "Paul Sanders",
501 "byUniqueId": "1681162687395",
502 "role": "Project Manager",
503 "when": "2023-04-15T16:08:19Z",
504 "value": "Yes"
505 },
506 "reason": null
507 }
508 ]
509 },
510 {
511 "name": "enrollment_open_date",
512 "fieldType": "popUpCalendar",
513 "dataType": "date",
514 "errorCode": "valid",
515 "whenCreated": "2023-04-15T16:07:14Z",
516 "keepHistory": true,
517 "entry": null
518 }
519 ]
520 }
521 ]
522 }
523 ]
524 },
525 {
526 "name": "Artemis",
527 "uniqueId": "1691420994591",
528 "numberOfPatients": 0,
529 "countOfRandomizedPatients": 0,
530 "whenCreated": "2023-08-07T15:14:23Z",
531 "creator": "Paul Sanders",
532 "numberOfForms": 1,
533 "forms": [
534 {
535 "name": "demographic.form.name.site.demographics",
536 "lastModified": "2023-08-07T15:14:23Z",
537 "whoLastModifiedName": "Paul Sanders",
538 "whoLastModifiedRole": "Project Manager",
539 "whenCreated": 1691420994611,
540 "hasErrors": false,
541 "hasWarnings": false,
542 "locked": false,
543 "user": null,
544 "dateTimeChanged": null,
545 "formTitle": "Site Demographics",
546 "formIndex": 1,
547 "formGroup": "Demographic",
548 "formState": "In-Work",
549 "states": [
550 {
551 "value": "form.state.in.work",
552 "signer": "Paul Sanders - Project Manager",
553 "signerUniqueId": "1681162687395",
554 "dateSigned": "2023-08-07T15:14:23Z"
555 }
556 ],
557 "categories": [
558 {
559 "name": "Demographics",
560 "categoryType": "normal",
561 "highestIndex": 0,
562 "fields": [
563 {
564 "name": "address",
565 "fieldType": "text",
566 "dataType": "string",
567 "errorCode": "valid",
568 "whenCreated": "2023-08-07T15:09:54Z",
569 "keepHistory": true,
570 "entries": [
571 {
572 "entryId": "1",
573 "value": {
574 "by": "Paul Sanders",
575 "byUniqueId": "1681162687395",
576 "role": "Project Manager",
577 "when": "2023-08-07T15:14:21Z",
578 "value": "1111 Moon Drive"
579 },
580 "reason": null
581 }
582 ]
583 }
584 ]
585 }
586 ]
587 }
588 ]
589 }
590 ]
591}
592 "#;
593
594 let result: SiteNative = serde_json::from_str(json_str).unwrap();
595
596 assert_yaml_snapshot!(result);
597 }
598}