1use chrono::{DateTime, Utc};
2
3#[cfg(feature = "python")]
4use pyo3::{
5 exceptions::PyValueError,
6 prelude::*,
7 types::{PyDateTime, PyDict},
8};
9
10use serde::{Deserialize, Serialize};
11
12pub use crate::native::common::{Category, Comment, Entry, Field, Form, Reason, State, Value};
13
14#[cfg(feature = "python")]
15use crate::native::deserializers::to_py_datetime;
16
17#[cfg(not(feature = "python"))]
18#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
19#[serde(rename_all = "camelCase")]
20pub struct Site {
21 #[serde(alias = "@name")]
22 #[serde(alias = "name")]
23 pub name: String,
24 #[serde(rename = "uniqueId")]
25 #[serde(alias = "@uniqueId")]
26 #[serde(alias = "uniqueId")]
27 pub unique_id: String,
28 #[serde(rename = "numberOfPatients")]
29 #[serde(alias = "@numberOfPatients")]
30 #[serde(alias = "numberOfPatients")]
31 pub number_of_patients: usize,
32 #[serde(rename = "countOfRandomizedPatients")]
33 #[serde(alias = "@countOfRandomizedPatients")]
34 #[serde(alias = "countOfRandomizedPatients")]
35 pub count_of_randomized_patients: usize,
36 #[serde(rename = "whenCreated")]
37 #[serde(alias = "@whenCreated")]
38 #[serde(alias = "whenCreated")]
39 pub when_created: Option<DateTime<Utc>>,
40 #[serde(alias = "@creator")]
41 #[serde(alias = "creator")]
42 pub creator: String,
43 #[serde(rename = "numberOfForms")]
44 #[serde(alias = "@numberOfForms")]
45 #[serde(alias = "numberOfForms")]
46 pub number_of_forms: usize,
47
48 #[serde(rename = "form")]
49 #[serde(alias = "form")]
50 pub forms: Option<Vec<Form>>,
51}
52
53#[cfg(feature = "python")]
54#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
55#[serde(rename_all = "camelCase")]
56#[pyclass]
57pub struct Site {
58 #[serde(alias = "@name")]
59 #[serde(alias = "name")]
60 pub name: String,
61 #[serde(rename = "uniqueId")]
62 #[serde(alias = "@uniqueId")]
63 #[serde(alias = "uniqueId")]
64 pub unique_id: String,
65 #[serde(rename = "numberOfPatients")]
66 #[serde(alias = "@numberOfPatients")]
67 #[serde(alias = "numberOfPatients")]
68 pub number_of_patients: usize,
69 #[serde(rename = "countOfRandomizedPatients")]
70 #[serde(alias = "@countOfRandomizedPatients")]
71 #[serde(alias = "countOfRandomizedPatients")]
72 pub count_of_randomized_patients: usize,
73 #[serde(rename = "whenCreated")]
74 #[serde(alias = "@whenCreated")]
75 #[serde(alias = "whenCreated")]
76 pub when_created: Option<DateTime<Utc>>,
77 #[serde(alias = "@creator")]
78 #[serde(alias = "creator")]
79 pub creator: String,
80 #[serde(rename = "numberOfForms")]
81 #[serde(alias = "@numberOfForms")]
82 #[serde(alias = "numberOfForms")]
83 pub number_of_forms: usize,
84
85 #[serde(rename = "form")]
86 #[serde(alias = "form")]
87 pub forms: Option<Vec<Form>>,
88}
89
90#[cfg(feature = "python")]
91#[pymethods]
92impl Site {
93 #[getter]
94 fn name(&self) -> PyResult<String> {
95 Ok(self.name.clone())
96 }
97
98 #[getter]
99 fn unique_id(&self) -> PyResult<String> {
100 Ok(self.unique_id.clone())
101 }
102
103 #[getter]
104 fn number_of_patients(&self) -> PyResult<usize> {
105 Ok(self.number_of_patients)
106 }
107
108 #[getter]
109 fn count_of_randomized_patients(&self) -> PyResult<usize> {
110 Ok(self.count_of_randomized_patients)
111 }
112
113 #[getter]
114 fn when_created<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
115 self.when_created
116 .as_ref()
117 .map(|dt| to_py_datetime(py, dt))
118 .transpose()
119 }
120
121 #[getter]
122 fn creator(&self) -> PyResult<String> {
123 Ok(self.creator.clone())
124 }
125
126 #[getter]
127 fn number_of_forms(&self) -> PyResult<usize> {
128 Ok(self.number_of_forms)
129 }
130
131 #[getter]
132 fn forms(&self) -> PyResult<Option<Vec<Form>>> {
133 Ok(self.forms.clone())
134 }
135
136 pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
137 let dict = PyDict::new(py);
138 dict.set_item("name", &self.name)?;
139 dict.set_item("unique_id", &self.unique_id)?;
140 dict.set_item("number_of_patients", self.number_of_patients)?;
141 dict.set_item(
142 "count_of_randomized_patients",
143 self.count_of_randomized_patients,
144 )?;
145 dict.set_item(
146 "when_created",
147 self.when_created
148 .as_ref()
149 .map(|dt| to_py_datetime(py, dt))
150 .transpose()?,
151 )?;
152 dict.set_item("creator", &self.creator)?;
153 dict.set_item("number_of_forms", self.number_of_forms)?;
154
155 let mut form_dicts = Vec::new();
156 if let Some(forms) = &self.forms {
157 for form in forms {
158 let form_dict = form.to_dict(py)?;
159 form_dicts.push(form_dict);
160 }
161 dict.set_item("forms", form_dicts)?;
162 } else {
163 dict.set_item("forms", py.None())?;
164 }
165
166 Ok(dict)
167 }
168}
169
170#[cfg(not(feature = "python"))]
171#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
173#[serde(rename_all = "camelCase")]
174pub struct SiteNative {
175 #[serde(alias = "site")]
176 pub sites: Vec<Site>,
177}
178
179#[cfg(not(feature = "python"))]
180impl SiteNative {
181 pub fn to_json(&self) -> serde_json::Result<String> {
200 let json = serde_json::to_string(&self)?;
201
202 Ok(json)
203 }
204}
205
206#[cfg(feature = "python")]
207#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
209#[serde(rename_all = "camelCase")]
210#[pyclass(get_all)]
211pub struct SiteNative {
212 #[serde(alias = "site")]
213 pub sites: Vec<Site>,
214}
215
216#[cfg(feature = "python")]
217#[pymethods]
218impl SiteNative {
219 #[getter]
220 fn sites(&self) -> PyResult<Vec<Site>> {
221 Ok(self.sites.clone())
222 }
223
224 fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
226 let dict = PyDict::new(py);
227 let mut site_dicts = Vec::new();
228 for site in &self.sites {
229 let site_dict = site.to_dict(py)?;
230 site_dicts.push(site_dict);
231 }
232 dict.set_item("sites", site_dicts)?;
233 Ok(dict)
234 }
235
236 fn to_json(&self) -> PyResult<String> {
238 serde_json::to_string(&self)
239 .map_err(|_| PyErr::new::<PyValueError, _>("Error converting to JSON"))
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use insta::assert_yaml_snapshot;
247
248 #[test]
249 fn deserialize_site_native_json() {
250 let json_str = r#"{
251 "sites": [
252 {
253 "name": "Some Site",
254 "uniqueId": "1681574834910",
255 "numberOfPatients": 4,
256 "countOfRandomizedPatients": 0,
257 "whenCreated": "2023-04-15T16:08:19Z",
258 "creator": "Paul Sanders",
259 "numberOfForms": 1,
260 "forms": [
261 {
262 "name": "demographic.form.name.site.demographics",
263 "lastModified": "2023-04-15T16:08:19Z",
264 "whoLastModifiedName": "Paul Sanders",
265 "whoLastModifiedRole": "Project Manager",
266 "whenCreated": 1681574834930,
267 "hasErrors": false,
268 "hasWarnings": false,
269 "locked": false,
270 "user": null,
271 "dateTimeChanged": null,
272 "formTitle": "Site Demographics",
273 "formIndex": 1,
274 "formGroup": "Demographic",
275 "formState": "In-Work",
276 "states": [
277 {
278 "value": "form.state.in.work",
279 "signer": "Paul Sanders - Project Manager",
280 "signerUniqueId": "1681162687395",
281 "dateSigned": "2023-04-15T16:08:19Z"
282 }
283 ],
284 "categories": [
285 {
286 "name": "Demographics",
287 "categoryType": "normal",
288 "highestIndex": 0,
289 "fields": [
290 {
291 "name": "address",
292 "fieldType": "text",
293 "dataType": "string",
294 "errorCode": "valid",
295 "whenCreated": "2023-04-15T16:07:14Z",
296 "keepHistory": true,
297 "entry": null
298 },
299 {
300 "name": "company",
301 "fieldType": "text",
302 "dataType": "string",
303 "errorCode": "valid",
304 "whenCreated": "2023-04-15T16:07:14Z",
305 "keepHistory": true,
306 "entries": [
307 {
308 "entryId": "1",
309 "value": {
310 "by": "Paul Sanders",
311 "byUniqueId": "1681162687395",
312 "role": "Project Manager",
313 "when": "2023-04-15T16:08:19Z",
314 "value": "Some Company"
315 },
316 "reason": null
317 }
318 ]
319 },
320 {
321 "name": "site_code_name",
322 "fieldType": "hidden",
323 "dataType": "string",
324 "errorCode": "valid",
325 "whenCreated": "2023-04-15T16:07:14Z",
326 "keepHistory": true,
327 "entry": [
328 {
329 "entryId": "1",
330 "value": {
331 "by": "set from calculation",
332 "byUniqueId": null,
333 "role": "System",
334 "when": "2023-04-15T16:08:19Z",
335 "value": "ABC-Some Site"
336 },
337 "reason": {
338 "by": "set from calculation",
339 "byUniqueId": null,
340 "role": "System",
341 "when": "2023-04-15T16:08:19Z",
342 "value": "calculated value"
343 }
344 },
345 {
346 "entryId": "2",
347 "value": {
348 "by": "set from calculation",
349 "byUniqueId": null,
350 "role": "System",
351 "when": "2023-04-15T16:07:24Z",
352 "value": "Some Site"
353 },
354 "reason": {
355 "by": "set from calculation",
356 "byUniqueId": null,
357 "role": "System",
358 "when": "2023-04-15T16:07:24Z",
359 "value": "calculated value"
360 }
361 }
362 ]
363 }
364 ]
365 },
366 {
367 "name": "Enrollment",
368 "categoryType": "normal",
369 "highestIndex": 0,
370 "field": [
371 {
372 "name": "enrollment_closed_date",
373 "fieldType": "popUpCalendar",
374 "dataType": "date",
375 "errorCode": "valid",
376 "whenCreated": "2023-04-15T16:07:14Z",
377 "keepHistory": true,
378 "entry": null
379 },
380 {
381 "name": "enrollment_open",
382 "fieldType": "radio",
383 "dataType": "string",
384 "errorCode": "valid",
385 "whenCreated": "2023-04-15T16:07:14Z",
386 "keepHistory": true,
387 "entry": [
388 {
389 "entryId": "1",
390 "value": {
391 "by": "Paul Sanders",
392 "byUniqueId": "1681162687395",
393 "role": "Project Manager",
394 "when": "2023-04-15T16:08:19Z",
395 "value": "Yes"
396 },
397 "reason": null
398 }
399 ]
400 },
401 {
402 "name": "enrollment_open_date",
403 "fieldType": "popUpCalendar",
404 "dataType": "date",
405 "errorCode": "valid",
406 "whenCreated": "2023-04-15T16:07:14Z",
407 "keepHistory": true,
408 "entry": null
409 }
410 ]
411 }
412 ]
413 }
414 ]
415 },
416 {
417 "name": "Artemis",
418 "uniqueId": "1691420994591",
419 "numberOfPatients": 0,
420 "countOfRandomizedPatients": 0,
421 "whenCreated": "2023-08-07T15:14:23Z",
422 "creator": "Paul Sanders",
423 "numberOfForms": 1,
424 "forms": [
425 {
426 "name": "demographic.form.name.site.demographics",
427 "lastModified": "2023-08-07T15:14:23Z",
428 "whoLastModifiedName": "Paul Sanders",
429 "whoLastModifiedRole": "Project Manager",
430 "whenCreated": 1691420994611,
431 "hasErrors": false,
432 "hasWarnings": false,
433 "locked": false,
434 "user": null,
435 "dateTimeChanged": null,
436 "formTitle": "Site Demographics",
437 "formIndex": 1,
438 "formGroup": "Demographic",
439 "formState": "In-Work",
440 "states": [
441 {
442 "value": "form.state.in.work",
443 "signer": "Paul Sanders - Project Manager",
444 "signerUniqueId": "1681162687395",
445 "dateSigned": "2023-08-07T15:14:23Z"
446 }
447 ],
448 "categories": [
449 {
450 "name": "Demographics",
451 "categoryType": "normal",
452 "highestIndex": 0,
453 "fields": [
454 {
455 "name": "address",
456 "fieldType": "text",
457 "dataType": "string",
458 "errorCode": "valid",
459 "whenCreated": "2023-08-07T15:09:54Z",
460 "keepHistory": true,
461 "entries": [
462 {
463 "entryId": "1",
464 "value": {
465 "by": "Paul Sanders",
466 "byUniqueId": "1681162687395",
467 "role": "Project Manager",
468 "when": "2023-08-07T15:14:21Z",
469 "value": "1111 Moon Drive"
470 },
471 "reason": null
472 }
473 ]
474 }
475 ]
476 }
477 ]
478 }
479 ]
480 }
481 ]
482}
483 "#;
484
485 let result: SiteNative = serde_json::from_str(json_str).unwrap();
486
487 assert_yaml_snapshot!(result);
488 }
489}