redmine_api/api/
projects.rs

1//! Projects Rest API Endpoint definitions
2//!
3//! [`Redmine Documentation`](https://www.redmine.org/projects/redmine/wiki/Rest_Projects)
4//!
5//! - [x] all projects endpoint
6//! - [x] specific project endpoint
7//! - [x] create project endpoint
8//! - [x] update project endpoint
9//! - [x] archive project endpoint
10//! - [x] unarchive project endpoint
11//! - [x] delete project endpoint
12
13use derive_builder::Builder;
14use reqwest::Method;
15use std::borrow::Cow;
16
17use crate::api::custom_fields::CustomFieldName;
18use crate::api::enumerations::TimeEntryActivityEssentials;
19use crate::api::issue_categories::IssueCategoryEssentials;
20use crate::api::issues::AssigneeEssentials;
21use crate::api::trackers::TrackerEssentials;
22use crate::api::versions::VersionEssentials;
23use crate::api::{Endpoint, NoPagination, Pageable, QueryParams, ReturnsJsonResponse};
24use serde::Serialize;
25use std::collections::HashMap;
26
27/// a minimal type for Redmine modules used in lists enabled modules
28#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
29pub struct Module {
30    /// numeric id
31    pub id: u64,
32    /// name (all lower-case and with underscores so probably not meant for display purposes)
33    pub name: String,
34}
35
36/// a minimal type for Redmine projects used in lists of projects included in
37/// other Redmine objects (e.g. custom fields)
38#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
39pub struct ProjectEssentials {
40    /// numeric id
41    pub id: u64,
42    /// display name
43    pub name: String,
44}
45
46impl From<Project> for ProjectEssentials {
47    fn from(v: Project) -> Self {
48        ProjectEssentials {
49            id: v.id,
50            name: v.name,
51        }
52    }
53}
54
55impl From<&Project> for ProjectEssentials {
56    fn from(v: &Project) -> Self {
57        ProjectEssentials {
58            id: v.id,
59            name: v.name.to_owned(),
60        }
61    }
62}
63
64/// a type for projects to use as an API return type
65///
66/// alternatively you can use your own type limited to the fields you need
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
68pub struct Project {
69    /// numeric id
70    pub id: u64,
71    /// display name
72    pub name: String,
73    /// URL slug
74    pub identifier: String,
75    /// description
76    pub description: Option<String>,
77    /// the project homepage
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub homepage: Option<String>,
80    /// is the project public (visible to anonymous users)
81    pub is_public: Option<bool>,
82    /// the parent project (id and name)
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub parent: Option<ProjectEssentials>,
85    /// will the project inherit members from its ancestors
86    pub inherit_members: Option<bool>,
87    /// the default user/group issues in this project are assigned to
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub default_assignee: Option<AssigneeEssentials>,
90    /// the default version for issues in this project
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub default_version: Option<VersionEssentials>,
93    /// ID of the default version. It works only with existing shared versions
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub default_version_id: Option<u64>,
96    /// trackers to enable in the project
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub tracker_ids: Option<Vec<u64>>,
99    /// modules to enable in the project
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub enabled_module_names: Option<Vec<String>>,
102    /// custom issue fields to enable in the project
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub issue_custom_field_id: Option<Vec<u64>>,
105    /// values for custom fields
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub custom_field_values: Option<HashMap<u64, String>>,
108    /// archived or not?
109    pub status: u64,
110    /// The time when this project was created
111    #[serde(
112        serialize_with = "crate::api::serialize_rfc3339",
113        deserialize_with = "crate::api::deserialize_rfc3339"
114    )]
115    pub created_on: time::OffsetDateTime,
116    /// The time when this project was last updated
117    #[serde(
118        serialize_with = "crate::api::serialize_rfc3339",
119        deserialize_with = "crate::api::deserialize_rfc3339"
120    )]
121    pub updated_on: time::OffsetDateTime,
122    /// issue categories (only with include parameter)
123    #[serde(default, skip_serializing_if = "Option::is_none")]
124    pub issue_categories: Option<Vec<IssueCategoryEssentials>>,
125    /// time entry activities (only with include parameter)
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub time_entry_activities: Option<Vec<TimeEntryActivityEssentials>>,
128    /// enabled modules in this project (only with include parameter)
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub enabled_modules: Option<Vec<Module>>,
131    /// trackers in this project (only with include parameter)
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub trackers: Option<Vec<TrackerEssentials>>,
134    /// custom field ids and names in this project (only with include parameter)
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub issue_custom_fields: Option<Vec<CustomFieldName>>,
137}
138
139/// The types of associated data which can be fetched along with a project
140#[derive(Debug, Clone)]
141pub enum ProjectsInclude {
142    /// Trackers enabled in the project
143    Trackers,
144    /// Issue categories in the project
145    IssueCategories,
146    /// Redmine Modules enabled in the project
147    EnabledModules,
148    /// Time entry activities for the project
149    TimeEntryActivities,
150    /// Issue custom fields for the project
151    IssueCustomFields,
152}
153
154impl std::fmt::Display for ProjectsInclude {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        match self {
157            Self::Trackers => {
158                write!(f, "trackers")
159            }
160            Self::IssueCategories => {
161                write!(f, "issue_categories")
162            }
163            Self::EnabledModules => {
164                write!(f, "enabled_modules")
165            }
166            Self::TimeEntryActivities => {
167                write!(f, "time_entry_activities")
168            }
169            Self::IssueCustomFields => {
170                write!(f, "issue_custom_fields")
171            }
172        }
173    }
174}
175
176/// The endpoint for all Redmine projects
177#[derive(Debug, Clone, Builder)]
178#[builder(setter(strip_option))]
179pub struct ListProjects {
180    /// the types of associate data to include
181    #[builder(default)]
182    include: Option<Vec<ProjectsInclude>>,
183}
184
185impl ReturnsJsonResponse for ListProjects {}
186impl Pageable for ListProjects {
187    fn response_wrapper_key(&self) -> String {
188        "projects".to_string()
189    }
190}
191
192impl ListProjects {
193    /// Create a builder for the endpoint.
194    #[must_use]
195    pub fn builder() -> ListProjectsBuilder {
196        ListProjectsBuilder::default()
197    }
198}
199
200impl Endpoint for ListProjects {
201    fn method(&self) -> Method {
202        Method::GET
203    }
204
205    fn endpoint(&self) -> Cow<'static, str> {
206        "projects.json".into()
207    }
208
209    fn parameters(&self) -> QueryParams {
210        let mut params = QueryParams::default();
211        params.push_opt("include", self.include.as_ref());
212        params
213    }
214}
215
216/// The types of associated data which can be fetched along with a project
217#[derive(Debug, Clone)]
218pub enum ProjectInclude {
219    /// Trackers enabled in the project
220    Trackers,
221    /// Issue categories in the project
222    IssueCategories,
223    /// Redmine Modules enabled in the project
224    EnabledModules,
225    /// Time Entry Activities enabled in the project
226    TimeEntryActivities,
227    /// Issue custom fields for the project
228    IssueCustomFields,
229}
230
231impl std::fmt::Display for ProjectInclude {
232    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233        match self {
234            Self::Trackers => {
235                write!(f, "trackers")
236            }
237            Self::IssueCategories => {
238                write!(f, "issue_categories")
239            }
240            Self::EnabledModules => {
241                write!(f, "enabled_modules")
242            }
243            Self::TimeEntryActivities => {
244                write!(f, "time_entry_activities")
245            }
246            Self::IssueCustomFields => {
247                write!(f, "issue_custom_fields")
248            }
249        }
250    }
251}
252
253/// The endpoint for a specific Redmine project
254#[derive(Debug, Clone, Builder)]
255#[builder(setter(strip_option))]
256pub struct GetProject<'a> {
257    /// the project id or name as it appears in the URL
258    #[builder(setter(into))]
259    project_id_or_name: Cow<'a, str>,
260    /// the types of associate data to include
261    #[builder(default)]
262    include: Option<Vec<ProjectInclude>>,
263}
264
265impl ReturnsJsonResponse for GetProject<'_> {}
266impl NoPagination for GetProject<'_> {}
267
268impl<'a> GetProject<'a> {
269    /// Create a builder for the endpoint.
270    #[must_use]
271    pub fn builder() -> GetProjectBuilder<'a> {
272        GetProjectBuilder::default()
273    }
274}
275
276impl Endpoint for GetProject<'_> {
277    fn method(&self) -> Method {
278        Method::GET
279    }
280
281    fn endpoint(&self) -> Cow<'static, str> {
282        format!("projects/{}.json", &self.project_id_or_name).into()
283    }
284
285    fn parameters(&self) -> QueryParams {
286        let mut params = QueryParams::default();
287        params.push_opt("include", self.include.as_ref());
288        params
289    }
290}
291
292/// The endpoint to archive a Redmine project
293#[derive(Debug, Clone, Builder)]
294#[builder(setter(strip_option))]
295pub struct ArchiveProject<'a> {
296    /// the project id or name as it appears in the URL of the project to archive
297    #[builder(setter(into))]
298    project_id_or_name: Cow<'a, str>,
299}
300
301impl<'a> ArchiveProject<'a> {
302    /// Create a builder for the endpoint.
303    #[must_use]
304    pub fn builder() -> ArchiveProjectBuilder<'a> {
305        ArchiveProjectBuilder::default()
306    }
307}
308
309impl Endpoint for ArchiveProject<'_> {
310    fn method(&self) -> Method {
311        Method::PUT
312    }
313
314    fn endpoint(&self) -> Cow<'static, str> {
315        format!("projects/{}/archive.json", &self.project_id_or_name).into()
316    }
317}
318
319/// The endpoint to unarchive a Redmine project
320#[derive(Debug, Clone, Builder)]
321#[builder(setter(strip_option))]
322pub struct UnarchiveProject<'a> {
323    /// the project id or name as it appears in the URL of the project to unarchive
324    #[builder(setter(into))]
325    project_id_or_name: Cow<'a, str>,
326}
327
328impl<'a> UnarchiveProject<'a> {
329    /// Create a builder for the endpoint.
330    #[must_use]
331    pub fn builder() -> UnarchiveProjectBuilder<'a> {
332        UnarchiveProjectBuilder::default()
333    }
334}
335
336impl Endpoint for UnarchiveProject<'_> {
337    fn method(&self) -> Method {
338        Method::PUT
339    }
340
341    fn endpoint(&self) -> Cow<'static, str> {
342        format!("projects/{}/unarchive.json", &self.project_id_or_name).into()
343    }
344}
345
346/// The endpoint to create a Redmine project
347#[serde_with::skip_serializing_none]
348#[derive(Debug, Clone, Builder, Serialize)]
349#[builder(setter(strip_option))]
350pub struct CreateProject<'a> {
351    /// the name of the project
352    #[builder(setter(into))]
353    name: Cow<'a, str>,
354    /// the identifier of the project as it appears in the URL
355    #[builder(setter(into))]
356    identifier: Cow<'a, str>,
357    /// the project description
358    #[builder(setter(into), default)]
359    description: Option<Cow<'a, str>>,
360    /// the project homepage
361    #[builder(setter(into), default)]
362    homepage: Option<Cow<'a, str>>,
363    /// is the project public (visible to anonymous users)
364    #[builder(default)]
365    is_public: Option<bool>,
366    /// the parent project id
367    #[builder(default)]
368    parent_id: Option<u64>,
369    /// will the project inherit members from its ancestors
370    #[builder(default)]
371    inherit_members: Option<bool>,
372    /// ID of the default user. It works only when the new project is a subproject and it inherits the members
373    #[builder(default)]
374    default_assigned_to_id: Option<u64>,
375    /// ID of the default version. It works only with existing shared versions
376    #[builder(default)]
377    default_version_id: Option<u64>,
378    /// trackers to enable in the project
379    #[builder(default)]
380    tracker_ids: Option<Vec<u64>>,
381    /// modules to enable in the project
382    #[builder(default)]
383    enabled_module_names: Option<Vec<Cow<'a, str>>>,
384    /// custom issue fields to enable in the project
385    #[builder(default)]
386    issue_custom_field_id: Option<Vec<u64>>,
387    /// values for custom fields
388    #[builder(default)]
389    custom_field_values: Option<HashMap<u64, Cow<'a, str>>>,
390}
391
392impl ReturnsJsonResponse for CreateProject<'_> {}
393impl NoPagination for CreateProject<'_> {}
394
395impl<'a> CreateProject<'a> {
396    /// Create a builder for the endpoint.
397    #[must_use]
398    pub fn builder() -> CreateProjectBuilder<'a> {
399        CreateProjectBuilder::default()
400    }
401}
402
403impl Endpoint for CreateProject<'_> {
404    fn method(&self) -> Method {
405        Method::POST
406    }
407
408    fn endpoint(&self) -> Cow<'static, str> {
409        "projects.json".into()
410    }
411
412    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
413        Ok(Some((
414            "application/json",
415            serde_json::to_vec(&ProjectWrapper::<CreateProject> {
416                project: (*self).to_owned(),
417            })?,
418        )))
419    }
420}
421
422/// The endpoint to update an existing Redmine project
423#[serde_with::skip_serializing_none]
424#[derive(Debug, Clone, Builder, Serialize)]
425#[builder(setter(strip_option))]
426pub struct UpdateProject<'a> {
427    /// the project id or name as it appears in the URL of the project to update
428    #[serde(skip_serializing)]
429    #[builder(setter(into))]
430    project_id_or_name: Cow<'a, str>,
431    /// the name of the project
432    #[builder(setter(into), default)]
433    name: Option<Cow<'a, str>>,
434    /// the identifier of the project as it appears in the URL
435    #[builder(setter(into), default)]
436    identifier: Option<Cow<'a, str>>,
437    /// the project description
438    #[builder(setter(into), default)]
439    description: Option<Cow<'a, str>>,
440    /// the project homepage
441    #[builder(setter(into), default)]
442    homepage: Option<Cow<'a, str>>,
443    /// is the project public (visible to anonymous users)
444    #[builder(default)]
445    is_public: Option<bool>,
446    /// the parent project id
447    #[builder(default)]
448    parent_id: Option<u64>,
449    /// will the project inherit members from its ancestors
450    #[builder(default)]
451    inherit_members: Option<bool>,
452    /// ID of the default user. It works only when the new project is a subproject and it inherits the members
453    #[builder(default)]
454    default_assigned_to_id: Option<u64>,
455    /// ID of the default version. It works only with existing shared versions
456    #[builder(default)]
457    default_version_id: Option<u64>,
458    /// trackers to enable in the project
459    #[builder(default)]
460    tracker_ids: Option<Vec<u64>>,
461    /// modules to enable in the project
462    #[builder(default)]
463    enabled_module_names: Option<Vec<Cow<'a, str>>>,
464    /// custom issue fields to enable in the project
465    #[builder(default)]
466    issue_custom_field_id: Option<Vec<u64>>,
467    /// values for custom fields
468    #[builder(default)]
469    custom_field_values: Option<HashMap<u64, Cow<'a, str>>>,
470}
471
472impl<'a> UpdateProject<'a> {
473    /// Create a builder for the endpoint.
474    #[must_use]
475    pub fn builder() -> UpdateProjectBuilder<'a> {
476        UpdateProjectBuilder::default()
477    }
478}
479
480impl Endpoint for UpdateProject<'_> {
481    fn method(&self) -> Method {
482        Method::PUT
483    }
484
485    fn endpoint(&self) -> Cow<'static, str> {
486        format!("projects/{}.json", self.project_id_or_name).into()
487    }
488
489    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
490        Ok(Some((
491            "application/json",
492            serde_json::to_vec(&ProjectWrapper::<UpdateProject> {
493                project: (*self).to_owned(),
494            })?,
495        )))
496    }
497}
498
499/// The endpoint to delete a Redmine project
500#[derive(Debug, Clone, Builder)]
501#[builder(setter(strip_option))]
502pub struct DeleteProject<'a> {
503    /// the project id or name as it appears in the URL of the project to delete
504    #[builder(setter(into))]
505    project_id_or_name: Cow<'a, str>,
506}
507
508impl<'a> DeleteProject<'a> {
509    /// Create a builder for the endpoint.
510    #[must_use]
511    pub fn builder() -> DeleteProjectBuilder<'a> {
512        DeleteProjectBuilder::default()
513    }
514}
515
516impl Endpoint for DeleteProject<'_> {
517    fn method(&self) -> Method {
518        Method::DELETE
519    }
520
521    fn endpoint(&self) -> Cow<'static, str> {
522        format!("projects/{}.json", &self.project_id_or_name).into()
523    }
524}
525
526/// helper struct for outer layers with a projects field holding the inner data
527#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
528pub struct ProjectsWrapper<T> {
529    /// to parse JSON with projects key
530    pub projects: Vec<T>,
531}
532
533/// A lot of APIs in Redmine wrap their data in an extra layer, this is a
534/// helper struct for outer layers with a project field holding the inner data
535#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
536pub struct ProjectWrapper<T> {
537    /// to parse JSON with project key
538    pub project: T,
539}
540
541#[cfg(test)]
542pub(crate) mod test {
543    use super::*;
544    use crate::api::test_helpers::with_project;
545    use pretty_assertions::assert_eq;
546    use std::error::Error;
547    use tokio::sync::RwLock;
548    use tracing_test::traced_test;
549
550    /// needed so we do not get 404s when listing while
551    /// creating/deleting or creating/updating/deleting
552    pub static PROJECT_LOCK: RwLock<()> = RwLock::const_new(());
553
554    #[traced_test]
555    #[test]
556    fn test_list_projects_first_page() -> Result<(), Box<dyn Error>> {
557        let _r_project = PROJECT_LOCK.read();
558        dotenvy::dotenv()?;
559        let redmine = crate::api::Redmine::from_env()?;
560        let endpoint = ListProjects::builder().build()?;
561        redmine.json_response_body_page::<_, Project>(&endpoint, 0, 25)?;
562        Ok(())
563    }
564
565    #[traced_test]
566    #[test]
567    fn test_list_projects_all_pages() -> Result<(), Box<dyn Error>> {
568        let _r_project = PROJECT_LOCK.read();
569        dotenvy::dotenv()?;
570        let redmine = crate::api::Redmine::from_env()?;
571        let endpoint = ListProjects::builder().build()?;
572        redmine.json_response_body_all_pages::<_, Project>(&endpoint)?;
573        Ok(())
574    }
575
576    #[traced_test]
577    #[tokio::test]
578    async fn test_list_projects_async_first_page() -> Result<(), Box<dyn Error>> {
579        let _r_project = PROJECT_LOCK.read();
580        dotenvy::dotenv()?;
581        let redmine = crate::api::RedmineAsync::from_env()?;
582        let endpoint = ListProjects::builder().build()?;
583        redmine
584            .json_response_body_page::<_, Project>(&endpoint, 0, 25)
585            .await?;
586        Ok(())
587    }
588
589    #[traced_test]
590    #[tokio::test]
591    async fn test_list_projects_async_all_pages() -> Result<(), Box<dyn Error>> {
592        let _r_project = PROJECT_LOCK.read();
593        dotenvy::dotenv()?;
594        let redmine = crate::api::RedmineAsync::from_env()?;
595        let endpoint = ListProjects::builder().build()?;
596        redmine
597            .json_response_body_all_pages::<_, Project>(&endpoint)
598            .await?;
599        Ok(())
600    }
601
602    #[traced_test]
603    #[test]
604    fn test_get_project() -> Result<(), Box<dyn Error>> {
605        let _r_project = PROJECT_LOCK.read();
606        dotenvy::dotenv()?;
607        let redmine = crate::api::Redmine::from_env()?;
608        let endpoint = GetProject::builder()
609            .project_id_or_name("sandbox")
610            .build()?;
611        redmine.json_response_body::<_, ProjectWrapper<Project>>(&endpoint)?;
612        Ok(())
613    }
614
615    #[function_name::named]
616    #[traced_test]
617    #[test]
618    fn test_create_project() -> Result<(), Box<dyn Error>> {
619        let name = format!("unittest_{}", function_name!());
620        with_project(&name, |_, _, _| Ok(()))?;
621        Ok(())
622    }
623
624    #[function_name::named]
625    #[traced_test]
626    #[test]
627    fn test_update_project() -> Result<(), Box<dyn Error>> {
628        let name = format!("unittest_{}", function_name!());
629        with_project(&name, |redmine, _id, name| {
630            let update_endpoint = super::UpdateProject::builder()
631                .project_id_or_name(name)
632                .description("Test-Description")
633                .build()?;
634            redmine.ignore_response_body::<_>(&update_endpoint)?;
635            Ok(())
636        })?;
637        Ok(())
638    }
639
640    /// this tests if any of the results contain a field we are not deserializing
641    ///
642    /// this will only catch fields we missed if they are part of the response but
643    /// it is better than nothing
644    #[traced_test]
645    #[test]
646    fn test_completeness_project_type() -> Result<(), Box<dyn Error>> {
647        let _r_project = PROJECT_LOCK.read();
648        dotenvy::dotenv()?;
649        let redmine = crate::api::Redmine::from_env()?;
650        let endpoint = ListProjects::builder().build()?;
651        let values: Vec<serde_json::Value> = redmine.json_response_body_all_pages(&endpoint)?;
652        for value in values {
653            let o: Project = serde_json::from_value(value.clone())?;
654            let reserialized = serde_json::to_value(o)?;
655            assert_eq!(value, reserialized);
656        }
657        Ok(())
658    }
659
660    /// this tests if any of the results contain a field we are not deserializing
661    ///
662    /// this will only catch fields we missed if they are part of the response but
663    /// it is better than nothing
664    ///
665    /// this version of the test will load all pages of projects and the individual
666    /// projects for each via GetProject which means it
667    /// can take a while so you need to use --include-ignored
668    /// or --ignored to run it
669    #[traced_test]
670    #[test]
671    fn test_completeness_project_type_all_pages_all_project_details() -> Result<(), Box<dyn Error>>
672    {
673        let _r_project = PROJECT_LOCK.read();
674        dotenvy::dotenv()?;
675        let redmine = crate::api::Redmine::from_env()?;
676        let endpoint = ListProjects::builder()
677            .include(vec![
678                ProjectsInclude::Trackers,
679                ProjectsInclude::IssueCategories,
680                ProjectsInclude::EnabledModules,
681                ProjectsInclude::TimeEntryActivities,
682                ProjectsInclude::IssueCustomFields,
683            ])
684            .build()?;
685        let projects = redmine.json_response_body_all_pages::<_, Project>(&endpoint)?;
686        for project in projects {
687            let get_endpoint = GetProject::builder()
688                .project_id_or_name(project.id.to_string())
689                .include(vec![
690                    ProjectInclude::Trackers,
691                    ProjectInclude::IssueCategories,
692                    ProjectInclude::EnabledModules,
693                    ProjectInclude::TimeEntryActivities,
694                    ProjectInclude::IssueCustomFields,
695                ])
696                .build()?;
697            let ProjectWrapper { project: value } = redmine
698                .json_response_body::<_, ProjectWrapper<serde_json::Value>>(&get_endpoint)?;
699            let o: Project = serde_json::from_value(value.clone())?;
700            let reserialized = serde_json::to_value(o)?;
701            assert_eq!(value, reserialized);
702        }
703        Ok(())
704    }
705}