pub struct Param { /* private fields */ }Expand description
One control at the top of a view.
Implementations§
Source§impl Param
impl Param
Sourcepub fn select(
key: impl Into<String>,
label: impl Into<String>,
options: impl IntoIterator<Item = impl Into<SelectOption>>,
) -> Self
pub fn select( key: impl Into<String>, label: impl Into<String>, options: impl IntoIterator<Item = impl Into<SelectOption>>, ) -> Self
A choice among options, which are rebuilt on every request like a schema is, so a select can be filled from a table.
Examples found in repository?
examples/library/main.rs (line 577)
550 fn params(&self, ctx: &Context, asked: &ViewArgs) -> Result<Vec<Param>, ApiError> {
551 let branches = Self::branches(ctx)?;
552 let options: Vec<SelectOption> = branches
553 .iter()
554 .map(|b| SelectOption::labelled(&b.code, &b.name))
555 .collect();
556 let first = branches.first().map(|b| b.code.clone()).unwrap_or_default();
557
558 // A genre and one of its subgenres. The second list follows the first
559 // one's answer, which is why `params` is told what was asked before
560 // anything is settled. A subgenre belonging to the genre chosen last
561 // time is no answer to the question being asked now, and falls back to
562 // every subgenre of the genre chosen this time.
563 let genres: Vec<Genre> = ctx.optional_rows(GENRES_FILE)?;
564 let mut named: Vec<&str> = genres.iter().map(|g| g.genre.as_str()).collect();
565 named.sort_unstable();
566 named.dedup();
567
568 let chosen = asked.get_or("genre", "");
569 let mut subgenres: Vec<&str> = genres
570 .iter()
571 .filter(|g| g.genre == chosen)
572 .map(|g| g.subgenre.as_str())
573 .collect();
574 subgenres.sort_unstable();
575
576 Ok(vec![
577 Param::select("branch", "Branch", options).default(first),
578 Param::select("genre", "Genre", Self::any("Every genre", named)).default(""),
579 Param::select(
580 "subgenre",
581 "Subgenre",
582 Self::any("Every subgenre", subgenres),
583 )
584 .default(""),
585 Param::string("author", "Author"),
586 ])
587 }
588
589 fn render(&self, args: &ViewArgs, ctx: &Context) -> Result<ViewData, ApiError> {
590 let books: Vec<Book> = ctx.optional_rows(BOOKS_FILE)?;
591 let branches = Self::branches(ctx)?;
592 let code = args.get_or("branch", "");
593 let branch = branches.iter().find(|b| b.code == code);
594 let named = branch.map(|b| b.name.as_str()).unwrap_or(code);
595
596 let genre = args.get_or("genre", "");
597 let subgenre = args.get_or("subgenre", "");
598 let author = args.get_or("author", "").trim().to_lowercase();
599
600 // The books this branch holds that answer the rest of the question,
601 // whether they are out or not. A parameter left empty asks nothing.
602 let held: Vec<&Book> = books
603 .iter()
604 .filter(|b| b.shelved.contains_key(code))
605 .filter(|b| genre.is_empty() || b.genre == genre)
606 .filter(|b| subgenre.is_empty() || b.subgenre == subgenre)
607 .filter(|b| {
608 author.is_empty()
609 || format!("{} {}", b.author_first, b.author_last)
610 .to_lowercase()
611 .contains(&author)
612 })
613 .collect();
614
615 let now = today();
616 let mut out = Vec::new();
617 let mut overdue = Vec::new();
618 for book in held.iter().filter(|b| b.lent.unwrap_or(false)) {
619 let days = days_from_civil(&book.due).map(|due| now - due).unwrap_or(0);
620 if days > 0 {
621 overdue.push(OnLoan::row(book, days));
622 } else {
623 out.push(OnLoan::row(book, days));
624 }
625 }
626
627 let lent = out.len() + overdue.len();
628 Ok(ViewData::new()
629 .note(format!(
630 "{lent} of {} book(s) at {named} are out on loan. A title links to its catalogue entry.",
631 held.len()
632 ))
633 .section(
634 Section::new(OnLoan::columns())
635 .heading("Out")
636 .note("Days left before they are due.")
637 .rows(out)?,
638 )
639 .section(
640 Section::new(OnLoan::columns())
641 .heading("Overdue")
642 .note("Days past due. Chase these.")
643 .rows(overdue)?,
644 ))
645 }
646}
647
648// ── The Branches view, which is a card per branch ────────────────────────────
649
650/// Every branch as a card, grouped by whether it is open.
651///
652/// A card is what a view answers with when the question is "how do these things
653/// stand" rather than "what are the values of these fields": there are no
654/// columns here, and each card says how one branch stands, what it is called,
655/// and two counts, with the whole card a link to the branch's own page.
656struct BranchCards;
657
658/// How many of the books a branch holds, and how many of those are out.
659fn shelf_counts(books: &[Book], code: &str) -> (usize, usize) {
660 let here: Vec<&Book> = books
661 .iter()
662 .filter(|b| b.shelved.contains_key(code))
663 .collect();
664 let out = here.iter().filter(|b| b.lent.unwrap_or(false)).count();
665 (here.len(), out)
666}
667
668fn librarian(branch: &Branch) -> String {
669 format!("{} {}", branch.librarian_first, branch.librarian_last)
670 .trim()
671 .to_string()
672}
673
674impl ViewLogic for BranchCards {
675 fn name(&self) -> &'static str {
676 "all-branches"
677 }
678
679 fn title(&self) -> &'static str {
680 "All branches"
681 }
682
683 fn render(&self, _args: &ViewArgs, ctx: &Context) -> Result<ViewData, ApiError> {
684 let branches: Vec<Branch> = ctx.optional_rows(BRANCHES_FILE)?;
685 let books: Vec<Book> = ctx.optional_rows(BOOKS_FILE)?;
686
687 let card = |branch: &Branch| {
688 let (here, out) = shelf_counts(&books, &branch.code);
689 let open = branch.open.unwrap_or(false);
690 let mut card = Card::new(branch.name.as_str())
691 .status(if open {
692 Status::new("Open", Tone::Good)
693 } else {
694 // A shut branch is a fact about it rather than a fault, so
695 // it is neutral—which is also what reads quieter.
696 Status::new("Shut", Tone::Neutral)
697 })
698 .identifier(branch.code.as_str())
699 .subtitle(format!(
700 "{}, {} staff",
701 librarian(branch),
702 branch.staff.unwrap_or(0)
703 ))
704 .row("Books here", here)
705 .row("Out on loan", out)
706 .link(ViewLink::new("branch").arg("branch", &branch.code));
707 if branch.hours.is_empty() {
708 card = card.sentence(
709 "No opening hours are recorded, so nothing here says when it can be visited.",
710 );
711 }
712 card
713 };
714
715 let group = |heading: &str, open: bool| {
716 CardGroup::new(heading).cards(
717 branches
718 .iter()
719 .filter(|b| b.open.unwrap_or(false) == open)
720 .map(&card),
721 )
722 };
723
724 Ok(ViewData::new()
725 .note("A card is a branch. Open one for what it holds and what is out.")
726 .group(group("Open", true))
727 .group(group("Shut", false)))
728 }
729}
730
731// ── The Branch view, which is one branch in detail ───────────────────────────
732
733/// One branch: what it has out, what is on its shelves, and when it is open.
734///
735/// It is reached from a card rather than from the switcher, so its one
736/// parameter is hidden: the page draws no control for it, and the card's link
737/// is what answers it. The parameter is a select all the same, so a link to a
738/// branch that has since gone falls back to the first one rather than to a page
739/// about nothing.
740struct BranchDetail;
741
742impl BranchDetail {
743 /// The books a branch holds, best rated first, which is the order the
744 /// numbered section is a ranking in.
745 fn by_rating<'a>(books: &'a [Book], code: &str, lent: bool) -> Vec<&'a Book> {
746 let mut held: Vec<&Book> = books
747 .iter()
748 .filter(|b| b.shelved.contains_key(code) && b.lent.unwrap_or(false) == lent)
749 .collect();
750 held.sort_by(|a, b| {
751 b.rating
752 .unwrap_or(0.0)
753 .total_cmp(&a.rating.unwrap_or(0.0))
754 .then_with(|| a.title.cmp(&b.title))
755 });
756 held
757 }
758
759 /// What a form asks before a book goes out. The four kinds of field are all
760 /// here, since this is the page the bundle's form controls are developed
761 /// against.
762 fn loan_form(title: &str) -> Form {
763 Form::new("lend-a-book")
764 .arg("title", title)
765 .field(Field::text("borrower", "Borrower"))
766 .field(Field::number("days", "Days out").default(21))
767 .field(Field::date("from", "Date lent").default(civil_from_days(today())))
768 .field(
769 Field::one_of(
770 "condition",
771 "Condition it left in",
772 ["As new", "Good", "Worn"],
773 )
774 .default("Good"),
775 )
776 }
777}
778
779impl ViewLogic for BranchDetail {
780 fn name(&self) -> &'static str {
781 "branch"
782 }
783
784 fn title(&self) -> &'static str {
785 "Branch"
786 }
787
788 /// This page is about one branch, and which one arrives through a card's
789 /// link. A switcher entry for it would open whichever branch the parameter
790 /// happens to default to, which is nobody's question.
791 fn in_switcher(&self) -> bool {
792 false
793 }
794
795 fn params(&self, ctx: &Context, _asked: &ViewArgs) -> Result<Vec<Param>, ApiError> {
796 let branches: Vec<Branch> = ctx.optional_rows(BRANCHES_FILE)?;
797 let codes: Vec<SelectOption> = branches
798 .iter()
799 .map(|b| SelectOption::labelled(&b.code, &b.name))
800 .collect();
801 let first = branches.first().map(|b| b.code.clone()).unwrap_or_default();
802 Ok(vec![
803 Param::select("branch", "Branch", codes)
804 .default(first)
805 .hidden(),
806 ])
807 }Sourcepub fn string(key: impl Into<String>, label: impl Into<String>) -> Self
pub fn string(key: impl Into<String>, label: impl Into<String>) -> Self
Examples found in repository?
examples/library/main.rs (line 585)
550 fn params(&self, ctx: &Context, asked: &ViewArgs) -> Result<Vec<Param>, ApiError> {
551 let branches = Self::branches(ctx)?;
552 let options: Vec<SelectOption> = branches
553 .iter()
554 .map(|b| SelectOption::labelled(&b.code, &b.name))
555 .collect();
556 let first = branches.first().map(|b| b.code.clone()).unwrap_or_default();
557
558 // A genre and one of its subgenres. The second list follows the first
559 // one's answer, which is why `params` is told what was asked before
560 // anything is settled. A subgenre belonging to the genre chosen last
561 // time is no answer to the question being asked now, and falls back to
562 // every subgenre of the genre chosen this time.
563 let genres: Vec<Genre> = ctx.optional_rows(GENRES_FILE)?;
564 let mut named: Vec<&str> = genres.iter().map(|g| g.genre.as_str()).collect();
565 named.sort_unstable();
566 named.dedup();
567
568 let chosen = asked.get_or("genre", "");
569 let mut subgenres: Vec<&str> = genres
570 .iter()
571 .filter(|g| g.genre == chosen)
572 .map(|g| g.subgenre.as_str())
573 .collect();
574 subgenres.sort_unstable();
575
576 Ok(vec![
577 Param::select("branch", "Branch", options).default(first),
578 Param::select("genre", "Genre", Self::any("Every genre", named)).default(""),
579 Param::select(
580 "subgenre",
581 "Subgenre",
582 Self::any("Every subgenre", subgenres),
583 )
584 .default(""),
585 Param::string("author", "Author"),
586 ])
587 }Sourcepub fn default(self, value: impl Into<String>) -> Self
pub fn default(self, value: impl Into<String>) -> Self
What the view is asked for when the address names nothing.
Examples found in repository?
examples/library/main.rs (line 577)
550 fn params(&self, ctx: &Context, asked: &ViewArgs) -> Result<Vec<Param>, ApiError> {
551 let branches = Self::branches(ctx)?;
552 let options: Vec<SelectOption> = branches
553 .iter()
554 .map(|b| SelectOption::labelled(&b.code, &b.name))
555 .collect();
556 let first = branches.first().map(|b| b.code.clone()).unwrap_or_default();
557
558 // A genre and one of its subgenres. The second list follows the first
559 // one's answer, which is why `params` is told what was asked before
560 // anything is settled. A subgenre belonging to the genre chosen last
561 // time is no answer to the question being asked now, and falls back to
562 // every subgenre of the genre chosen this time.
563 let genres: Vec<Genre> = ctx.optional_rows(GENRES_FILE)?;
564 let mut named: Vec<&str> = genres.iter().map(|g| g.genre.as_str()).collect();
565 named.sort_unstable();
566 named.dedup();
567
568 let chosen = asked.get_or("genre", "");
569 let mut subgenres: Vec<&str> = genres
570 .iter()
571 .filter(|g| g.genre == chosen)
572 .map(|g| g.subgenre.as_str())
573 .collect();
574 subgenres.sort_unstable();
575
576 Ok(vec![
577 Param::select("branch", "Branch", options).default(first),
578 Param::select("genre", "Genre", Self::any("Every genre", named)).default(""),
579 Param::select(
580 "subgenre",
581 "Subgenre",
582 Self::any("Every subgenre", subgenres),
583 )
584 .default(""),
585 Param::string("author", "Author"),
586 ])
587 }
588
589 fn render(&self, args: &ViewArgs, ctx: &Context) -> Result<ViewData, ApiError> {
590 let books: Vec<Book> = ctx.optional_rows(BOOKS_FILE)?;
591 let branches = Self::branches(ctx)?;
592 let code = args.get_or("branch", "");
593 let branch = branches.iter().find(|b| b.code == code);
594 let named = branch.map(|b| b.name.as_str()).unwrap_or(code);
595
596 let genre = args.get_or("genre", "");
597 let subgenre = args.get_or("subgenre", "");
598 let author = args.get_or("author", "").trim().to_lowercase();
599
600 // The books this branch holds that answer the rest of the question,
601 // whether they are out or not. A parameter left empty asks nothing.
602 let held: Vec<&Book> = books
603 .iter()
604 .filter(|b| b.shelved.contains_key(code))
605 .filter(|b| genre.is_empty() || b.genre == genre)
606 .filter(|b| subgenre.is_empty() || b.subgenre == subgenre)
607 .filter(|b| {
608 author.is_empty()
609 || format!("{} {}", b.author_first, b.author_last)
610 .to_lowercase()
611 .contains(&author)
612 })
613 .collect();
614
615 let now = today();
616 let mut out = Vec::new();
617 let mut overdue = Vec::new();
618 for book in held.iter().filter(|b| b.lent.unwrap_or(false)) {
619 let days = days_from_civil(&book.due).map(|due| now - due).unwrap_or(0);
620 if days > 0 {
621 overdue.push(OnLoan::row(book, days));
622 } else {
623 out.push(OnLoan::row(book, days));
624 }
625 }
626
627 let lent = out.len() + overdue.len();
628 Ok(ViewData::new()
629 .note(format!(
630 "{lent} of {} book(s) at {named} are out on loan. A title links to its catalogue entry.",
631 held.len()
632 ))
633 .section(
634 Section::new(OnLoan::columns())
635 .heading("Out")
636 .note("Days left before they are due.")
637 .rows(out)?,
638 )
639 .section(
640 Section::new(OnLoan::columns())
641 .heading("Overdue")
642 .note("Days past due. Chase these.")
643 .rows(overdue)?,
644 ))
645 }
646}
647
648// ── The Branches view, which is a card per branch ────────────────────────────
649
650/// Every branch as a card, grouped by whether it is open.
651///
652/// A card is what a view answers with when the question is "how do these things
653/// stand" rather than "what are the values of these fields": there are no
654/// columns here, and each card says how one branch stands, what it is called,
655/// and two counts, with the whole card a link to the branch's own page.
656struct BranchCards;
657
658/// How many of the books a branch holds, and how many of those are out.
659fn shelf_counts(books: &[Book], code: &str) -> (usize, usize) {
660 let here: Vec<&Book> = books
661 .iter()
662 .filter(|b| b.shelved.contains_key(code))
663 .collect();
664 let out = here.iter().filter(|b| b.lent.unwrap_or(false)).count();
665 (here.len(), out)
666}
667
668fn librarian(branch: &Branch) -> String {
669 format!("{} {}", branch.librarian_first, branch.librarian_last)
670 .trim()
671 .to_string()
672}
673
674impl ViewLogic for BranchCards {
675 fn name(&self) -> &'static str {
676 "all-branches"
677 }
678
679 fn title(&self) -> &'static str {
680 "All branches"
681 }
682
683 fn render(&self, _args: &ViewArgs, ctx: &Context) -> Result<ViewData, ApiError> {
684 let branches: Vec<Branch> = ctx.optional_rows(BRANCHES_FILE)?;
685 let books: Vec<Book> = ctx.optional_rows(BOOKS_FILE)?;
686
687 let card = |branch: &Branch| {
688 let (here, out) = shelf_counts(&books, &branch.code);
689 let open = branch.open.unwrap_or(false);
690 let mut card = Card::new(branch.name.as_str())
691 .status(if open {
692 Status::new("Open", Tone::Good)
693 } else {
694 // A shut branch is a fact about it rather than a fault, so
695 // it is neutral—which is also what reads quieter.
696 Status::new("Shut", Tone::Neutral)
697 })
698 .identifier(branch.code.as_str())
699 .subtitle(format!(
700 "{}, {} staff",
701 librarian(branch),
702 branch.staff.unwrap_or(0)
703 ))
704 .row("Books here", here)
705 .row("Out on loan", out)
706 .link(ViewLink::new("branch").arg("branch", &branch.code));
707 if branch.hours.is_empty() {
708 card = card.sentence(
709 "No opening hours are recorded, so nothing here says when it can be visited.",
710 );
711 }
712 card
713 };
714
715 let group = |heading: &str, open: bool| {
716 CardGroup::new(heading).cards(
717 branches
718 .iter()
719 .filter(|b| b.open.unwrap_or(false) == open)
720 .map(&card),
721 )
722 };
723
724 Ok(ViewData::new()
725 .note("A card is a branch. Open one for what it holds and what is out.")
726 .group(group("Open", true))
727 .group(group("Shut", false)))
728 }
729}
730
731// ── The Branch view, which is one branch in detail ───────────────────────────
732
733/// One branch: what it has out, what is on its shelves, and when it is open.
734///
735/// It is reached from a card rather than from the switcher, so its one
736/// parameter is hidden: the page draws no control for it, and the card's link
737/// is what answers it. The parameter is a select all the same, so a link to a
738/// branch that has since gone falls back to the first one rather than to a page
739/// about nothing.
740struct BranchDetail;
741
742impl BranchDetail {
743 /// The books a branch holds, best rated first, which is the order the
744 /// numbered section is a ranking in.
745 fn by_rating<'a>(books: &'a [Book], code: &str, lent: bool) -> Vec<&'a Book> {
746 let mut held: Vec<&Book> = books
747 .iter()
748 .filter(|b| b.shelved.contains_key(code) && b.lent.unwrap_or(false) == lent)
749 .collect();
750 held.sort_by(|a, b| {
751 b.rating
752 .unwrap_or(0.0)
753 .total_cmp(&a.rating.unwrap_or(0.0))
754 .then_with(|| a.title.cmp(&b.title))
755 });
756 held
757 }
758
759 /// What a form asks before a book goes out. The four kinds of field are all
760 /// here, since this is the page the bundle's form controls are developed
761 /// against.
762 fn loan_form(title: &str) -> Form {
763 Form::new("lend-a-book")
764 .arg("title", title)
765 .field(Field::text("borrower", "Borrower"))
766 .field(Field::number("days", "Days out").default(21))
767 .field(Field::date("from", "Date lent").default(civil_from_days(today())))
768 .field(
769 Field::one_of(
770 "condition",
771 "Condition it left in",
772 ["As new", "Good", "Worn"],
773 )
774 .default("Good"),
775 )
776 }
777}
778
779impl ViewLogic for BranchDetail {
780 fn name(&self) -> &'static str {
781 "branch"
782 }
783
784 fn title(&self) -> &'static str {
785 "Branch"
786 }
787
788 /// This page is about one branch, and which one arrives through a card's
789 /// link. A switcher entry for it would open whichever branch the parameter
790 /// happens to default to, which is nobody's question.
791 fn in_switcher(&self) -> bool {
792 false
793 }
794
795 fn params(&self, ctx: &Context, _asked: &ViewArgs) -> Result<Vec<Param>, ApiError> {
796 let branches: Vec<Branch> = ctx.optional_rows(BRANCHES_FILE)?;
797 let codes: Vec<SelectOption> = branches
798 .iter()
799 .map(|b| SelectOption::labelled(&b.code, &b.name))
800 .collect();
801 let first = branches.first().map(|b| b.code.clone()).unwrap_or_default();
802 Ok(vec![
803 Param::select("branch", "Branch", codes)
804 .default(first)
805 .hidden(),
806 ])
807 }Draw no control for this parameter. It is for a parameter that arrives through a link rather than through the page—which story a detail page is about—where a control would be a second way to ask a question the reader has already asked. It is still declared, so it takes a default and is still handed to the view.
Examples found in repository?
examples/library/main.rs (line 805)
795 fn params(&self, ctx: &Context, _asked: &ViewArgs) -> Result<Vec<Param>, ApiError> {
796 let branches: Vec<Branch> = ctx.optional_rows(BRANCHES_FILE)?;
797 let codes: Vec<SelectOption> = branches
798 .iter()
799 .map(|b| SelectOption::labelled(&b.code, &b.name))
800 .collect();
801 let first = branches.first().map(|b| b.code.clone()).unwrap_or_default();
802 Ok(vec![
803 Param::select("branch", "Branch", codes)
804 .default(first)
805 .hidden(),
806 ])
807 }pub fn key(&self) -> &str
Trait Implementations§
Auto Trait Implementations§
impl Freeze for Param
impl RefUnwindSafe for Param
impl Send for Param
impl Sync for Param
impl Unpin for Param
impl UnsafeUnpin for Param
impl UnwindSafe for Param
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more