diff --git a/404.html b/404.html index 4a14edeeb..bab4609d8 100644 --- a/404.html +++ b/404.html @@ -5,13 +5,13 @@ Page Not Found | Geo Garden Club - +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

- + \ No newline at end of file diff --git a/assets/js/0a1fe8aa.811ec745.js b/assets/js/0a1fe8aa.c251911e.js similarity index 63% rename from assets/js/0a1fe8aa.811ec745.js rename to assets/js/0a1fe8aa.c251911e.js index ba1ad434c..d6fe67e21 100644 --- a/assets/js/0a1fe8aa.811ec745.js +++ b/assets/js/0a1fe8aa.c251911e.js @@ -1 +1 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[8912],{3905:(e,t,n)=>{n.d(t,{Zo:()=>d,kt:()=>m});var a=n(7294);function i(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function l(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function o(e){for(var t=1;t=0||(i[n]=e[n]);return i}(e,t);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(i[n]=e[n])}return i}var p=a.createContext({}),s=function(e){var t=a.useContext(p),n=t;return e&&(n="function"==typeof e?e(t):o(o({},t),e)),n},d=function(e){var t=s(e.components);return a.createElement(p.Provider,{value:t},e.children)},u="mdxType",h={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},c=a.forwardRef((function(e,t){var n=e.components,i=e.mdxType,l=e.originalType,p=e.parentName,d=r(e,["components","mdxType","originalType","parentName"]),u=s(n),c=i,m=u["".concat(p,".").concat(c)]||u[c]||h[c]||l;return n?a.createElement(m,o(o({ref:t},d),{},{components:n})):a.createElement(m,o({ref:t},d))}));function m(e,t){var n=arguments,i=t&&t.mdxType;if("string"==typeof e||i){var l=n.length,o=new Array(l);o[0]=c;var r={};for(var p in t)hasOwnProperty.call(t,p)&&(r[p]=t[p]);r.originalType=e,r[u]="string"==typeof e?e:i,o[1]=r;for(var s=2;s{n.r(t),n.d(t,{assets:()=>p,contentTitle:()=>o,default:()=>h,frontMatter:()=>l,metadata:()=>r,toc:()=>s});var a=n(7462),i=(n(7294),n(3905));const l={hide_table_of_contents:!1},o="Deployment",r={unversionedId:"develop/release-1.0/deployment",id:"develop/release-1.0/deployment",title:"Deployment",description:"For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet.",source:"@site/docs/develop/release-1.0/deployment.md",sourceDirName:"develop/release-1.0",slug:"/develop/release-1.0/deployment",permalink:"/docs/develop/release-1.0/deployment",draft:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Core Value Propositions",permalink:"/docs/develop/release-1.0/cvp"},next:{title:"Architecture",permalink:"/docs/develop/release-1.0/architecture"}},p={},s=[{value:"Firebase App Distribution",id:"firebase-app-distribution",level:2},{value:"Documenting deployment versions",id:"documenting-deployment-versions",level:2},{value:"Deployment management",id:"deployment-management",level:2},{value:"1. Update the ChangeLog",id:"1-update-the-changelog",level:2},{value:"2. Build the iOS release",id:"2-build-the-ios-release",level:2},{value:"3. Build the android release",id:"3-build-the-android-release",level:2},{value:"4. Build the web app",id:"4-build-the-web-app",level:2},{value:"Previewing prior to deployment",id:"previewing-prior-to-deployment",level:4}],d={toc:s},u="wrapper";function h(e){let{components:t,...n}=e;return(0,i.kt)(u,(0,a.Z)({},d,n,{components:t,mdxType:"MDXLayout"}),(0,i.kt)("h1",{id:"deployment"},"Deployment"),(0,i.kt)("p",null,"For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet."),(0,i.kt)("h2",{id:"firebase-app-distribution"},"Firebase App Distribution"),(0,i.kt)("p",null,"For the Beta Release, we are using ",(0,i.kt)("a",{parentName:"p",href:"https://firebase.google.com/docs/app-distribution"},"Firebase App Distribution")," as the deployment mechanism. This has the following implications:"),(0,i.kt)("ol",null,(0,i.kt)("li",{parentName:"ol"},"We deploy to iOS, Android, and the web."),(0,i.kt)("li",{parentName:"ol"},"We must obtain the email address for every user who wishes to have GGC on their device. ")),(0,i.kt)("h2",{id:"documenting-deployment-versions"},"Documenting deployment versions"),(0,i.kt)("p",null,"We expect to make many deployments during the Beta release period as we fix bugs or implement enhancements. Each new deployment will require a new version number (specified in the pubspec.yml file), and we will document what has changed in each new version via ",(0,i.kt)("a",{parentName:"p",href:"https://github.com/geogardenclub/ggc_app/blob/main/CHANGELOG.md"},"CHANGELOG.md"),". To manage version numbers and the changelog file, we will use ",(0,i.kt)("a",{parentName:"p",href:"https://pub.dev/packages/cider"},"Cider"),"."),(0,i.kt)("p",null,"We also want to be able to access the ChangeLog inside the deployed app---this is a simple way for users to both know what version of the app they have installed, and what new features or changes they can expect to find in a new version. In order to implement that, there is a script called ",(0,i.kt)("inlineCode",{parentName:"p"},"run_cider.sh")," that runs the cider command and also copies the resulting CHANGELOG.md file into the assets/changelog directory so that it will be bundled and deployed with the app, and available to users within their Settings screen."),(0,i.kt)("p",null,"We will adhere to two standards:"),(0,i.kt)("ol",null,(0,i.kt)("li",{parentName:"ol"},"For the changelog format, we will adhere to ",(0,i.kt)("a",{parentName:"li",href:"https://keepachangelog.com/en/1.0.0/"},"Keep a Changelog"),"."),(0,i.kt)("li",{parentName:"ol"},"For the version number format, we will adhere to ",(0,i.kt)("a",{parentName:"li",href:"https://semver.org/spec/v2.0.0.html"},"Semantic Versioning"),'. For the beta release, since there is no public "API", the ',(0,i.kt)("em",{parentName:"li"},"major"),' version will always be "1". We will increment the ',(0,i.kt)("em",{parentName:"li"},"minor")," version when there are new UI or business logic enhancements to the application, and we will increment the ",(0,i.kt)("em",{parentName:"li"},"patch")," version in the event of deployments made only to fix bugs. We will not use the pre-release or build suffix notation.")),(0,i.kt)("h2",{id:"deployment-management"},"Deployment management"),(0,i.kt)("p",null,'The deployment process is handled by a single developer referred to as the "Deployment Manager" (DM). Initially, Philip will be the DM.'),(0,i.kt)("h2",{id:"1-update-the-changelog"},"1. Update the ChangeLog"),(0,i.kt)("p",null,"Invoke ",(0,i.kt)("inlineCode",{parentName:"p"},"./run_cider log added ")," to document new additions (or use ",(0,i.kt)("inlineCode",{parentName:"p"},"changed")," or ",(0,i.kt)("inlineCode",{parentName:"p"},"fixed"),") since the last release. Enclose the message in quotes. For example:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-shell"},'./run_cider.sh log added "Terms and Conditions"\n')),(0,i.kt)("p",null,"Invoke ",(0,i.kt)("inlineCode",{parentName:"p"},"./run_cider.sh bump minor")," to increment the version number in pubspec.yml."),(0,i.kt)("p",null,"Invoke ",(0,i.kt)("inlineCode",{parentName:"p"},"./run_cider.sh release")," to update the ChangeLog to indicate that a new release with the current version in pubspec.yml has happened on the current date."),(0,i.kt)("p",null,"Commit the changed ChangeLog and pubspec.yml to main."),(0,i.kt)("h2",{id:"2-build-the-ios-release"},"2. Build the iOS release"),(0,i.kt)("p",null,"Documentation is available at: ",(0,i.kt)("a",{parentName:"p",href:"https://medium.com/@Ikay_codes/distribute-your-flutter-app-with-firebase-app-distribution-fc83e0ffb547"},"Distribute your Flutter App with FireBase App Distribution")," and ",(0,i.kt)("a",{parentName:"p",href:"https://docs.flutter.dev/deployment/ios"},"Build and release an iOS app"),". Here's a summary:"),(0,i.kt)("p",null,'In XCode, check "General" and "Signing and Capabilities" tabs. You may need to login to Apple to get the Team details and Provisioning details to be specified correctly.'),(0,i.kt)("p",null,"In a terminal, run ",(0,i.kt)("inlineCode",{parentName:"p"},"flutter build ios"),". Among other things, this tells XCode about the new version number in pubspec.yml. "),(0,i.kt)("p",null,"Back in XCode, invoke Product > Build, then Product > Archive."),(0,i.kt)("p",null,'If archiving completes successfully, then a dialog box will pop up with "Distribute App". Click it, and specify "Ad hoc" distribution, "Automatic signing", and keep clicking dialogs as they appear until the .ipa file is created. '),(0,i.kt)("p",null,"Upload the new .ipa to Firebase App Distribution by going to the App Distribution tab in the Firebase console and dropping the file into the upload area. "),(0,i.kt)("p",null,"Select the testers to receive the deployment."),(0,i.kt)("p",null,"Initiate deployment."),(0,i.kt)("h2",{id:"3-build-the-android-release"},"3. Build the android release"),(0,i.kt)("p",null,"Documentation is available at ",(0,i.kt)("a",{parentName:"p",href:"https://docs.flutter.dev/deployment/android"},"Build and release an Android app"),". Here's a summary:"),(0,i.kt)("p",null,"On my (2021) Mac laptop, I have generated an upload-keystore.jks file with CN=Philip Johnson, OU=geogardenclub, O=geogardenclub, L=Bellingham, ST=WA, C=US. "),(0,i.kt)("p",null,"If no changes are required to build.gradle or AndroidManifest.xml, then it should be possible to build the Android APK file by invoking:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-shell"},"flutter build apk\n")),(0,i.kt)("p",null,"This should result in the creation of an APK file in ",(0,i.kt)("inlineCode",{parentName:"p"},"build/app/outputs/flutter-apk/app-release.apk"),"."),(0,i.kt)("p",null,"To upload it, go to App Distribution, select ",(0,i.kt)("inlineCode",{parentName:"p"},"ggc_app (android)")," in the top of the window, and upload the file."),(0,i.kt)("p",null,"Select testers and distribute in the standards way."),(0,i.kt)("h2",{id:"4-build-the-web-app"},"4. Build the web app"),(0,i.kt)("p",null,"Documentation is available at: ",(0,i.kt)("a",{parentName:"p",href:"https://docs.flutter.dev/deployment/web"},"Build and release a web app"),"."),(0,i.kt)("p",null,"There are some one-time configuration steps documented at the above link, but once you've gone through them, you can simply invoke:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-shell"},"firebase deploy\n")),(0,i.kt)("p",null,"At the end, I received output like this:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-shell"},"Project Console: https://console.firebase.google.com/project/ggc-app-2de7b/overview\nHosting URL: https://ggc-app-2de7b.web.app\n")),(0,i.kt)("h4",{id:"previewing-prior-to-deployment"},"Previewing prior to deployment"),(0,i.kt)("p",null,"Note that if you want to preview the webapp prior to deployment, you can install ",(0,i.kt)("a",{parentName:"p",href:"https://pub.dev/packages/dhttpd"},"dhttp"),", then run:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-shell"},"$ flutter build web\n$ dhttpd --path build/web/\n")),(0,i.kt)("p",null,"Open http://localhost:8080 to see the app."))}h.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[8912],{3905:(e,t,n)=>{n.d(t,{Zo:()=>d,kt:()=>m});var a=n(7294);function i(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function l(e){for(var t=1;t=0||(i[n]=e[n]);return i}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(i[n]=e[n])}return i}var p=a.createContext({}),s=function(e){var t=a.useContext(p),n=t;return e&&(n="function"==typeof e?e(t):l(l({},t),e)),n},d=function(e){var t=s(e.components);return a.createElement(p.Provider,{value:t},e.children)},u="mdxType",h={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},c=a.forwardRef((function(e,t){var n=e.components,i=e.mdxType,o=e.originalType,p=e.parentName,d=r(e,["components","mdxType","originalType","parentName"]),u=s(n),c=i,m=u["".concat(p,".").concat(c)]||u[c]||h[c]||o;return n?a.createElement(m,l(l({ref:t},d),{},{components:n})):a.createElement(m,l({ref:t},d))}));function m(e,t){var n=arguments,i=t&&t.mdxType;if("string"==typeof e||i){var o=n.length,l=new Array(o);l[0]=c;var r={};for(var p in t)hasOwnProperty.call(t,p)&&(r[p]=t[p]);r.originalType=e,r[u]="string"==typeof e?e:i,l[1]=r;for(var s=2;s{n.r(t),n.d(t,{assets:()=>p,contentTitle:()=>l,default:()=>h,frontMatter:()=>o,metadata:()=>r,toc:()=>s});var a=n(7462),i=(n(7294),n(3905));const o={hide_table_of_contents:!1},l="Deployment",r={unversionedId:"develop/release-1.0/deployment",id:"develop/release-1.0/deployment",title:"Deployment",description:"For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet.",source:"@site/docs/develop/release-1.0/deployment.md",sourceDirName:"develop/release-1.0",slug:"/develop/release-1.0/deployment",permalink:"/docs/develop/release-1.0/deployment",draft:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Core Value Propositions",permalink:"/docs/develop/release-1.0/cvp"},next:{title:"Architecture",permalink:"/docs/develop/release-1.0/architecture"}},p={},s=[{value:"Firebase App Distribution",id:"firebase-app-distribution",level:2},{value:"Documenting deployment versions",id:"documenting-deployment-versions",level:2},{value:"Deployment management",id:"deployment-management",level:2},{value:"1. Update the ChangeLog",id:"1-update-the-changelog",level:2},{value:"2. Build the iOS release",id:"2-build-the-ios-release",level:2},{value:"3. Build the android release",id:"3-build-the-android-release",level:2},{value:"4. Build the web app",id:"4-build-the-web-app",level:2},{value:"Previewing prior to deployment",id:"previewing-prior-to-deployment",level:4}],d={toc:s},u="wrapper";function h(e){let{components:t,...n}=e;return(0,i.kt)(u,(0,a.Z)({},d,n,{components:t,mdxType:"MDXLayout"}),(0,i.kt)("h1",{id:"deployment"},"Deployment"),(0,i.kt)("p",null,"For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet."),(0,i.kt)("h2",{id:"firebase-app-distribution"},"Firebase App Distribution"),(0,i.kt)("p",null,"For the Beta Release, we are using ",(0,i.kt)("a",{parentName:"p",href:"https://firebase.google.com/docs/app-distribution"},"Firebase App Distribution")," as the deployment mechanism. This has the following implications:"),(0,i.kt)("ol",null,(0,i.kt)("li",{parentName:"ol"},"We deploy to iOS, Android, and the web."),(0,i.kt)("li",{parentName:"ol"},"We must obtain the email address for every user who wishes to have GGC on their device. ")),(0,i.kt)("h2",{id:"documenting-deployment-versions"},"Documenting deployment versions"),(0,i.kt)("p",null,"We expect to make many deployments during the Beta release period as we fix bugs or implement enhancements. Each new deployment will require a new version number (specified in the pubspec.yml file), and we will document what has changed in each new version via ",(0,i.kt)("a",{parentName:"p",href:"https://github.com/geogardenclub/ggc_app/blob/main/CHANGELOG.md"},"CHANGELOG.md"),". To manage version numbers and the changelog file, we will use ",(0,i.kt)("a",{parentName:"p",href:"https://pub.dev/packages/cider"},"Cider"),"."),(0,i.kt)("p",null,"We also want to be able to access the ChangeLog inside the deployed app---this is a simple way for users to both know what version of the app they have installed, and what new features or changes they can expect to find in a new version. In order to implement that, there is a script called ",(0,i.kt)("inlineCode",{parentName:"p"},"run_cider.sh")," that runs the cider command and also copies the resulting CHANGELOG.md file into the assets/changelog directory so that it will be bundled and deployed with the app, and available to users within their Settings screen."),(0,i.kt)("p",null,"We will adhere to two standards:"),(0,i.kt)("ol",null,(0,i.kt)("li",{parentName:"ol"},"For the changelog format, we will adhere to ",(0,i.kt)("a",{parentName:"li",href:"https://keepachangelog.com/en/1.0.0/"},"Keep a Changelog"),"."),(0,i.kt)("li",{parentName:"ol"},"For the version number format, we will adhere to ",(0,i.kt)("a",{parentName:"li",href:"https://semver.org/spec/v2.0.0.html"},"Semantic Versioning"),'. For the beta release, since there is no public "API", the ',(0,i.kt)("em",{parentName:"li"},"major"),' version will always be "1". We will increment the ',(0,i.kt)("em",{parentName:"li"},"minor")," version when there are new UI or business logic enhancements to the application, and we will increment the ",(0,i.kt)("em",{parentName:"li"},"patch")," version in the event of deployments made only to fix bugs. We will not use the pre-release or build suffix notation.")),(0,i.kt)("h2",{id:"deployment-management"},"Deployment management"),(0,i.kt)("p",null,'The deployment process is handled by a single developer referred to as the "Deployment Manager" (DM). Initially, Philip will be the DM.'),(0,i.kt)("h2",{id:"1-update-the-changelog"},"1. Update the ChangeLog"),(0,i.kt)("p",null,"Invoke ",(0,i.kt)("inlineCode",{parentName:"p"},"./run_cider log added ")," to document new additions (or use ",(0,i.kt)("inlineCode",{parentName:"p"},"changed")," or ",(0,i.kt)("inlineCode",{parentName:"p"},"fixed"),") since the last release. Enclose the message in quotes. For example:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-shell"},'./run_cider.sh log added "Terms and Conditions"\n')),(0,i.kt)("p",null,"Invoke ",(0,i.kt)("inlineCode",{parentName:"p"},"./run_cider.sh bump minor")," to increment the version number in pubspec.yml."),(0,i.kt)("p",null,"Invoke ",(0,i.kt)("inlineCode",{parentName:"p"},"./run_cider.sh release")," to update the ChangeLog to indicate that a new release with the current version in pubspec.yml has happened on the current date."),(0,i.kt)("p",null,"Commit the changed ChangeLog and pubspec.yml to main."),(0,i.kt)("h2",{id:"2-build-the-ios-release"},"2. Build the iOS release"),(0,i.kt)("p",null,"Documentation is available at: ",(0,i.kt)("a",{parentName:"p",href:"https://medium.com/@Ikay_codes/distribute-your-flutter-app-with-firebase-app-distribution-fc83e0ffb547"},"Distribute your Flutter App with FireBase App Distribution")," and ",(0,i.kt)("a",{parentName:"p",href:"https://docs.flutter.dev/deployment/ios"},"Build and release an iOS app"),". Here's a summary:"),(0,i.kt)("p",null,'In XCode, check "General" and "Signing and Capabilities" tabs. You may need to login to Apple to get the Team details and Provisioning details to be specified correctly.'),(0,i.kt)("p",null,"In a terminal, run ",(0,i.kt)("inlineCode",{parentName:"p"},"flutter build ios"),". Among other things, this tells XCode about the new version number in pubspec.yml. (Note that the updated version number might not appear in XCode, this should not be a problem, but double check after the archive is generated.)"),(0,i.kt)("p",null,"Back in XCode, invoke Product > Build, then Product > Archive."),(0,i.kt)("p",null,'If archiving completes successfully, then a dialog box will pop up with "Distribute App". Click it, and specify "Custom", then "Ad hoc" distribution, then "Automatic signing", and keep clicking dialogs as they appear until the .ipa file is created. '),(0,i.kt)("p",null,"Upload the new .ipa to Firebase App Distribution by going to the App Distribution tab in the Firebase console and dropping the file into the upload area. "),(0,i.kt)("p",null,"Select the testers to receive the deployment."),(0,i.kt)("p",null,"Initiate deployment."),(0,i.kt)("h2",{id:"3-build-the-android-release"},"3. Build the android release"),(0,i.kt)("p",null,"Documentation is available at ",(0,i.kt)("a",{parentName:"p",href:"https://docs.flutter.dev/deployment/android"},"Build and release an Android app"),". Here's a summary:"),(0,i.kt)("p",null,"On my (2021) Mac laptop, I have generated an upload-keystore.jks file with CN=Philip Johnson, OU=geogardenclub, O=geogardenclub, L=Bellingham, ST=WA, C=US. "),(0,i.kt)("p",null,"If no changes are required to build.gradle or AndroidManifest.xml, then it should be possible to build the Android APK file by invoking:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-shell"},"flutter build apk\n")),(0,i.kt)("p",null,"This should result in the creation of an APK file in ",(0,i.kt)("inlineCode",{parentName:"p"},"build/app/outputs/flutter-apk/app-release.apk"),"."),(0,i.kt)("p",null,"To upload it, go to App Distribution, select ",(0,i.kt)("inlineCode",{parentName:"p"},"ggc_app (android)")," in the top of the window, and upload the file."),(0,i.kt)("p",null,"Select testers and distribute in the standards way."),(0,i.kt)("h2",{id:"4-build-the-web-app"},"4. Build the web app"),(0,i.kt)("p",null,"Documentation is available at: ",(0,i.kt)("a",{parentName:"p",href:"https://docs.flutter.dev/deployment/web"},"Build and release a web app"),"."),(0,i.kt)("p",null,"There are some one-time configuration steps documented at the above link, but once you've gone through them, you can simply invoke:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-shell"},"firebase deploy\n")),(0,i.kt)("p",null,"At the end, I received output like this:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-shell"},"Project Console: https://console.firebase.google.com/project/ggc-app-2de7b/overview\nHosting URL: https://ggc-app-2de7b.web.app\n")),(0,i.kt)("h4",{id:"previewing-prior-to-deployment"},"Previewing prior to deployment"),(0,i.kt)("p",null,"Note that if you want to preview the webapp prior to deployment, you can install ",(0,i.kt)("a",{parentName:"p",href:"https://pub.dev/packages/dhttpd"},"dhttp"),", then run:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-shell"},"$ flutter build web\n$ dhttpd --path build/web/\n")),(0,i.kt)("p",null,"Open http://localhost:8080 to see the app."))}h.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.d75322e9.js b/assets/js/runtime~main.51d99425.js similarity index 71% rename from assets/js/runtime~main.d75322e9.js rename to assets/js/runtime~main.51d99425.js index b302eb728..f866d3918 100644 --- a/assets/js/runtime~main.d75322e9.js +++ b/assets/js/runtime~main.51d99425.js @@ -1 +1 @@ -(()=>{"use strict";var e,a,c,f,d,t={},r={};function b(e){var a=r[e];if(void 0!==a)return a.exports;var c=r[e]={exports:{}};return t[e].call(c.exports,c,c.exports,b),c.exports}b.m=t,e=[],b.O=(a,c,f,d)=>{if(!c){var t=1/0;for(i=0;i=d)&&Object.keys(b.O).every((e=>b.O[e](c[o])))?c.splice(o--,1):(r=!1,d0&&e[i-1][2]>d;i--)e[i]=e[i-1];e[i]=[c,f,d]},b.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return b.d(a,{a:a}),a},c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,b.t=function(e,f){if(1&f&&(e=this(e)),8&f)return e;if("object"==typeof e&&e){if(4&f&&e.__esModule)return e;if(16&f&&"function"==typeof e.then)return e}var d=Object.create(null);b.r(d);var t={};a=a||[null,c({}),c([]),c(c)];for(var r=2&f&&e;"object"==typeof r&&!~a.indexOf(r);r=c(r))Object.getOwnPropertyNames(r).forEach((a=>t[a]=()=>e[a]));return t.default=()=>e,b.d(d,t),d},b.d=(e,a)=>{for(var c in a)b.o(a,c)&&!b.o(e,c)&&Object.defineProperty(e,c,{enumerable:!0,get:a[c]})},b.f={},b.e=e=>Promise.all(Object.keys(b.f).reduce(((a,c)=>(b.f[c](e,a),a)),[])),b.u=e=>"assets/js/"+({4:"4550e3f5",53:"935f2afb",234:"39838d4e",533:"b2b675dd",616:"d0e791fc",814:"95f4d37c",825:"7be5f79d",864:"eac03826",1218:"1be3623c",1320:"b5521830",1420:"6741c1a9",1477:"b2f554cd",1549:"1506d638",1585:"2450005c",1937:"49882d99",1974:"2b7e4bbf",2076:"477a2718",2122:"b75ab542",2535:"814f3328",2799:"954aa590",2825:"0f7bf3fd",3014:"4a78ad51",3085:"1f391b9e",3089:"a6aa9e1f",3170:"5d9c8300",3328:"8c887cbc",3576:"98dbdb4e",3608:"9e4087bc",3844:"772c3429",4e3:"afc29949",4057:"c03baef0",4195:"c4f5d8e4",4345:"2a883cfd",4371:"e82ae094",4490:"850fe7ba",5929:"b89822c9",6103:"ccc49370",6265:"906ac375",6378:"7fda5be5",6427:"59628a4d",6800:"2f9db241",6974:"af21c641",7414:"393be207",7540:"0f1af657",7918:"17896441",8277:"8175d4ae",8294:"3463d78f",8700:"d75fb1fa",8912:"0a1fe8aa",9268:"ba771284",9278:"975c475b",9514:"1be78505",9597:"ccc89dea",9866:"11f6a8a1",9929:"1863cff0"}[e]||e)+"."+{4:"7ad67dca",53:"905e9469",234:"481530d0",412:"b078622e",533:"160181bc",616:"4f89fef9",814:"76530f14",825:"fa3cc8c0",864:"7790a19c",1218:"d11c7845",1320:"b9e2999d",1420:"f3b060f6",1477:"411ee99e",1506:"15f1cca7",1549:"cbd15b9c",1585:"ae8a43a0",1937:"ccf7713b",1974:"55d36594",2076:"3fa6aca8",2122:"4c1183bd",2535:"d076fce0",2799:"b83f7460",2825:"df9b0069",3014:"b5225116",3085:"8e54e8e5",3089:"8498bf64",3170:"faed4819",3328:"1fa3675b",3576:"fcef76b1",3608:"51a28fd2",3844:"a8ebcca4",4e3:"3fe8cdd8",4057:"317b70a4",4195:"257ce74a",4345:"8444fbf6",4371:"49a40ed9",4490:"0e664365",4972:"42ae5fe7",5929:"5dd28f84",6103:"12d16554",6265:"521967b9",6378:"36e42c64",6427:"bd7dbcff",6800:"b1803862",6974:"7c140e42",7414:"880ef78c",7540:"079dcb40",7918:"f7e188b3",8277:"f2aef675",8294:"3780c9e8",8700:"11c84342",8912:"811ec745",9268:"639cc857",9278:"805e417e",9514:"a58316fc",9597:"db23280d",9866:"14e7ff50",9929:"52749a3c"}[e]+".js",b.miniCssF=e=>{},b.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),b.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),f={},d="geogardenclub-github-io:",b.l=(e,a,c,t)=>{if(f[e])f[e].push(a);else{var r,o;if(void 0!==c)for(var n=document.getElementsByTagName("script"),i=0;i{r.onerror=r.onload=null,clearTimeout(s);var d=f[e];if(delete f[e],r.parentNode&&r.parentNode.removeChild(r),d&&d.forEach((e=>e(c))),a)return a(c)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:r}),12e4);r.onerror=l.bind(null,r.onerror),r.onload=l.bind(null,r.onload),o&&document.head.appendChild(r)}},b.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},b.p="/",b.gca=function(e){return e={17896441:"7918","4550e3f5":"4","935f2afb":"53","39838d4e":"234",b2b675dd:"533",d0e791fc:"616","95f4d37c":"814","7be5f79d":"825",eac03826:"864","1be3623c":"1218",b5521830:"1320","6741c1a9":"1420",b2f554cd:"1477","1506d638":"1549","2450005c":"1585","49882d99":"1937","2b7e4bbf":"1974","477a2718":"2076",b75ab542:"2122","814f3328":"2535","954aa590":"2799","0f7bf3fd":"2825","4a78ad51":"3014","1f391b9e":"3085",a6aa9e1f:"3089","5d9c8300":"3170","8c887cbc":"3328","98dbdb4e":"3576","9e4087bc":"3608","772c3429":"3844",afc29949:"4000",c03baef0:"4057",c4f5d8e4:"4195","2a883cfd":"4345",e82ae094:"4371","850fe7ba":"4490",b89822c9:"5929",ccc49370:"6103","906ac375":"6265","7fda5be5":"6378","59628a4d":"6427","2f9db241":"6800",af21c641:"6974","393be207":"7414","0f1af657":"7540","8175d4ae":"8277","3463d78f":"8294",d75fb1fa:"8700","0a1fe8aa":"8912",ba771284:"9268","975c475b":"9278","1be78505":"9514",ccc89dea:"9597","11f6a8a1":"9866","1863cff0":"9929"}[e]||e,b.p+b.u(e)},(()=>{var e={1303:0,532:0};b.f.j=(a,c)=>{var f=b.o(e,a)?e[a]:void 0;if(0!==f)if(f)c.push(f[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var d=new Promise(((c,d)=>f=e[a]=[c,d]));c.push(f[2]=d);var t=b.p+b.u(a),r=new Error;b.l(t,(c=>{if(b.o(e,a)&&(0!==(f=e[a])&&(e[a]=void 0),f)){var d=c&&("load"===c.type?"missing":c.type),t=c&&c.target&&c.target.src;r.message="Loading chunk "+a+" failed.\n("+d+": "+t+")",r.name="ChunkLoadError",r.type=d,r.request=t,f[1](r)}}),"chunk-"+a,a)}},b.O.j=a=>0===e[a];var a=(a,c)=>{var f,d,t=c[0],r=c[1],o=c[2],n=0;if(t.some((a=>0!==e[a]))){for(f in r)b.o(r,f)&&(b.m[f]=r[f]);if(o)var i=o(b)}for(a&&a(c);n{"use strict";var e,a,c,f,t,d={},r={};function b(e){var a=r[e];if(void 0!==a)return a.exports;var c=r[e]={exports:{}};return d[e].call(c.exports,c,c.exports,b),c.exports}b.m=d,e=[],b.O=(a,c,f,t)=>{if(!c){var d=1/0;for(i=0;i=t)&&Object.keys(b.O).every((e=>b.O[e](c[o])))?c.splice(o--,1):(r=!1,t0&&e[i-1][2]>t;i--)e[i]=e[i-1];e[i]=[c,f,t]},b.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return b.d(a,{a:a}),a},c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,b.t=function(e,f){if(1&f&&(e=this(e)),8&f)return e;if("object"==typeof e&&e){if(4&f&&e.__esModule)return e;if(16&f&&"function"==typeof e.then)return e}var t=Object.create(null);b.r(t);var d={};a=a||[null,c({}),c([]),c(c)];for(var r=2&f&&e;"object"==typeof r&&!~a.indexOf(r);r=c(r))Object.getOwnPropertyNames(r).forEach((a=>d[a]=()=>e[a]));return d.default=()=>e,b.d(t,d),t},b.d=(e,a)=>{for(var c in a)b.o(a,c)&&!b.o(e,c)&&Object.defineProperty(e,c,{enumerable:!0,get:a[c]})},b.f={},b.e=e=>Promise.all(Object.keys(b.f).reduce(((a,c)=>(b.f[c](e,a),a)),[])),b.u=e=>"assets/js/"+({4:"4550e3f5",53:"935f2afb",234:"39838d4e",533:"b2b675dd",616:"d0e791fc",814:"95f4d37c",825:"7be5f79d",864:"eac03826",1218:"1be3623c",1320:"b5521830",1420:"6741c1a9",1477:"b2f554cd",1549:"1506d638",1585:"2450005c",1937:"49882d99",1974:"2b7e4bbf",2076:"477a2718",2122:"b75ab542",2535:"814f3328",2799:"954aa590",2825:"0f7bf3fd",3014:"4a78ad51",3085:"1f391b9e",3089:"a6aa9e1f",3170:"5d9c8300",3328:"8c887cbc",3576:"98dbdb4e",3608:"9e4087bc",3844:"772c3429",4e3:"afc29949",4057:"c03baef0",4195:"c4f5d8e4",4345:"2a883cfd",4371:"e82ae094",4490:"850fe7ba",5929:"b89822c9",6103:"ccc49370",6265:"906ac375",6378:"7fda5be5",6427:"59628a4d",6800:"2f9db241",6974:"af21c641",7414:"393be207",7540:"0f1af657",7918:"17896441",8277:"8175d4ae",8294:"3463d78f",8700:"d75fb1fa",8912:"0a1fe8aa",9268:"ba771284",9278:"975c475b",9514:"1be78505",9597:"ccc89dea",9866:"11f6a8a1",9929:"1863cff0"}[e]||e)+"."+{4:"7ad67dca",53:"905e9469",234:"481530d0",412:"b078622e",533:"160181bc",616:"4f89fef9",814:"76530f14",825:"fa3cc8c0",864:"7790a19c",1218:"d11c7845",1320:"b9e2999d",1420:"f3b060f6",1477:"411ee99e",1506:"15f1cca7",1549:"cbd15b9c",1585:"ae8a43a0",1937:"ccf7713b",1974:"55d36594",2076:"3fa6aca8",2122:"4c1183bd",2535:"d076fce0",2799:"b83f7460",2825:"df9b0069",3014:"b5225116",3085:"8e54e8e5",3089:"8498bf64",3170:"faed4819",3328:"1fa3675b",3576:"fcef76b1",3608:"51a28fd2",3844:"a8ebcca4",4e3:"3fe8cdd8",4057:"317b70a4",4195:"257ce74a",4345:"8444fbf6",4371:"49a40ed9",4490:"0e664365",4972:"42ae5fe7",5929:"5dd28f84",6103:"12d16554",6265:"521967b9",6378:"36e42c64",6427:"bd7dbcff",6800:"b1803862",6974:"7c140e42",7414:"880ef78c",7540:"079dcb40",7918:"f7e188b3",8277:"f2aef675",8294:"3780c9e8",8700:"11c84342",8912:"c251911e",9268:"639cc857",9278:"805e417e",9514:"a58316fc",9597:"db23280d",9866:"14e7ff50",9929:"52749a3c"}[e]+".js",b.miniCssF=e=>{},b.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),b.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),f={},t="geogardenclub-github-io:",b.l=(e,a,c,d)=>{if(f[e])f[e].push(a);else{var r,o;if(void 0!==c)for(var n=document.getElementsByTagName("script"),i=0;i{r.onerror=r.onload=null,clearTimeout(s);var t=f[e];if(delete f[e],r.parentNode&&r.parentNode.removeChild(r),t&&t.forEach((e=>e(c))),a)return a(c)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:r}),12e4);r.onerror=l.bind(null,r.onerror),r.onload=l.bind(null,r.onload),o&&document.head.appendChild(r)}},b.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},b.p="/",b.gca=function(e){return e={17896441:"7918","4550e3f5":"4","935f2afb":"53","39838d4e":"234",b2b675dd:"533",d0e791fc:"616","95f4d37c":"814","7be5f79d":"825",eac03826:"864","1be3623c":"1218",b5521830:"1320","6741c1a9":"1420",b2f554cd:"1477","1506d638":"1549","2450005c":"1585","49882d99":"1937","2b7e4bbf":"1974","477a2718":"2076",b75ab542:"2122","814f3328":"2535","954aa590":"2799","0f7bf3fd":"2825","4a78ad51":"3014","1f391b9e":"3085",a6aa9e1f:"3089","5d9c8300":"3170","8c887cbc":"3328","98dbdb4e":"3576","9e4087bc":"3608","772c3429":"3844",afc29949:"4000",c03baef0:"4057",c4f5d8e4:"4195","2a883cfd":"4345",e82ae094:"4371","850fe7ba":"4490",b89822c9:"5929",ccc49370:"6103","906ac375":"6265","7fda5be5":"6378","59628a4d":"6427","2f9db241":"6800",af21c641:"6974","393be207":"7414","0f1af657":"7540","8175d4ae":"8277","3463d78f":"8294",d75fb1fa:"8700","0a1fe8aa":"8912",ba771284:"9268","975c475b":"9278","1be78505":"9514",ccc89dea:"9597","11f6a8a1":"9866","1863cff0":"9929"}[e]||e,b.p+b.u(e)},(()=>{var e={1303:0,532:0};b.f.j=(a,c)=>{var f=b.o(e,a)?e[a]:void 0;if(0!==f)if(f)c.push(f[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var t=new Promise(((c,t)=>f=e[a]=[c,t]));c.push(f[2]=t);var d=b.p+b.u(a),r=new Error;b.l(d,(c=>{if(b.o(e,a)&&(0!==(f=e[a])&&(e[a]=void 0),f)){var t=c&&("load"===c.type?"missing":c.type),d=c&&c.target&&c.target.src;r.message="Loading chunk "+a+" failed.\n("+t+": "+d+")",r.name="ChunkLoadError",r.type=t,r.request=d,f[1](r)}}),"chunk-"+a,a)}},b.O.j=a=>0===e[a];var a=(a,c)=>{var f,t,d=c[0],r=c[1],o=c[2],n=0;if(d.some((a=>0!==e[a]))){for(f in r)b.o(r,f)&&(b.m[f]=r[f]);if(o)var i=o(b)}for(a&&a(c);n Blog | Geo Garden Club - +

· One min read

Welcome to the Geo Garden Club site!

We have decided to rebrand the "Agile Garden Club" project as "Geo Garden Club". We feel that "Geo" represents the spirit of the project better than "Agile", and besides, it's less characters to type.

- + \ No newline at end of file diff --git a/blog/2023/02/10/welcome.html b/blog/2023/02/10/welcome.html index 7a5dcabe9..7bb07882f 100644 --- a/blog/2023/02/10/welcome.html +++ b/blog/2023/02/10/welcome.html @@ -5,13 +5,13 @@ Welcome, Geo Garden Club! Aloha, Agile Garden Club! | Geo Garden Club - +

Welcome, Geo Garden Club! Aloha, Agile Garden Club!

· One min read

Welcome to the Geo Garden Club site!

We have decided to rebrand the "Agile Garden Club" project as "Geo Garden Club". We feel that "Geo" represents the spirit of the project better than "Agile", and besides, it's less characters to type.

- + \ No newline at end of file diff --git a/blog/archive.html b/blog/archive.html index ebe533613..0fbb93ad0 100644 --- a/blog/archive.html +++ b/blog/archive.html @@ -5,13 +5,13 @@ Archive | Geo Garden Club - + - + \ No newline at end of file diff --git a/docs/develop.html b/docs/develop.html index 7588cc9e9..61e38dfab 100644 --- a/docs/develop.html +++ b/docs/develop.html @@ -5,13 +5,13 @@ Welcome to the GGC Developers Guide | Geo Garden Club - +

Welcome to the GGC Developers Guide

Welcome to the Developer's Guide for the Geo Garden Club project.

Please note that the Developer's Guide consists of all pages in the /docs/develop subdirectory of the GGC website. The Developer's Guide is "partially hidden", in that these pages do not appear in the sidebar of the pages discoverable from the home page of the site, and the robots.txt file discourages crawlers from retrieving this set of pages. So, in general, to access these pages, you need to be given the URL (https://geogardenclub.com/docs/develop) by a member of the project.

That said, this hidden-ness is only partial because:

  1. If you share the URL with others, then they can access these pages. There is no access control. Please use restraint.
  2. The GitHub geogardenclub.github.io repository containing the sources for these pages is a public repository. (GGC,LLC is not yet rich enough to pay $4/seat/month for GitHub Teams.) Thus, anyone who cares to poke around in that repo can discover all of these pages.

After some thought, I have concluded that the benefits of using Docusaurus for the GGC Developer Guide outweigh the risks involved with the possibility of architectural and design information "leaking" to competitors. But, I am noting this issue up front so that everyone adding information to these pages should consider the impact of that information being exposed to outsiders at some future date.

- + \ No newline at end of file diff --git a/docs/develop/mockup/chatgpt-feedback.html b/docs/develop/mockup/chatgpt-feedback.html index 5a2b9c2b8..7ab4f5ed6 100644 --- a/docs/develop/mockup/chatgpt-feedback.html +++ b/docs/develop/mockup/chatgpt-feedback.html @@ -5,13 +5,13 @@ ChatGPT feedback | Geo Garden Club - +

ChatGPT feedback

Just for fun, I had a conversation with ChatGPT in April, 2023 about how to design a gardening app. Much of its responses were uninsightful, but it did come up with some banging ideas for badges.

Design a program to address food resiliency by support communities of practice among home gardeners.

Sure, here's a high-level design for a program that could support communities of practice among home gardeners and help improve food resiliency:

  1. Build a platform for home gardeners to connect with each other and share knowledge and resources. The platform could be a website or mobile app that allows users to create profiles, post photos and descriptions of their gardens, and ask and answer questions.

  2. Develop a system for categorizing and tagging posts so that users can easily find information relevant to their interests and needs. For example, users could tag posts with the types of plants they're growing, the climate and soil conditions in their region, and the pests or diseases they're dealing with.

  3. Recruit experienced gardeners to serve as mentors and moderators on the platform. These individuals could offer advice and guidance to less experienced gardeners, help answer questions, and keep discussions on track.

  4. Host regular online events, such as webinars or Q&A sessions, where users can learn from experts in the field and connect with each other in real time.

  5. Develop partnerships with local organizations and businesses to provide resources and support to the community. For example, the program could partner with a local nursery to offer discounts on seedlings or with a food bank to distribute surplus produce grown by community members.

  6. Encourage users to share their experiences and successes with others in the community. This could include posting photos of their gardens or sharing recipes using the produce they've grown.

  7. Provide incentives for users to participate in the community. For example, users could earn badges or points for contributing to discussions or answering questions, which could be redeemed for prizes such as gardening tools or gift cards.

By implementing these strategies, the program could help create a vibrant and supportive community of home gardeners who are working together to improve food resiliency in their local communities.

Now implement that design in Flutter.

As an AI language model, I cannot implement code. However, I can give you an overview of what a Flutter app that implements the design for supporting communities of practice among home gardeners might look like.

Firstly, the app would have a home screen with different sections for users to explore. The main sections could be:

  1. News Feed: This section would show the latest posts and updates from other gardeners in the community.

  2. Gardening Tips: This section would provide users with a collection of gardening tips and tricks. Users could also submit their own tips and tricks to share with the community.

  3. Q&A: This section would allow users to ask questions about gardening and get answers from experienced gardeners in the community. Users could also answer questions and earn badges for their contributions.

  4. Events: This section would display upcoming online events, such as webinars or Q&A sessions, where users can learn from experts in the field and connect with each other in real-time.

  5. Resources: This section would provide users with access to gardening resources such as articles, videos, and tutorials.

  6. Profile: This section would allow users to create and edit their profile, view their activity history, and connect with other gardeners.

The app would also have a search function that would allow users to search for posts, users, or resources based on keywords or tags. The app could also have a messaging feature that would allow users to communicate with each other privately.

In terms of implementing the design in Flutter, the app could use Flutter's built-in widgets to create the UI elements for the different sections of the app. For example, a ListView widget could be used to display posts in the News Feed section, while a GridView widget could be used to display resources in the Resources section.

Flutter's routing system could be used to navigate between different screens in the app, such as from the News Feed to the Q&A section. The app could also use Flutter's networking libraries to fetch data from a backend server, such as the latest posts from other gardeners.

Overall, Flutter provides a lot of powerful tools and features that can be used to implement a community-building app for home gardeners.

Make the design more oriented toward experienced gardeners.

Sure, here's an updated design that is more oriented towards experienced gardeners:

  1. Expert Insights: This section would provide in-depth articles, videos, and tutorials on advanced gardening topics such as soil science, crop rotation, and pest management. Users could also submit their own content to share with the community.

  2. Peer-to-Peer Mentoring: This section would allow experienced gardeners to mentor other gardeners in the community. Users could request a mentor or be matched with a mentor based on their interests and needs. Mentors could provide personalized advice and support to mentees through private messaging or video calls.

  3. Advanced Q&A: This section would allow users to ask and answer advanced gardening questions. Questions could be categorized by topics such as greenhouse gardening, hydroponics, or permaculture.

  4. Plant Database: This section would provide a comprehensive database of different plant varieties, including information on growth requirements, pest resistance, and culinary uses. Users could also contribute to the database by adding new plant varieties or updating existing entries.

  5. Events: This section would display upcoming events, such as conferences or workshops, where experienced gardeners can connect and learn from each other.

  6. Community Projects: This section would highlight community gardening projects, such as urban farms or community gardens, where experienced gardeners can share their skills and knowledge with others.

  7. Profile: This section would allow users to create and edit their profile, view their activity history, and connect with other experienced gardeners in the community.

The app could also have a badge system that rewards users for their contributions to the community. For example, users could earn badges for answering questions, contributing to the plant database, or mentoring other gardeners.

In terms of implementation in Flutter, the app could use more advanced UI elements such as graphs and charts to display data and statistics about plant growth and health. The app could also use more advanced networking libraries to fetch and display data from external databases or APIs.

Overall, this updated design is more focused on advanced gardening topics and provides opportunities for experienced gardeners to share their expertise with others in the community.

Provide examples of badges that would be appropriate for experienced gardeners.

(Editors note: I was so enamored of the initial responses that I prompted ChatGPT three more times for additional ideas. The final set are collapsed into this single reply.)

Sure, here are some examples of badges that would be appropriate for experienced gardeners in the app:

  1. Master Gardener: This badge could be earned by users who have completed a master gardening program or have equivalent experience and knowledge in the field.

  2. Plant Whisperer: This badge could be earned by users who have demonstrated exceptional knowledge and skills in the care and maintenance of plants.

  3. Community Builder: This badge could be earned by users who have contributed significantly to the growth and development of the gardening community, such as by organizing events or mentoring other gardeners.

  4. Pesticide-Free Gardener: This badge could be earned by users who have demonstrated a commitment to using natural and organic methods of pest control in their gardening practices.

  5. Greenhouse Guru: This badge could be earned by users who have demonstrated exceptional knowledge and skills in greenhouse gardening, such as by successfully growing a wide variety of plants in a greenhouse environment.

  6. Permaculture Pro: This badge could be earned by users who have demonstrated exceptional knowledge and skills in permaculture gardening, such as by successfully implementing permaculture principles in their garden or community project.

  7. Soil Scientist: This badge could be earned by users who have demonstrated exceptional knowledge and skills in soil science, such as by successfully improving soil quality and fertility in their garden.

  8. Seed Saving Specialist: This badge could be earned by users who have demonstrated exceptional knowledge and skills in saving and storing seeds, such as by successfully preserving heirloom varieties of plants.

  9. Composting Champion: This badge could be earned by users who have demonstrated exceptional knowledge and skills in composting, such as by successfully producing high-quality compost for use in their garden.

  10. Food Forest Pioneer: This badge could be earned by users who have successfully implemented food forest principles in their garden or community project, such as by creating a diverse and self-sustaining ecosystem that produces a variety of edible crops.

  11. Irrigation Expert: This badge could be earned by users who have demonstrated exceptional knowledge and skills in irrigation, such as by successfully designing and implementing an efficient irrigation system in their garden.

  12. Edible Landscaper: This badge could be earned by users who have successfully integrated edible plants into their landscaping, such as by creating an aesthetically pleasing and productive garden that provides both beauty and food.

  13. Beekeeper: This badge could be earned by users who have successfully kept and cared for bees, such as by producing honey or pollinating crops in their garden or community project.

  14. Farmer's Market Vendor: This badge could be earned by users who have successfully sold their garden produce at a farmer's market or other local venue, such as by developing a profitable and sustainable small-scale farming business.

  15. Vermiculture Master: This badge could be earned by users who have demonstrated exceptional knowledge and skills in vermicomposting, such as by successfully raising and caring for worms to produce high-quality compost.

  16. Orchardist: This badge could be earned by users who have successfully maintained and harvested from an orchard, such as by growing fruit trees and managing pests and diseases.

  17. Aquaponics Ace: This badge could be earned by users who have successfully implemented and maintained an aquaponics system, such as by raising fish and using their waste to fertilize plants.

  18. Foraging Pro: This badge could be earned by users who have demonstrated exceptional knowledge and skills in foraging for wild edible plants, such as by safely and sustainably harvesting a variety of wild foods.

  19. Herbalist: This badge could be earned by users who have demonstrated exceptional knowledge and skills in growing, harvesting, and using medicinal herbs, such as by successfully creating herbal remedies for various ailments.

  20. Garden Photographer: This badge could be earned by users who have demonstrated exceptional skills in capturing the beauty of their garden through photography, such as by sharing high-quality and visually appealing images of their plants and landscapes.

  21. Garden Writer: This badge could be earned by users who have demonstrated exceptional skills in writing about gardening, such as by creating engaging and informative blog posts or articles about various gardening topics.

  22. Heirloom Hero: This badge could be earned by users who have demonstrated exceptional knowledge and skills in growing and preserving heirloom varieties of plants, such as by successfully saving seeds and sharing them with the community.

  23. Landscape Designer: This badge could be earned by users who have demonstrated exceptional skills in designing and creating beautiful and functional gardens, such as by successfully incorporating different elements like paths, water features, and seating areas.

  24. Garden Educator: This badge could be earned by users who have demonstrated exceptional skills in educating others about gardening, such as by creating and delivering engaging and informative workshops or courses.

  25. Pollinator Protector: This badge could be earned by users who have demonstrated exceptional knowledge and skills in creating pollinator-friendly habitats in their garden or community project, such as by incorporating native plants, providing shelter and nesting sites, and avoiding the use of pesticides.

  26. Garden Artist: This badge could be earned by users who have demonstrated exceptional skills in creating art inspired by their garden or using garden materials, such as by producing paintings, sculptures, or installations.

  27. Fruit Tree Pruner: This badge could be earned by users who have demonstrated exceptional knowledge and skills in pruning fruit trees, such as by maintaining the trees' health and maximizing their fruit production.

  28. Sustainable Gardener: This badge could be earned by users who have demonstrated exceptional commitment to sustainable gardening practices, such as by reducing waste, conserving water, and using organic methods of pest control.

These badges would recognize users who have demonstrated exceptional knowledge and skills in various aspects of gardening and would encourage users to continue learning and sharing their expertise with the community.

- + \ No newline at end of file diff --git a/docs/develop/mockup/customer-feedback.html b/docs/develop/mockup/customer-feedback.html index 57644050e..62217d6dc 100644 --- a/docs/develop/mockup/customer-feedback.html +++ b/docs/develop/mockup/customer-feedback.html @@ -5,13 +5,13 @@ Customer feedback | Geo Garden Club - +

Customer feedback

Following the Lean Startup principle of "validated learning", we performed an evaluation study of the technology innovations present in the mockup with 24 experienced gardeners during the summer and fall of 2022. We report on the results of that study in the following 15 minute video (or 7 minutes, if you run the video at 2x speed):

If you just want to click through the slides, here they are:

The net of the customer feedback phase is that we found no compelling evidence to abandon the project.

- + \ No newline at end of file diff --git a/docs/develop/mockup/design.html b/docs/develop/mockup/design.html index 1944934ce..698c08189 100644 --- a/docs/develop/mockup/design.html +++ b/docs/develop/mockup/design.html @@ -5,13 +5,13 @@ Design and implementation | Geo Garden Club - +

Design and implementation

We built a simple single page web application using React. The goal of the system was to provide an "executable mockup" to show potential customers our vision of some of the features that would be made available in our production technology.

This system used real data collected from two gardens. You can view the mockups for each garden here:

- + \ No newline at end of file diff --git a/docs/develop/mockup/entrepreneur-feedback.html b/docs/develop/mockup/entrepreneur-feedback.html index 84cf81661..222abd97c 100644 --- a/docs/develop/mockup/entrepreneur-feedback.html +++ b/docs/develop/mockup/entrepreneur-feedback.html @@ -5,7 +5,7 @@ Entrepreneur feedback | Geo Garden Club - + @@ -14,7 +14,7 @@ Philip


Hi Philip,

I'm happy I could contribute some ideas. I suspected you were already doing many of those things :-) Jenna - It's nice to meet you.

Comments are below.

We plan to offer at least 6-9 months free use of the app for new users---essentially allowing folks to use it for one gardening season without cost. Our idea is that after that time, they will have entered a significant amount of their own gardening data into the app, resulting in a subtle kind of "lock in".

That makes sense. You can do this for Apple App Store and Google Play apps via promo codes. Make sure to create a paid app, and then use promo codes because you can't convert a free app to a paid app.

If we find too many people quitting after six months, then we could go to a free version with ads, but I'd really like to avoid ads for a variety of reasons if we can.

I'm also not a fan of using ads, but some companies don't have any other revenue model. I think AGC probably does if you can build a good premium service. For a subscription service you really need to concentrate on providing ongoing and ever-improving services such as Garden Kumu, gamification (e.g. contests, challenges, badges) and locale specific guidance from AGC experts and senior chapter members.

I suggest providing strong incentives for senior chapter members to share their knowledge. In addition to recognition for status via badges you might want to provide things like coupons for Kokua Coop, Leahi Health, etc. You may also want to (quietly) waive or discount their subscription fees.

You are so right that "serious gardener" doesn't mean "no fun required gardener"! We've thought about a variety of game mechanics (badges, contests, etc). Another angle is citizen science: there are various climate and environmental groups that have campaigns where gardeners could provide useful observations about plant growth.

That is a good idea. I would ask if you can list them as partners. It would be nice to have a list of collaborating green companies / nonprofits on your site. This lends credibility.

Your suggestion to make it social is also spot on. One primary goal of the app is to foster a high quality "community of practice" around gardening in a given geographic region. The ability to message others within the app and easily share gardening data is going to be key.

I agree. People often underestimate the effort in putting a community together on a proprietary platform. It can be done, but it's a lot of work both from a technology and a community growth / management perspective. The devil is in the details.

Regarding photos, the app will support taking photos (tagged to the garden and plant varietal) that can be shared. In fact, one thing we learned is that if we provide a kind of "Garden Instagram" within the app, then some folks who aren't even gardeners said they would subscribe to the app simply to see what their friends were doing in their gardens.

That is a great idea. Lots of people are interested in gardening but can't participate because they don't have land and / or time.

We also know (from Jenna's personal experience) that gardeners like to share pictures of their gardens and particularly beautiful/interesting plants. So, having a garden-specific "feed" seems like a fun and attractive feature. We didn't present this idea because we believe that by itself, it's not enough to make the application viable. We first have to nail the ability to support gardeners in planning and execution of their garden first. If we can succeed there, then the Garden Instagram feature will be icing on the top.

That makes sense. The main reason companies like this fail is simply failure to launch. They fail to scope an MVP that addresses the 20% of their ideas that are critical to early success.

Yes, we agree about the opportunities for partnering and sharing. We think the app can informally facilitate lots of cooperation: some folks could (for example) coordinate their succession planting with each other so that everyone always has (for example) a good supply of lettuce. There is also a huge opportunity for seed sharing. This is exciting, because locally grown seeds are often going to be better adapted to local conditions (natural selection!). In fact, Jenna is presenting our idea to a seed saving/sharing organization in the near future.

True. Communities in rural Japan are very good at this. They share seeds, they give excess produce to neighbors, and just generally function as a unified community gardening engine.

And, yes, our target demographic is Millenials and Gen Z. Thankfully, Jenna is a millennial.

👍

We eventually need to get someone even younger on the team.

:-) My 9 you is a very serious gardener. He has been trained well by grandma. Noa knows how to plant seeds with the right spacing and depth for a given plant, handle watering, fertilizing, cultivating, etc. They can start young!

Thank you for "Let me know if there is anything else I can do to help." Actually, I already have something in mind. I've been getting up to speed with Flutter and am now more-or-less competent with creating CRUD apps using Riverpod and Firebase and pushing them out to TestFlight. I want to make some more progress on an initial MVP, but at some point I would like to ask you to take a look at the UI and code and let me know where we're going terribly, terribly wrong. :) Probably early summer. I propose to take you out to lunch to discuss the results of the code review.

No problem. I've personally been doing SwiftUI work, but we have three excellent Flutter developers at Ikayzo. I've also been doing lots of app design / branding and UXD work (examples from our flamboyant Niftia customer attached.)


From: Jenna Deane

Hi E1,

Thanks for the feedback and tips! You mentioned how in rural Japan gardeners operate as a "unified community gardening engine" and that is exactly what this technology aims to do. Do I need to plan a trip to Japan to research? Obviously yes. ;)

I appreciated the emphasis on gamification - contests, badges, incentives for good recording of growing data and how that can intersect with community building. I have Spotify and love their "Wrapped" year in review. Getting interesting stats about your growing habits could be a fun element to add down the line (once we have growing habits to aggregate data from!) and could be provided in a shareable format.

I love the cross generational passing of skills between your child and his grandma! I really enjoyed working with kids in school gardens and often it was the grandparents who were most engaged with the garden. It makes me think - this technology could help the youth document the gardening practices and of their elders. What important data to record!

Very cool Niftia design! So sleek and who doesn't love a peacock? Made me want to visit the site to learn more. NFTs!

Thanks again, Jenna


Hi Jenna,

Thanks for the feedback and tips! You mentioned how in rural Japan gardeners operate as a "unified community gardening engine" and that is exactly what this technology aims to do. Do I need to plan a trip to Japan to research? Obviously yes. ;)

Absolutely. I recommend doing your research in Okinawa at the Club Med in Ishigaki. There are plenty of people with gardens in the vicinity. It will be rough, but someone has to do it.

I appreciated the emphasis on gamification - contests, badges, incentives for good recording of growing data and how that can intersect with community building. I have Spotify and love their "Wrapped" year in review. Getting interesting stats about your growing habits could be a fun element to add down the line (once we have growing habits to aggregate data from!) and could be provided in a shareable format.

That sounds great. I'd like to have that data for our community garden and grandma's gardens.

I love the cross generational passing of skills between your child and his grandma!

:-) It is a precious thing. She taught him all kinds of interesting things like how to find bamboo shoots that are the right size for digging up, and how to prepare and cook them. They are an amazing bright purple.

I really enjoyed working with kids in school gardens

That is a lot of fun, and it's a great opportunity to teach botany, chemistry, entomology and a dozen other things! Also, after spending all day on a computer, it's nice to just sink your hands into the land.

and often it was the grandparents who were most engaged with the garden.

That is certainly true in our family, and many of our boys' friends' families as well. They learn gardening from grandma and / or grandpa.

It makes me think - this technology could help the youth document the gardening practices and of their elders. What important data to record!

Absolutely. It is a great opportunity to preserve traditional gardening practices.

Very cool Niftia design! So sleek and who doesn't love a peacock? Made me want to visit the site to learn more. NFTs!

Thank you. It was / is a fun project. I think our customer was unlucky with the timing given the precipitous fall in value, but hopefully they'll make a comeback. He is very upset that Stephen Colbert makes fun of NFTs every other night. I haven't told him Stephen is my favorite comedian.

Entrepreneur 2 (E2)

I had a zoom call with E2 this morning. They seemed pretty enthusiastic about our direction and didn't bring up any warning signs about our approach and process. They had a number of interesting thoughts which I will summarize as follows:

  1. Consider "local first". They are building apps that cache data locally first, and then later upload to the web. This can improve performance, but more profoundly, can provide for much more data privacy than a traditional "sync to the cloud immediately" approach.

  2. They think it's important for us to first ensure that we design an app that folks view as a "trusted friend"---that the data is safe, that it will be there for them later in a useful form. Only then will the "social" aspects become relevant.

  3. They made a very interesting comment---they've recently taken over a plot in a community garden, and wishes there was a way to communicate and coordinate with the other folks in that community. This is kind of like a "micro-chapter". I just had the idea that we might want to support hashtags for gardens. Then folks could add #makiki-community-garden (or whatever) to their garden and filter all the analyses by that hashtag. That would basically enable users to self-organize into smaller groups within a chapter.

Entrepreneur 3 (E3)

GGC looks like a cool project, fun that you get to work with Jenna! It looks like an interesting niche, but getting a critical mass of users seems like it would be challenging in general because of the intersection of serious gardeners and the geographic span of chapters (walking & biking distance!) I would worry that it would be hard to get that combination in many places, since most apps / Internet tools thrive by having few if any geographic boundaries, but that is not possible for GGC (by design). I wonder if you could appeal to both novice & serious gardeners as a way to build community to get to critical mass, and perhaps explore the pathways by which a novice becomes serious (which I assume is a thing).

GGC Motivation page notes:

  • I would add something about using technology to introduce some of the benefits of community gardening to home gardens to the TL;DR section of the Mo, since that seems like the core problem GGC is attempting to solve.
  • I think Food Shift has shifted from the term "food deserts" to "food apartheid", since they are often the result of systemic racism, not just a natural feature of a landscape (though humans make deserts too! 😦
  • I see the Agile Garden Club name is still in the diagram in the Motivation page

Entrepreneur 4 (E4)

I want to try to recapture E4’s excellent napkin points. All I can remember so far this morning is:

  1. Avoid conflating “food resilience” (direct hit for GGC) with “food insecurity” (lots of structural barriers for GGC).
  2. Need more BIPOC representation.
  3. A hyperlocal orientation does not have to preclude other dimensions for data sharing.
  4. First page would be better written in terms of “assets” that GGC wants to help leverage better than they are currently, rather than the current “academic grant proposal introduction section” (which is the only way I know how to write first pages.)

Other points (by E4):

  1. Include social enterprises that run urban/community farms as part of their programming
  2. use tags for profiling gardening conditions, community types etc to make effective and appropriate global sharing
  3. ensure features in the platform to be able to easily share the harvest (there is significant food wasted at all stages of the food supply chain, starting with community farms).
  4. pair up with nonprofits and write grants. The license fee can be included in the grant
  5. not just BIPOCS but include people with lived experience of food insecurity in the leadership Btw I read research stating that asset framing language yields more funding (donations and grants) so maybe it works for academic grants, too.

Entrepreneur 5 (E5)

(E5 is a commercial nursery owner, and so has both entreprenurial and gardening experience.)

Aloha Philip,

I'm taking a look at the info.

In your outcome data section, the rating criteria is a great idea. Based on my personal experience, you may consider revising your ratings on pest and disease resistance. I've seldom seen garden plants without some damage. In commercial agriculture, with pests you generally do not attempt to control the pest unless it is causing significant economic damage, i.e. a few holes etc are not a reason. Only if the damage will significantly affect yields. Home gardeners should not expect fruits and veg to look like they do in the grocery store.

Outcome Appearance- as above they won't look like grocery store produce. Many places are now marketing ugly fruit and veg. It reduces food waste and cost. Some of the best tomatoes I've eaten have been ugly but the best tasting. Just a thought, my citrus is generally ugly due to insect damage on the skins, but the flavor is unaffected.

Seed saving/sharing- The vast majority of seeds have been crossbred for various attributes such as disease resistance, nematode resistance, etc.. The seeds from these plants may or maynot have the same characteristics. Commercial tomatoes were bred to withstand a 5 mile per hour impact for machine harvesting but they didn't bother to consider flavor as a criteria. Heirloom plants are open pollinated and are what we call pure line in that the seeds from the plants produce very similar to exactly the same genetically. So, sharing heirloom seeds is good but not hybrid seed.

I see that you have Charlie Reppun on your team. I don't really know him but did hang out with his brother when we lived on the big island.

I hope this was somewhat helpful.

- + \ No newline at end of file diff --git a/docs/develop/onboarding.html b/docs/develop/onboarding.html index 96f1e6411..d836d18a0 100644 --- a/docs/develop/onboarding.html +++ b/docs/develop/onboarding.html @@ -5,13 +5,13 @@ Onboarding | Geo Garden Club - +

Onboarding

Welcome, new GGC Alpha Release developer! This page provides a checklist of things required to get started developing our technology.

Site access

You will need access to the following:

  • The GGC Discord server. Request an invite from Philip or Jenna.
  • The GGC GitHub organization. Please send Philip your GitHub username and he will invite you.
  • The ggc_app Firebase project. Please send Philip your gmail account name and he will add you.

Proficiency in Dart and Flutter

We assume that you already have basic proficiency in Dart and Flutter. If you are not sure of your proficiency, then we recommend that you work through the Dartapalooza and Flutterpalooza modules of Philip's mobile application development course.

Assignment of rights

Before you can contribute code to this project, you will need to sign a document that assigns the ownership of the code you contribute to Geo Garden Club, LLC. Please contact Philip or Jenna for details on how to do this.

Developer workflow

We use a basic process for development:

  • Tasks are specified in a GitHub project board. The project board for GGC is available at: https://github.com/orgs/geogardenclub/projects/1/views/1

  • Code is developed using a branch-and-merge model. Please name the branch "issue-XXX", where XXX is the issue number associated with the task associated with your coding.

  • If you are making a trivial fix, feel free to commit directly to the main branch.

  • We run a CI task to ensure that all code committed to the main branch passes dart analyze without triggering warnings or errors.

- + \ No newline at end of file diff --git a/docs/develop/release-1.0/architecture.html b/docs/develop/release-1.0/architecture.html index a0241a36a..3988172a0 100644 --- a/docs/develop/release-1.0/architecture.html +++ b/docs/develop/release-1.0/architecture.html @@ -5,13 +5,13 @@ Architecture | Geo Garden Club - +

Architecture

The GeoGardenClub app (GGC) conforms (most of the time) to the architectural approach advocated by Andreas Bizzotto which he calls the "Riverpod Architecture". If you are not familiar with this approach, it's worth spending a few minutes reading through his description, which is available as a set of readings in the architecture module in my mobile application development course.

Client-server architecture perspective

To begin, GGC can be viewed as a simple client-server application: there is a central back-end server (in our case, Firestore) that communicates with front-end clients (in our case, the Flutter ggc_app application):

Layered application architecture perspective

In the above diagram, the client app is structured as four layers. This layering is strict, in that each layer communicates only with the layer above and below it.

Repository Layer. This bottom-most layer implements generic code for communication with Firebase: querying collections for documents; adding, deleting, and modifying documents, and so forth. In this layer, data is represented in JSON format (for entities) or binary format (for images).

Data Layer. The data layer implements "feature-specific" communication with Firebase. For example, the Data Layer code for the Chapter feature implements classes that queries the Chapter collection in appropriate ways. In this layer, data is still represented as JSON or binary.

Domain Layer. The domain layer implements code to translate between the data representations used at the data layer (i.e. JSON and Binary) and the data representations used at the Presentation Layer (i.e. Dart classes for entities and collections). The domain layer also implements the "business logic" of the application as discussed in the Collections and business logic section.

Presentation Layer. The presentation layer implements the Flutter-based user interface. All of the classes at the presentation layer are Widgets. GGC divides UI classes into two types: "Screens" and "Views". Screens implement a "top-level" page: they return a Scaffold Widget and can be routed to. Views are "components": they are the building blocks for Screens and can potentially appear in multiple Screens.

Directory structure perspective

There is a relatively straightforward correspondence between the above layers and the directory structure in the ggc_app repository. The top-level of the repo is more or less like any Flutter app. Here is a semi-annotated version of most of the top-level files and directories:

ggc_app/
.github/ # CI GitHub Actions
android/
assets/
ios/
lib/ # Source code here
linux/
macos/
stories/ # Monarch stories here.
test/
web/
windows/
analysis_options.yaml
build_runner.sh # Useful if you change data model.
lakos.sh # Build a diagram of the architecture
pubspec.yaml
run_monarch.sh # Run the Monarch UI Story system

The lib/ directory is where most of the action is. Here's a semi-annotated perspective of some the top-level of the lib/ directory:

lib/
features/ # Feature-based organization for Data, Domain, and Presentation layers
repositories/ # Implements the "Repository" layer
main.dart # Main entry point
router.dart # Implements routes using go_router
theme_data.dart # Implements a theme using FlexColorScheme

Finally, here's a look inside the features/ directory:

features/
authentication/ # Authentication using firebase_ui_auth.
/presentation # Implementation only requires UI widgets.

common/ # Cross-cutting code

chapter/ # Implementation of Chapter feature
data/ # Firebase interface
domain/ # Chapter, ChapterCollection, etc.
presentation/ # ChapterIndexScreen, ChapterView, etc.

crop/
garden/
gardener/
home/
observation/
:
:

Each feature can have one or more of the following subdirectories: domain/, data/, and presentation/. The authentication feature only requires a presentation/ subdirectory, while the chapter feature requires all three.

- + \ No newline at end of file diff --git a/docs/develop/release-1.0/coding-standards.html b/docs/develop/release-1.0/coding-standards.html index 668d393b3..837ca9126 100644 --- a/docs/develop/release-1.0/coding-standards.html +++ b/docs/develop/release-1.0/coding-standards.html @@ -5,13 +5,13 @@ Coding Standards | Geo Garden Club - +

Coding Standards

In GGC, coding standards are similar to design patterns, but focus on practices that reduce or avoid "technical debt".

Technical debt refers to implementation practices that result in the need for refactoring of the code base at a future time.

Coding standards apply to main branch only

The following standards apply only to code that you are about to merge into the main branch. You may want to violate these standards temporarily during initial development of a feature in your non-main branch. That's OK.

Delete debugging/unused code

Often during development, you will insert debugging statements to help diagnose a problem. For example:

home_screen_observations_view.dart
 Widget build(BuildContext context) {
// logger.d('HomeScreenObservationsView.build $chapters');
// logger.d('HomeScreenObservationsView.build ${chapters.observations}');
// logger.d('HomeScreenObservationsView.build ${chapters.observations.size()}');
// logger.d('current user: ${users.currentUser}');
// logger.d('current gardener: ${gardens.gardeners.getGardener(users.currentUserID)}');
// List<String> chapterNames = chapters.getChapterNames();
List<Observation> observations = chapters.observations.getAllObservations();

Or, you might try one way to implement a feature, but eventually decide upon another way. For example:

home_screen_observations_view.dart
child: ListView(
// children: observations
// .map((observation) => InstagramCard(
// observation: observation,
// chapterName: chapterNames[0],
// observations: chapters.observations,
// tags: chapters.tags,
// users: users))
// .toList()));
children: observations.map((observation) => ObservationCard(observation: observation, chapters: chapters, gardens: gardens, users: users)).toList()));

Rather than comment out debugging or unused code, please delete it prior to merging into main. Deleting this code improves the signal-to-noise ratio for future readers. In the case of debugging statements, it is easy to re-insert them later if needed (and often, you will want to inspect different values later, so the commented lines aren't helpful).

If you are concerned about deleting potentially valuable code, then feel free to copy the file into the graveyard/ directory prior to deleting the commented out code.

I can think of one possible exception to this rule: you are debating between two alternative implementations, and you want others to experiment by commenting out one alternative and then the other. But in all the cases I can think of, we have decided these kinds of issues via screen shots rather than code.

Don't inline multi-statement callbacks

We want to keep code modular and avoid deeply indented code. Deeply indented code is more difficult to read, and (because it's deeply indented) more cognitively demanding to understand.

One good heuristic to avoid deeply indented code is to not inline multi-line callbacks. For example, consider the following implementation of a PopupMenuButton:

task_card.dart
                PopupMenuButton(
initialValue: _selectedMenu,
onSelected: (SampleItem result) {
setState(() {
_selectedMenu = result;
});
switch (result) {
case SampleItem.editTask:
context.pushNamed(AppRoute.editTask.name,
pathParameters: {'taskID': widget.task.taskID});
break;
case SampleItem.gardenDetails:
context.goNamed(AppRoute.gardenDetails.name,
pathParameters: {
'gardenID': widget.task.gardenID
});
break;
// case SampleItem.editPlanting:
// context.pushNamed(AppRoute.planting.name,
// pathParameters: {
// 'gardenID': widget.task.gardenID,
// 'plantingID': widget.task.plantingID
// });
// break;
}
},
itemBuilder: (BuildContext context) => popupMenuItems),

In this case, some of the code is indented 34 spaces, using up almost half of the allotted 80 character line width.

To avoid this situation, notice that the onSelected: argument is an inline callback, which could be easily rewritten as a local function:

void _onSelected(SampleItem result) {
setState(() => _selectedMenu = result);
switch (result) {
case SampleItem.editTask:
context.pushNamed(AppRoute.editTask.name,
pathParameters: {'taskID': widget.task.taskID});
break;
case SampleItem.gardenDetails:
context.goNamed(AppRoute.gardenDetails.name,
pathParameters: {'gardenID': widget.task.gardenID});
break;
}
}

And then provided as the callback value as follows:

                  PopupMenuButton(
initialValue: _selectedMenu,
onSelected: _onSelected,
itemBuilder: (BuildContext context) => popupMenuItems),

This rewrite makes it easier to understand the PopupMenuButton invocation (because it is now only four lines long) as well as the onSelected callback (because it now has access to almost the full 80 character line width).

This PopupMenuButton code snippet is also useful because it illustrates the situation in which inlining a callback is appropriate! This is when the callback is a one-liner, such as the argument to the itemBuilder: parameter.

Don't inline form field definitions

Another situation that often leads to deeply indented code is when form field definitions are inline. For example, consider the first 30 lines of this call to FormBuilder:

add_outcome_screen.dart
                       FormBuilder(
child: Column(
children: [
Text('Outcome for $plantingName.',
style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 5),
const Text(
'Please rate the following on a scale of 1 to 5, with 5 being the best.'),
const SizedBox(height: 10),
FormBuilderSlider(
key: _germinationFieldKey,
name: 'Germination',
min: 1,
max: 5,
divisions: 4,
decoration: ggcInputDecoration(
label: 'Germination',
required: true,
hintText: ''),
initialValue: _germinationValue,
valueTransformer: (value) {
return value!.toInt();
},
onChanged: (value) {
setState(() {
_germinationValue = value!;
});
},
),

This is hard to read due to all the indentation, and it also has the potential to create very long form definitions. In this case, the complete call to FormBuilder is 200 lines long!

To create more readable code, and also to create opportunities for reuse, define form fields as widgets in the lib/common/input-fields directory. For example, here is the definition for a text field that allows the user to name (or rename) their garden:

garden_text_field.dart
class GardenTextField extends StatelessWidget {
const GardenTextField(
{super.key, required this.gardens, this.onTap, this.currName});

final GardenCollection gardens;
final void Function(String value)? onTap;
final String? currName;


Widget build(BuildContext context) {
String fieldName = 'Garden Name';
return FieldPadding(
child: FormBuilderTextField(
name: fieldName,
key: FieldKey.gardenTextField,
decoration: ggcInputDecoration(
label: fieldName,
required: true,
hintText: '4-20 chars, betanumeric/spaces, unique',
),
initialValue: currName,
validator: FormBuilderValidators.compose([
GgcValidators.validName(),
GgcValidators.uniqueGardenName(gardens, currName)
])),
);
}
}

Now you can use it in a call to FormBuilder, where the total number of lines in the definition will typically be only a few more than the total number of fields:

add_garden_screen.dart
  FormBuilder(
key: _formKey,
child: Column(
children: [
GardenTextField(gardens: widget.gardens),
const SingleImagePicker(required: false),
EditorsTextField(users: widget.users),
FormButtons(onSubmit: onSubmit, onCancel: onCancel),
],
),
);

As a bonus, GardenTextField is used in both the AddGarden form and the EditGarden Form, which avoids duplicate code.

Also note that the onSubmit: and onCancel: callbacks are not inlined, conforming to the prior coding standard.

Avoid deep indentation

The prior two coding standards should significantly reduce the depth of indentation, but there may be other situations which result in deeply indented code.

As a heuristic, if indentation exceeds 5 or 6 levels, think about creating local functions to encapsulate semantically meaningful units of functionality, and then invoking them instead of inlining all of the code.

Don't write media-adaptive code

For the beta release, we are not going to optimize layout for different screen sizes. So, please do not (for example) use MediaQuery to adjust values for different screen sizes. For example:

double width = MediaQuery.of(context).size.width;
if (!widget.readOnly) {
// compensate for the checkbox
width = width - 50;
}
if (width > 400) {
// horizontal mode so remove more.
width = width - 110;
}

The reason for this is to avoid: (a) investing time into writing code that we might abandon later once we decide on a comprehensive approach to screen-dependent layout, and (b) an inconsistent UI that is sometimes adaptive and sometimes not.

For more information on this issue, see:

If you need to adjust the screen size for some other reason, that's OK.

Use named routes

Use named routing. For example, write this:

onPressed: () =>
context.pushNamed(AppRoute.editObservation.name, pathParameters: {'observationID': widget.observation.observationID, 'gardenID': widget.observation.gardenID}),

Not this:

onPressed: () =>
context.push('/editObservation/${widget.observation.observationID}/${widget.observation.gardenID}');

The reason is that if you change the path, you will have to change all the links to that path. If you use named routing, you only have to change the path in one place.

Prefer widgets to helper methods

It is possible to create "helper" functions that return widgets, such as:

lib/common/functions/make_fab.dart
FloatingActionButton makeFAB(String route, BuildContext context) {
return FloatingActionButton(
onPressed: () {
context.pushNamed(route);
},
child: const Icon(Icons.add),
);
}

FloatingActionButton makeFABWithParameters(
String route, Map<String, String> pathParameters, BuildContext context) {
return FloatingActionButton(
onPressed: () {
context.pushNamed(route, pathParameters: pathParameters);
},
child: const Icon(Icons.add),
);
}

There are several reasons why it is better to create widgets than helper methods, as is explained here:

In this case, here's what the stateless widget version would look like:

lib/common/widgets/ggc_fab.dart
class GgcFAB extends StatelessWidget {
const GgcFAB(
{super.key, required this.route, this.pathParameters = const {}});

final String route;
final Map<String, String> pathParameters;


Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: () {
context.pushNamed(route, pathParameters: pathParameters);
},
child: const Icon(Icons.add),
);
}
}

The following code illustrates the very minimal differences in how they are called:

  Widget? getFloatingActionButton(BuildContext context, int selectedIndex) {
if (selectedIndex == 0) {
return GgcFAB(route: AppRoute.createPlanting.name);
}
if (selectedIndex == 3) {
return makeFABWithParameters(AppRoute.createGardenTask.name,
{'gardenID': widget.gardenID}, context);
}
return null;
}

It gets a little nicer if you convert to the stateless widget approach entirely, since you can tighten up the return type and remove the context argument:

  GgcFAB? getFloatingActionButton(int selectedIndex) {
if (selectedIndex == 0) {
return GgcFAB(route: AppRoute.createPlanting.name);
}
if (selectedIndex == 3) {
return GgcFAB(route: AppRoute.createGardenTask.name,
pathParameters: {'gardenID': widget.gardenID});
}
return null;
}

Don't repeat titles

The title should appear in the scaffold. It does not need to be repeated in the body:

Prefer late to dummy field values

Sometimes you need to create an entity that has required fields before you know what those fields are. It is tempting to create a "dummy" entity with clearly incorrect values and then overwrite the fields once you know what the correct values are. For example, here's some code from TaskCard:

    Planting updatedPlanting = Planting(
plantingID: 'plantingID',
chapterID: 'chapterID',
gardenID: 'gardenID',
cropID: 'cropID',
cropName: 'cropName',
lastUpdate: DateTime.now());
switch (task.taskType) {
case 'sow':
updatedPlanting = planting.copyWith(
startDate: completedDate, lastUpdate: DateTime.now());
break;
case 'transplant':
updatedPlanting = planting.copyWith(
transplantDate: completedDate, lastUpdate: DateTime.now());
break;
case 'firstHarvest':
updatedPlanting = planting.copyWith(
firstHarvestDate: completedDate, lastUpdate: DateTime.now());
break;
case 'endHarvest':
updatedPlanting = planting.copyWith(
endHarvestDate: completedDate, lastUpdate: DateTime.now());
break;
case 'pull':
updatedPlanting = planting.copyWith(
pullDate: completedDate, lastUpdate: DateTime.now());
break;
case 'other':
// TODO: implement other what do we do if they are finishing a non planting task?
break;
}

You can make the code shorter, and communicate your intent more clearly, by using the late keyword:

    late Planting updatedPlanting;
switch (task.taskType) {
case 'sow':
updatedPlanting = planting.copyWith(
startDate: completedDate, lastUpdate: DateTime.now());
break;
case 'transplant':
updatedPlanting = planting.copyWith(
transplantDate: completedDate, lastUpdate: DateTime.now());
break;
case 'firstHarvest':
updatedPlanting = planting.copyWith(
firstHarvestDate: completedDate, lastUpdate: DateTime.now());
break;
case 'endHarvest':
updatedPlanting = planting.copyWith(
endHarvestDate: completedDate, lastUpdate: DateTime.now());
break;
case 'pull':
updatedPlanting = planting.copyWith(
pullDate: completedDate, lastUpdate: DateTime.now());
break;
case 'other':
// TODO: implement other what do we do if they are finishing a non planting task?
break;
}

A more important reason to use late is that if you fail to initialize the entity, you will get a runtime error that clearly indicates the problem, rather than a runtime error that initially seems unrelated (i.e. failure to find a chapterID).

Case Study: Task Card

I've recently refactored the code for Task Cards and believe a short description of the experience could provide some insight into our current design and coding best practices.

The GGC Task Card (at the time of writing) looked like this:

As you can see, the "description" is a little wordy. My initial goal was to simply change the implementation of this card so that the description would be more tabular in nature, provide the garden and bed names (if available) from the task document, and include the description field only in the case of "custom" tasks.

The problem

So, I went to task_card.dart, and discovered this:

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:jiffy/jiffy.dart';

import '../../../repositories/firestore/firestore_path.dart';
import '../../../repositories/firestore/firestore_service.dart';
import '../../../router.dart';
import '../../chapter/domain/chapter_collection.dart';
import '../../common/widgets/ggc_card.dart';
import '../../common/widgets/ggc_loading_indicator.dart';
import '../../garden/domain/garden_collection.dart';
import '../../global_snackbar.dart';
import '../../planting/domain/planting.dart';
import '../../user/domain/user_collection.dart';
import '../domain/task.dart';

class TaskCard extends StatefulWidget {
final Task task;
final ChapterCollection chapters;
final GardenCollection gardens;
final UserCollection users;
final bool readOnly;

const TaskCard(
{super.key,
required this.task,
required this.chapters,
required this.gardens,
required this.users,
required this.readOnly});


State<TaskCard> createState() => _TaskCardState();
}

enum TaskCardAction { updateTask, deleteTask }

class _TaskCardState extends State<TaskCard> {
final _service = FirestoreService.instance;
bool _isWorking = false;
bool isChecked = false;
TaskCardAction? _selectedAction;

Future<Planting> getPlanting(String plantingID) {
return _service.fetchDocument(
path: FirestorePath.planting(plantingID),
builder: (data, documentId) => Planting.fromJson(data!));
}


Widget build(BuildContext context) {
DateTime now = DateTime.now();
bool late = widget.task.dueDate.isBefore(now);
final difference = widget.task.dueDate.difference(now);
final days = difference.inDays;
String dateStr = '';
if (days > 60) {
dateStr = DateFormat.yMd().format(widget.task.dueDate);
} else {
dateStr = Jiffy.parseFromDateTime(widget.task.dueDate).fromNow();
}
TextStyle? textStyle;
if (late) {
textStyle = TextStyle(
color: Theme.of(context).colorScheme.error,
// fontWeight: FontWeight.bold
);
}
double width = MediaQuery.of(context).size.width;
if (!widget.readOnly) {
// compensate for the checkbox
width = width - 50;
}
width = width - 120;
List<PopupMenuEntry<TaskCardAction>> popupMenuItems = [
const PopupMenuItem<TaskCardAction>(
value: TaskCardAction.updateTask,
child: Text('Update Task'),
),
const PopupMenuItem(
value: TaskCardAction.deleteTask, child: Text('Delete Task'))
];

return _isWorking
? const GgcLoadingIndicator()
: GgcCard(
child: ListTile(
dense: false,
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
horizontalTitleGap: 6,
//Code runs with this line commented out but theme isn't used.
// tileColor: tileColor,
title: Row(
children: [
SizedBox(
width: width,
child: Text(widget.task.title,
style: textStyle,
softWrap: false,
overflow: TextOverflow.ellipsis)),
const Spacer(),
PopupMenuButton(
initialValue: _selectedAction,
onSelected: (TaskCardAction result) {
setState(() {
_selectedAction = result;
});
switch (result) {
case TaskCardAction.updateTask:
context.pushNamed(AppRoute.taskUpdate.name,
pathParameters: {
'taskID': widget.task.taskID,
'gardenID': widget.task.gardenID
});
break;
case TaskCardAction.deleteTask:
context.pushNamed(AppRoute.taskDelete.name,
pathParameters: {
'gardenID': widget.task.gardenID,
'taskID': widget.task.taskID
});
break;
}
},
itemBuilder: (BuildContext context) => popupMenuItems),
],
),
subtitle: Text('${widget.task.description} Due $dateStr',
style: textStyle),
isThreeLine: true,
leading: !widget.readOnly
? Checkbox(
checkColor: Theme.of(context).primaryColor,
fillColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return Theme.of(context)
.primaryColor; // Color when checkbox is checked
}
return Colors
.transparent; // Transparent fill color when checkbox is not checked
}),
value: isChecked,
onChanged: (bool? value) async {
if (value == true) {
DateTime? completedDate = await showDatePicker(
context: context,
helpText:
'When did you complete ${widget.task.title}?',
initialDate: widget.task.dueDate,
firstDate: DateTime(2020),
lastDate: DateTime((DateTime.now().year + 1)));
if (completedDate != null) {
setState(() {
_isWorking = true;
});
updatePlanting(widget.task, completedDate)
.then((_) => setState(() {
_isWorking = false;
}));
}
}
},
)
: null,
),
);
}

Future updatePlanting(Task task, DateTime completedDate) async {
String plantingID = widget.task.plantingID;
Planting planting = await getPlanting(plantingID);
late Planting updatedPlanting;
switch (task.taskType) {
case 'sow':
updatedPlanting = planting.copyWith(
startDate: completedDate, lastUpdate: DateTime.now());
break;
case 'transplant':
updatedPlanting = planting.copyWith(
transplantDate: completedDate, lastUpdate: DateTime.now());
break;
case 'firstHarvest':
updatedPlanting = planting.copyWith(
firstHarvestDate: completedDate, lastUpdate: DateTime.now());
break;
case 'endHarvest':
updatedPlanting = planting.copyWith(
endHarvestDate: completedDate, lastUpdate: DateTime.now());
break;
case 'pull':
updatedPlanting = planting.copyWith(
pullDate: completedDate, lastUpdate: DateTime.now());
break;
case 'other':
// TODO: implement other what do we do if they are finishing a non planting task?
break;
}
// update the planting if completed
if (updatedPlanting.plantingID != 'plantingID') {
_service
.setData(
path: FirestorePath.planting(updatedPlanting.plantingID),
data: updatedPlanting.toJson())
.then((val) => GlobalSnackBar.show('Planting update succeeded.'))
.catchError((e) =>
GlobalSnackBar.show('Planting update failed\n${e.toString()}.'));
}
// remove the task
deleteTask(task);
}

Future deleteTask(Task task) async {
_service
.deleteData(path: FirestorePath.task(task.taskID))
.then((val) => GlobalSnackBar.show('Task delete succeeded.'))
.catchError(
(e) => GlobalSnackBar.show('Task delete failed\n${e.toString()}.'));
}
}

Here are a few of the things I noticed about task_card.dart:

  • It is over 200 LOC. Generally, our top-level Card implementations are around 50 LOC. This is a red flag.
  • The implementation is what I would call "flat", or "inline". In other words, there is no modularization of the TaskCard UI components. You can see this by looking at the import statements: there is not a single import of a widget in the same directory.
  • The code to implement the popup menu is approximately 35 LOC, but is scattered across 100 LOC.
  • The implementation of a UI component (TaskCard) includes code making asynchronous database calls. Our current best practice calls for the use of "mutator controllers" to bridge between UI components and the backend database.

These issues make understanding this single file of code difficult. For example:

  • How and where should I change to code to conditionally format the description field based on task type?
  • What are the functions of the _isWorking, _isChecked, and _selectedAction state variables?
  • Changes or enhancements following this "inline" design will make this code even more complicated. At some point, it will become very difficult to understand and maintain.

One solution

There are two simple design patterns that I used to modularize and simplify the code so that I could implement my table-based description enhancement.

I made each visible UI component into its own widget. Looking at the TaskCard, an obvious top-level decomposition is into two Widgets: a "Title" widget and a "Description" widget. The "Title" widget can be further decomposed into three widgets: a "Checkbox", "Title", and "PopUp Menu". The following annotated screenshot of the TaskCard illustrates this breakdown with the top-level decomposition in red and the nested decomposition in green:

I used the mutator controller design pattern to move the database access code out of the UI component and into the controller. Interestingly, this not only made the DB access code more simple, it even made it a bit more efficient because multiple collections needed updates and the mutator controller supports batch updates.

After implementing these changes, task_card.dart now looks like this:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../chapter/domain/chapter_collection.dart';
import '../../common/widgets/ggc_card.dart';
import '../../common/widgets/ggc_error.dart';
import '../../common/widgets/ggc_loading_indicator.dart';
import '../../garden/domain/garden_collection.dart';
import '../../global_snackbar.dart';
import '../../user/domain/user_collection.dart';
import '../domain/task.dart';
import 'mutate_task_controller.dart';
import 'task_card_description.dart';
import 'task_card_title_row.dart';

typedef OnCompletedCallback = void Function(Task task, DateTime completedDate);

class TaskCard extends ConsumerWidget {
final Task task;
final ChapterCollection chapters;
final GardenCollection gardens;
final UserCollection users;
final bool readOnly;

const TaskCard(
{super.key,
required this.task,
required this.chapters,
required this.gardens,
required this.users,
required this.readOnly});


Widget build(BuildContext context, WidgetRef ref) {
void onCompleted(Task task, DateTime completedDate) {
ref.read(mutateTaskControllerProvider.notifier).completeTask(
task: task,
completedDate: completedDate,
onSuccess: () {
GlobalSnackBar.show('Task completed.');
});
}

AsyncValue asyncUpdate = ref.watch(mutateTaskControllerProvider);
return asyncUpdate.when(
data: (_) => GgcCard(
child: Column(children: [
TaskCardTitleRow(task: task, onCompleted: onCompleted),
TaskCardDescription(task: task),
])),
loading: () => const GgcLoadingIndicator(),
error: (e, st) => GgcError(e.toString(), st.toString()));
}
}

Let's see how the problems with the original implementation have been addressed.

First, the size of task_card.dart is now around 50 lines of code, back to a typical size for a GGC "Card" UI component.

Second, the UI code is modularized into five widgets: TaskCard, TaskCardTitleRow, TaskCardDescription, TaskCardCheckbox, and TaskCardPopupMenu.

Third, the code to implement the PopupMenu is now encapsulated within a single widget. Interestingly, this refactoring revealed that there is a popup menu in ObservationCard with a very similar structure! It would be straight forward to do an additional refactoring to create a single generic popup menu (for example, GgcPopupMenu) that can be used anywhere we need one.

Fourth, as already noted, the asynchronous DB access code is now entirely encapsulated within the completeTask method of the mutator. The completeTask method is 25 LOC, while the original inline approach required approximately 60 LOC. That is a significant simplification.

Finally, here's what my new version of TaskCard looks like:

The top and bottom tasks are "implicit" tasks (based on Planting dates), while the middle task is an "explicit" task (defined by the gardener.)

No code is ever "perfect" or "complete". I am sure that there are more improvements to be made to TaskCard. But I hope this case study helps improve our collective intuition about how to design and implement Flutter code.

- + \ No newline at end of file diff --git a/docs/develop/release-1.0/cvp.html b/docs/develop/release-1.0/cvp.html index b86ef7039..7695e446a 100644 --- a/docs/develop/release-1.0/cvp.html +++ b/docs/develop/release-1.0/cvp.html @@ -5,13 +5,13 @@ Core Value Propositions | Geo Garden Club - +

Core Value Propositions

The goal of the 1.0 (Beta) release is to provide an app to a small group that will use the app and provide us with feedback. The 1.0 release will partially test our business model by helping us evaluate its success at implementing the "core value propositions" (CVPs) for GGC.

Core value propositions are the minimal set of capabilities that must be provided to users in order for them to find the application to be of acceptable value (i.e. they will want to use the app to support their gardening practices.)

Core Value Propositions come in two flavors:

  • Unique value propositions. These are capabilities that, to our knowledge, are not available in any other app.
  • Non-unique value propositions. These are capabilities that may be available in some form in other apps, but which we have to provide in our app in order for the app to be of minimally acceptable value.

For each of the following four CVPs, we will indicate which aspects are unique, and which are non-unique.

1. Effective support for chapters.

All users are associated with a single chapter based upon their zip code. For the 1.0 release, only a single chapter will be supported.

The concept of a chapter, and an associated geographic boundary for shared data, is a unique value proposition of GGC. To our knowledge, no other garden planning app has this capability.

Effective support means:

  • Users find the geographic boundary of their chapter to be useful,
  • Users are comfortable sharing data only within the boundaries of their chapter.

Design implications include:

  • Upon initial signin, the user must enter a zip code. This is used to identify their chapter. One cannot edit their zip code once entered in order to prevent users from "skipping around" to different chapters. For the beta release, the only valid zip codes are those associated with Whatcom-WA.
  • We will implement a Firebase collection that maps zip codes to chapter names. Zip codes are initially mapped to county names, so there will exist a chapter for every zip code. The beta release will not require this collection.

2. Effective support for garden planning.

The 1.0 release provides the ability for users to easily define one or more gardens. Each garden consists of a number of beds. Beds contain plantings. Plantings consist of crops or varieties, plus several important dates and (potentially) observations.

Garden planning is not, in general, a unique value proposition of GGC. There are many garden planning apps. Our goal is to be competitive with any other garden planning app with respect to the user experience. In addition, there are a couple of value propositions unique to GGC due to Chapters.

Effective support means:

  • Users find that GGC has an above threshold feature set for garden planning.
  • In general, users find GGC to be as easy (or hopefully easier) to use for planning than whatever they were doing before.
  • If some aspect of GGC planning is more complicated, then the additional complexity has a positive return on investment for users.

Design implications include:

  • Ability to define and represent gardens, beds, seeds, plantings, crops, varieties, dates, observations.
  • Useful representations for the plan, such as a timeline view and/or calendar view.
  • The ability to specify a planting in general terms (i.e. as a crop) during planning, then later narrow it to a specific variety.
  • The ability to view other gardens in the chapter. (This is a unique value proposition.)
  • The ability to review historical outcome data (and perhaps other prior experiences with the crop or variety) from other chapter members in order to improve seed selection and other aspects of planning. (unique value proposition)
  • The ability to see garden plans for the season of interest under development by other members. (unique value proposition)

3. Effective support for garden management.

The 1.0 release supports garden management. We use "management" to refer to features of the app that are used during the actual growing season, as opposed to "planning", which refer to features that are used prior to the actual growing season.

There are other garden planning apps that support garden management. However, the ability of GGC to provide information about garden management practices of other users in the chapter should make garden management a significant value proposition for GGC.

Effective support means:

  • Users find that GGC has an above threshold feature set for garden management.
  • Users find GGC to be as easy (or easier) to use for management than whatever they were doing before.
  • If there are aspects of GGC garden management that are more complicated, then the additional complexity has a positive return on investment for users.

Design implications include:

  • A Task list that shows upcoming activities or events, generated from the planting data.
  • Notifications of interesting events or observations in other gardens in the chapter, provided via a "Feed" or some other page. (unique value proposition)
  • Ability to allow a group of users to collaboratively manage a single garden. (unique value proposition)

4. Effective support for a community of practice.

The 1.0 release provides mechanisms that facilitates the formation of a "community of practice" within a chapter.

If successful, the ability of GGC to facilitate the creation of communities of practice would be a unique value proposition.

Effective support means:

  • Users obtain demonstrable value from interactions with other chapter members and access to their garden plans and management.
  • Users do not feel negatively about the information sharing designed into the app.

Design implications include:

  • The ability to message other chapter member(s) (either as a "broadcast" message or a DM).
  • The ability to take pictures, annotate them, and share with other chapter members.
  • The ability to access aggregate chapter outcome data about crops and varietals.
  • The ability to see information about other gardens, including the beds and plantings, both current and historical.
  • The ability to indicate that you have seeds for a particular varietal that you are willing to share in the community.

Features outside scope of 1.0 release

To clarify what will be in the 1.0 release, it is also useful to clarify what will not be in this release. The excluded features include:

  • Allowing a user to be a member of multiple chapters.
  • Sharing of data beyond the members of a chapter.
  • A "public", web-based view of a garden that can be shared to anyone with the URL.
  • Climate data.
  • Chapter Chairs, who can moderate, promote, and otherwise manage the chapter.
  • User-defined Hashtags, and the ability to filter gardens, plantings, observations, etc by the hashtag.
  • Disconnected operation. The beta release will require an internet connection to enable full capabilities. It might implement a local cache so it would be possible to provide read-only access to the garden plans and data when not connected to the internet.
- + \ No newline at end of file diff --git a/docs/develop/release-1.0/deployment.html b/docs/develop/release-1.0/deployment.html index 9c074c74a..8b1f919cd 100644 --- a/docs/develop/release-1.0/deployment.html +++ b/docs/develop/release-1.0/deployment.html @@ -5,13 +5,13 @@ Deployment | Geo Garden Club - +
-

Deployment

For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet.

Firebase App Distribution

For the Beta Release, we are using Firebase App Distribution as the deployment mechanism. This has the following implications:

  1. We deploy to iOS, Android, and the web.
  2. We must obtain the email address for every user who wishes to have GGC on their device.

Documenting deployment versions

We expect to make many deployments during the Beta release period as we fix bugs or implement enhancements. Each new deployment will require a new version number (specified in the pubspec.yml file), and we will document what has changed in each new version via CHANGELOG.md. To manage version numbers and the changelog file, we will use Cider.

We also want to be able to access the ChangeLog inside the deployed app---this is a simple way for users to both know what version of the app they have installed, and what new features or changes they can expect to find in a new version. In order to implement that, there is a script called run_cider.sh that runs the cider command and also copies the resulting CHANGELOG.md file into the assets/changelog directory so that it will be bundled and deployed with the app, and available to users within their Settings screen.

We will adhere to two standards:

  1. For the changelog format, we will adhere to Keep a Changelog.
  2. For the version number format, we will adhere to Semantic Versioning. For the beta release, since there is no public "API", the major version will always be "1". We will increment the minor version when there are new UI or business logic enhancements to the application, and we will increment the patch version in the event of deployments made only to fix bugs. We will not use the pre-release or build suffix notation.

Deployment management

The deployment process is handled by a single developer referred to as the "Deployment Manager" (DM). Initially, Philip will be the DM.

1. Update the ChangeLog

Invoke ./run_cider log added <message> to document new additions (or use changed or fixed) since the last release. Enclose the message in quotes. For example:

./run_cider.sh log added "Terms and Conditions"

Invoke ./run_cider.sh bump minor to increment the version number in pubspec.yml.

Invoke ./run_cider.sh release to update the ChangeLog to indicate that a new release with the current version in pubspec.yml has happened on the current date.

Commit the changed ChangeLog and pubspec.yml to main.

2. Build the iOS release

Documentation is available at: Distribute your Flutter App with FireBase App Distribution and Build and release an iOS app. Here's a summary:

In XCode, check "General" and "Signing and Capabilities" tabs. You may need to login to Apple to get the Team details and Provisioning details to be specified correctly.

In a terminal, run flutter build ios. Among other things, this tells XCode about the new version number in pubspec.yml.

Back in XCode, invoke Product > Build, then Product > Archive.

If archiving completes successfully, then a dialog box will pop up with "Distribute App". Click it, and specify "Ad hoc" distribution, "Automatic signing", and keep clicking dialogs as they appear until the .ipa file is created.

Upload the new .ipa to Firebase App Distribution by going to the App Distribution tab in the Firebase console and dropping the file into the upload area.

Select the testers to receive the deployment.

Initiate deployment.

3. Build the android release

Documentation is available at Build and release an Android app. Here's a summary:

On my (2021) Mac laptop, I have generated an upload-keystore.jks file with CN=Philip Johnson, OU=geogardenclub, O=geogardenclub, L=Bellingham, ST=WA, C=US.

If no changes are required to build.gradle or AndroidManifest.xml, then it should be possible to build the Android APK file by invoking:

flutter build apk

This should result in the creation of an APK file in build/app/outputs/flutter-apk/app-release.apk.

To upload it, go to App Distribution, select ggc_app (android) in the top of the window, and upload the file.

Select testers and distribute in the standards way.

4. Build the web app

Documentation is available at: Build and release a web app.

There are some one-time configuration steps documented at the above link, but once you've gone through them, you can simply invoke:

firebase deploy

At the end, I received output like this:

Project Console: https://console.firebase.google.com/project/ggc-app-2de7b/overview
Hosting URL: https://ggc-app-2de7b.web.app

Previewing prior to deployment

Note that if you want to preview the webapp prior to deployment, you can install dhttp, then run:

$ flutter build web
$ dhttpd --path build/web/

Open http://localhost:8080 to see the app.

- +

Deployment

For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet.

Firebase App Distribution

For the Beta Release, we are using Firebase App Distribution as the deployment mechanism. This has the following implications:

  1. We deploy to iOS, Android, and the web.
  2. We must obtain the email address for every user who wishes to have GGC on their device.

Documenting deployment versions

We expect to make many deployments during the Beta release period as we fix bugs or implement enhancements. Each new deployment will require a new version number (specified in the pubspec.yml file), and we will document what has changed in each new version via CHANGELOG.md. To manage version numbers and the changelog file, we will use Cider.

We also want to be able to access the ChangeLog inside the deployed app---this is a simple way for users to both know what version of the app they have installed, and what new features or changes they can expect to find in a new version. In order to implement that, there is a script called run_cider.sh that runs the cider command and also copies the resulting CHANGELOG.md file into the assets/changelog directory so that it will be bundled and deployed with the app, and available to users within their Settings screen.

We will adhere to two standards:

  1. For the changelog format, we will adhere to Keep a Changelog.
  2. For the version number format, we will adhere to Semantic Versioning. For the beta release, since there is no public "API", the major version will always be "1". We will increment the minor version when there are new UI or business logic enhancements to the application, and we will increment the patch version in the event of deployments made only to fix bugs. We will not use the pre-release or build suffix notation.

Deployment management

The deployment process is handled by a single developer referred to as the "Deployment Manager" (DM). Initially, Philip will be the DM.

1. Update the ChangeLog

Invoke ./run_cider log added <message> to document new additions (or use changed or fixed) since the last release. Enclose the message in quotes. For example:

./run_cider.sh log added "Terms and Conditions"

Invoke ./run_cider.sh bump minor to increment the version number in pubspec.yml.

Invoke ./run_cider.sh release to update the ChangeLog to indicate that a new release with the current version in pubspec.yml has happened on the current date.

Commit the changed ChangeLog and pubspec.yml to main.

2. Build the iOS release

Documentation is available at: Distribute your Flutter App with FireBase App Distribution and Build and release an iOS app. Here's a summary:

In XCode, check "General" and "Signing and Capabilities" tabs. You may need to login to Apple to get the Team details and Provisioning details to be specified correctly.

In a terminal, run flutter build ios. Among other things, this tells XCode about the new version number in pubspec.yml. (Note that the updated version number might not appear in XCode, this should not be a problem, but double check after the archive is generated.)

Back in XCode, invoke Product > Build, then Product > Archive.

If archiving completes successfully, then a dialog box will pop up with "Distribute App". Click it, and specify "Custom", then "Ad hoc" distribution, then "Automatic signing", and keep clicking dialogs as they appear until the .ipa file is created.

Upload the new .ipa to Firebase App Distribution by going to the App Distribution tab in the Firebase console and dropping the file into the upload area.

Select the testers to receive the deployment.

Initiate deployment.

3. Build the android release

Documentation is available at Build and release an Android app. Here's a summary:

On my (2021) Mac laptop, I have generated an upload-keystore.jks file with CN=Philip Johnson, OU=geogardenclub, O=geogardenclub, L=Bellingham, ST=WA, C=US.

If no changes are required to build.gradle or AndroidManifest.xml, then it should be possible to build the Android APK file by invoking:

flutter build apk

This should result in the creation of an APK file in build/app/outputs/flutter-apk/app-release.apk.

To upload it, go to App Distribution, select ggc_app (android) in the top of the window, and upload the file.

Select testers and distribute in the standards way.

4. Build the web app

Documentation is available at: Build and release a web app.

There are some one-time configuration steps documented at the above link, but once you've gone through them, you can simply invoke:

firebase deploy

At the end, I received output like this:

Project Console: https://console.firebase.google.com/project/ggc-app-2de7b/overview
Hosting URL: https://ggc-app-2de7b.web.app

Previewing prior to deployment

Note that if you want to preview the webapp prior to deployment, you can install dhttp, then run:

$ flutter build web
$ dhttpd --path build/web/

Open http://localhost:8080 to see the app.

+ \ No newline at end of file diff --git a/docs/develop/release-1.0/design-components/badges.html b/docs/develop/release-1.0/design-components/badges.html index eda71919f..413f7306b 100644 --- a/docs/develop/release-1.0/design-components/badges.html +++ b/docs/develop/release-1.0/design-components/badges.html @@ -5,13 +5,13 @@ Badges | Geo Garden Club - +

Badges

Goals

The beta release implements a badge system for gardens and gardeners. This badge system is designed to accomplish the following goals:

  1. Foster user engagement and enjoyment through a game mechanic that publicizes achievements by gardens and gardeners. Gardeners should find it fun to accumulate badges that are associated with their profile and their garden(s).
  2. Foster a community of practice by helping gardeners connect with others with similar interests and/or greater expertise with respect to a specific gardening topic. For example, if a user is interested in vermiculture, the badge system provides a mechanism for them to find other gardeners who already have experience in this area.
  3. Provide a useful, compact representation of garden and gardener characteristics. The app provides "summary" cards for gardens and gardeners. Users should find the presence (and/or absence) of badges helpful in forming a high level understanding of these entities.
  4. Provide a mechanism that identifies ways to improve gardening practices. The badge system makes visible the practices that are important to the GGC mission of food resiliency and sustainable gardening, such as seed saving, composting, and water conservation. This means that a simple heuristic for "getting better at gardening" is to simply "get more badges".
What about Chapters?

The beta release will not implement Chapter-level badges for two reasons:

  1. The primary goals for Chapter-level badges are: (a) encouraging members of a Chapter via "peer pressure" to conform to certain best practices, and (b) making possible Chapter "leaderboards" so that Chapters can assess their capabilities relative to other chapters. Neither of these are important for the beta release where we will focus on a single Chapter with a relatively small number of members.
  2. We will implement garden and gardener-based badge processing within each client, which is simple, scalable, and (hopefully) efficient. Implementing a Chapter-level badge system at scale will require Firebase cloud functions. These functions require specialized knowledge to implement correctly.

Design principles

Types

The beta release will implement two types of badges: garden badges and gardener badges. Garden badges reflect the characteristics of a garden across one or more years. Gardener badges reflect characteristics of a gardener across all of the gardens with which they are associated.

Levels

Each badge can be achieved at three levels of increasing sophistication and/or expertise. Level 1 badges are relatively easy to achieve. Level 2 and Level 3 badges indicate increasing levels of expertise or accomplishment with respect to the badge subject.

Levels will be visually represented by 1-3 stars along the left side of the badge. Here`s an example:

Verification (i.e. badge processing)

Verification of badges can be done in the following ways: "via attestation", "via observation", or "via planting". Depending upon the badge and/or level, one or more of these verification approaches might be required.

"Via attestation" means that the Gardener (owner) has simply attested that they (or their garden) adheres to certain practices. This is implemented as an "Attestation" section in the Garden and Gardener forms. For example, when creating or updating a Garden, the gardener (owner) can simply check a box to attest that the garden is pesticide-free. Gardeners are on the honor system to attest only to practices that they believe to be true.

"Via Observation" requires the Gardener (owner or editor) to post one or more Observations with one or more badge-specific tags in a single Garden.

"Via Planting" requires the Gardener (owner) to have created Planting data in a single Garden that helps to satisfy the criteria for a badge.

Beta release badge processing is client-side only

As the above indicates, for the beta release, badge processing occurs on the client-side, and is triggered by updates to garden, gardener, observation, or planting documents.

The current criteria are designed so that they can be assessed via either WithCoreData or WithGardenData. See the Implementation Notes section associated with each badge for an indication of which "With" widget can be used.

There are many ways we could define the criteria for a badge. The criteria we choose must align with the beta release design constraints. If a criteria turns out to be too expensive to verify via client-side processing, then we should change the criteria, not change the design.

Observation tags

Many badges require the posting of (public) Observations to provide evidence for a specific practice. To implement badge processing, the system needs to be able to identify Observations that are intended to support achievement of a particular badge. This will be done by the user attaching one or more pre-defined tags to an Observation. Some practices (i.e. cover crops) can help the user achieve multiple badges, so the tag labels are not designed to indicate any particular badge.

The system will "take the user's word" for the appropriateness of the tags to the Observation. We hope that the public nature of these observations will prevent users from misusing this process.

Implementation hints

Badge processing has the following general implementation characteristics:

  • Triggered as part of "mutation" of Gardener, Garden, Planting, and Observation entities.
  • During submit() processing, the BadgeProcessor is called with the Garden, Chapter, and User collections, plus the entities about to be mutated. It then calls a function for each Badge, passing it this data. Each badge-specific function has its own function returns the set of BadgeInstances to be created and deleted.

Badge implementation also involves the creation of the Badges page. This page should provide a description of each Badge, the criteria required to obtain each level, and chips to indicate the Gardens (or Gardeners) that currently hold the badge.

Badge implementation will also require updates to the Garden and Gardener entities and mutation processing in order to support the various "attestation" checkboxes. Each attestation can be implemented as an optional boolean field in the Garden or Gardener entity.

Garden badges

Here are proposals for the beta release garden badges.

Pesticide free

General Criteria

No pesticides are used in this garden.

Observation tags

N/A

LevelVerification
1a. The user has attested that the Garden is pesticide free.
b. There is Planting data for this garden for a single calendar year.
2a. The user has attested that the Garden is pesticide free.
b. There is Planting data for this garden for exactly two calendar years.
3a. The user has attested that the Garden is pesticide free.
b. There is Planting data for this garden for three or more calendar years.

Implementation notes

Triggered as part of Garden or Planting mutation.

Requires WithGardenData.

Pollinator Friendly

General Criteria

The garden has pollinator-friendly practices such as: (1) Using a wide variety of plants that bloom from early spring into late fall, (2) Avoiding modern hybrid flowers, especially those with "doubled" flowers, (3) Eliminating pesticides whenever possible, (4) Including larval host plants in your landscape, (5) Creating a damp salt lick for butterflies and bees, (6) Leaving dead trees, or at least an occasional dead limb, in order to provide essential nesting sites for native bees, and (7) Adding to nectar resources by providing a hummingbird feeder.

Observation tags

#DitchChemicals, #Habitat, #Hummingbirds, #LarvalHostPlants, #NativeBees, #NativePlants, #PesticideFree, #SaltLick.

LevelVerification
1a. There are Observations indicating at least three of the practices within a single calendar year.
2a. There are Observations indicating at least three of the practices for two calendar years.
3a. There are Observations indicating at least three of the practices for three or more calendar years.

Implementation notes

Triggered as part of Observation mutation.

Requires WithGardenData.

Sustainable Soil

General Criteria

Garden soil has been improved by using sheet mulch, compost, and/or cover crops.

Observation tags

#Compost, #CoverCrops, #SheetMulch, #Mulch, #CropRotation

LevelVerification
1a. There are Observations indicating at least three of the practices within a single calendar year.
2a. There are Observations indicating at least three of the practices for two calendar years.
3a. There are Observations indicating at least three of the practices for three or more calendar years.

Implementation notes

Triggered as part of Observation mutation.

Requires WithGardenData.

Water Smart

General Criteria

The garden involves water conservation practices, including: (1) collecting and using rainwater; (2) drip irrigation or soaker hoses, or (3) timers to water during cooler parts of day to minimize water use.

Observation tags

#DripIrrigation, #Rainwater, #WaterTimer.

LevelVerification
1a. There are Observations indicating at least one of the practices within a single calendar year.
2a. There are Observations indicating at least one of the practices for two calendar years.
3a. There are Observations indicating at least one of the practices for three or more calendar years.

Implementation notes

Triggered as part of Observation mutation.

Requires WithGardenData.

Gardener badges

Here are proposals for the beta release Gardener badges.

Community Cultivator

General Criteria

The gardener has demonstrated experience with community and/or school gardening.

Observation tags:

N/A

LevelVerification
1a. The gardener is associated with exactly one garden which has the "Community or School Garden" attestation.
2a. The gardener is associated with exactly two gardens that have the "Community or School Garden" attestation.
3a. The gardener is associated with three or more gardens that have the "Community or School Garden" attestation.

Implementation notes

Triggered as part of Garden and Gardener mutation.

Requires WithCoreData.

Compost Champion

General Criteria

The gardener has experience composting in a gardens.

Observation tags

#Compost, #CompostTea, #Hugelkulture, #Vermiculture, #Worms.

LevelVerification
1a. The gardener (owner or editor) has posted Observations indicating at least one of the practices for a single calendar year in a garden.
2a. The gardener (owner or editor) has posted Observations indicating at least one of the practices for two calendar years in a single garden.
3a. The gardener (owner or editor) has posted Observations indicating at least one of the practices for three or more calendar years in a single garden.

Implementation notes

Triggered as part of Observation mutation.

Requires WithGardenData.

Note that the gardener cannot get to levels 2 or 3 by "switching" among different gardens. The postings must be from the same garden. This means WithGardenData is enough to evaluate the criteria.

Also, the gardener must make the Observations themselves. They can't "passively" obtain the badge because someone else in the Garden made Observations with the appropriate tags.

Crop Whisperer

General Criteria

The gardener has demonstrated expertise in growing a specific crop in a single garden.

Multiple Badge Alert!

Unlike other badges, this badge is crop-specific, and so a gardener can earn multiple Crop Whisperer badges ("Bean Whisperer", "Cucumber Whisperer")

Observation tags

N/A

LevelVerification
1a. There are Plantings for exactly three different varieties of the same crop.
b. At least two outcomes were awarded at least three stars in at least one Planting.
2a. There are Plantings for exactly four different varieties of the same crop.
b. At least two outcomes were awarded at least three stars in at least one Planting.
3a. There are Plantings for at least five different varieties of the same crop.
b. At least two outcomes were awarded at least three stars in at least one Planting.

Implementation notes

Triggered as part of Planting mutation.

Requires WithGardenData.

Greenhouse grower

General Criteria

The gardener has experience growing plants successfully in a greenhouse.

Observation tags

N/A.

LevelVerification
1a. There is a single Planting in a single Garden that was started in a greenhouse that survived to harvest and was awarded at least three stars for at least one outcomes.
2a. There are two Plantings in a single Garden that were started in a greenhouse that survived to harvest and were awarded at least three stars for at least one outcomes.
3a. There are three Plantings in a single Garden that were started in a greenhouse that survived to harvest and were awarded at least three stars for at least one outcomes.

Implementation notes

Triggered as part of Planting mutation.

Requires WithGardenData.

Permaculture Pro

General Criteria

The gardener has completed a Permaculture workshop to learn about the philosophy of permaculture and is also associated with garden(s) that have achieved permaculture-related badges

Observation tags

#PesticideFree, #SustainableSoil, #WaterSmart, #PollinatorFriendly

LevelVerification
1a. The gardener (owner or editor) has attested in their profile that they have completed a permaculture workshop.
b. There are Observations indicating at least one of the practices within a single calendar year.
2a. The gardener (owner or editor) has attested in their profile that they have completed a permaculture workshop.
b. There are Observations indicating at least one of the practices for exactly two calendar years.
3a. The gardener (owner or editor) has attested in their profile that they have completed a permaculture workshop.
b. There are Observations indicating at least one of the practices for three or more calendar years.

Implementation notes

Triggered as part of Garden and Observation mutations.

Requires WithGardenData.

Vermiculturalist

General Criteria

The gardener has experience with vermiculture (the controlled growing of worms) and vermicomposting (the use of worms to produce compost).

Observation tags:

#CompostTea, #Vermiculture, #Worms.

LevelVerification
1a. There are Observations indicating at least one of the practices within a single calendar year in a single Garden.
2a. There are Observations indicating at least one of the practices for two calendar years in a single Garden.
3a. There are Observations indicating at least one of the practices for three or more calendar years in a single Garden.

Implementation notes

Triggered as part of Observation mutations.

Requires WithGardenData.

Seed Saver

General Criteria

The gardener has demonstrated experience with seed saving practices, including: (1) Harvesting seeds from plants, (2) Drying seeds, (3) Storing seeds, (4) Germinating seeds, (5) Providing seeds to other members of the community.

Observation tags:

#SeedSaving, #SeedSharing

LevelVerification
1a. There are Observations indicating at least one of the practices within a single calendar year in a single Garden.
2a. There are Observations indicating at least one of the practices for two calendar years in a single Garden.
3a. There are Observations indicating at least one of the practices for three or more calendar years in a single Garden.

Implementation notes

Triggered as part of Observation mutations.

Requires WithGardenData.

Post-Beta badges

Here are some proposals for badges that we could add after the beta release. I have not edited these descriptions to conform to the latest design principles.

Chapter Chair

General Criteria

The gardener is serving as a Chair for the Chapter.

Note that GGC System Admins are responsible to designating which member(s) of a Chapter are the Chair(s). When they do this designation, they set a flag in the member`s profile indicating that they are currently a Chapter Chair and what date they started being Chair.

LevelVerification
1a. The gardener is currently the Chapter Chair, and has served as a Chapter Chair for one or two years.
2a. The gardener is currently the Chapter Chair, and has served as a Chapter Chair for three or four years.
3a. The gardener is currently the Chapter Chair, and has served as a Chapter Chair for five or more years.

Connected Community

General Criteria

The chapter has demonstrated a commitment to building a community of practice.

LevelVerification
1a. At least 100 gardeners in the chapter.
2a. At least 250 gardeners in the chapter.
3a. At least 500 gardeners in the chapter.

Climate Victors

General Criteria

The chapter has demonstrated a commitment to creating Climate Victory Gardens.

LevelVerification
1a. At least 50% of the chapter gardens have achieved the badge.
2a. At least 75% of the chapter gardens have achieved the badge.
3a. At least 90% of the chapter gardens have achieved the badge.

Pesticide Resistors

General Criteria

The chapter has demonstrated a commitment to avoiding the use of pesticides in their gardens.

LevelVerification
1a. At least 50% of the chapter gardens have achieved the badge.
2a. At least 75% of the chapter gardens have achieved the badge.
3a. At least 90% of the chapter gardens have achieved the badge.

Seed Sharers

General Criteria:

The chapter has demonstrated a commitment to seed sharing.

LevelVerification
1a. At least 50% of the chapter gardens have achieved the badge.
2a. At least 75% of the chapter gardens have achieved the badge.
3a. At least 90% of the chapter gardens have achieved the badge.

Climate Victory

General Criteria

A Climate Victory Garden has been added to Green America`s database and the garden implements one or more of the following practices: (1) grow food, (2) cover soils, (3) compost, (4) ditch chemicals, and (5) encourage biodiversity.

Observation tags

#Biodiversity, #Compost, #CoverCrops,#DitchChemicals, #PesticideFree, #PollinatorFriendly, #SheetMulch.

LevelVerification
1a. The user has attested that the Garden is in the Green America database.
b. There are Observations associated with this garden for at least two of the associated tags.
2a. The user has attested that the Garden is in the Green America database.
b. There are Observations associated with this garden for at least five of the associated tags.
3a. The user has attested that the Garden is in the Green America database.
b. There are Observations associated with this garden for at least five of the associated tags in at least two different calendar years.

Master gardener

General Criteria

The gardener has completed a master gardener program.

Shucks

I cannot think of a simple way to award more than one star. Ideas?

Observation tags

N/A

LevelVerification
1a. The gardener attests in their profile to having received a Master Gardener certification.
2(Not yet available)
3(Not yet available)

Bee Buddy

General Criteria

The gardener has experience caring for bees.

Observation tags

#Beekeeping, #Beekeeper

Aquaponics Ace

General Criteria

The gardener has demonstrated experience with aquaponics.

Observation tags

#Aquaponics, #FishAndPlants,

Herbalist Hero

General Criteria

The gardener has grown medicinal herbs and created remedies from them.

Observation tags:

#Herbalist, #HerbalRemedy, #PlantMedicine

Educator Extraordinaire

General Criteria

The gardener has provided educational experiences such as leading workshops, writing articles, or working as a garden educator in schools.

Observation tags:

#InspireAndTeach, #SkillSharing, #CommunityWorkshop

Orchard Orchestrator

General Criteria

The gardener has demonstrated experience with orchard management.

- + \ No newline at end of file diff --git a/docs/develop/release-1.0/design-components/data-model-old.html b/docs/develop/release-1.0/design-components/data-model-old.html index 4c262f51c..0d2deac25 100644 --- a/docs/develop/release-1.0/design-components/data-model-old.html +++ b/docs/develop/release-1.0/design-components/data-model-old.html @@ -5,13 +5,13 @@ Data Model | Geo Garden Club - +

Data Model

This page documents the data model intended to satisfy the beta release requirements.

Entities

In this document, "entity" refers to the fundamental forms of persistent data objects. Each entity is defined as a set of typed fields.

Entities are persisted through a set of Firebase collections. In general, each entity is a document that is stored in a corresponding collection: all of the Chapter entity documents are stored in a Firebase collection called Chapters, all of the Gardener entity documents are stored in a Firebase collection called Gardeners. Unfortunately, the "News" entity documents are stored in a Firebase collection called "Newss" (with two s's at the end) so that the entity and collection name are different.

In the ggc_app application, there are Dart "domain" classes that mirror these Firebase collections, so there is a Dart class called "Chapter", a Dart class called "ChapterCollection", and so forth.

To facilitate the design description, each field of an entity will be documented with one of the following "variants" R, O, or D:

VariantDescription
RRequired: The field value is stored as an explicit value in each document of the entity's collection, and all documents have a value for this field.
OOptional: The field value may or may not exist in a given document associated with the collection.

Finally, the following documentation includes example documents (JSON objects) generated from the DataModelMigrator application.

Chapter

The Chapter entity contains the following fields:

FieldTypeR/ODescription
chapterIDStringRA unique ID with the format chapter-<chapterNum>
nameStringRThe name of the chapter, such as "Whatcom-WA"
zipCodesList<ZipCode>RThe zip codes associated with the chapter, derived from the ChapterZipMap.
profilePictureStringRThe path to a profile picture for this chapter
picturesList<String>OThe paths for additional pictures of this chapter.
lastUpdateDateTimeRA DateTime instance that timestamps the last update.

Here is an example of a Chapter collection document from the migrated data:

  {
"chapterID": "chapter-001",
"name": "Whatcom-WA",
"profilePicture": "/img/chapters/bellingham/bellingham-chapter-map.png",
"pictures": [
"/img/chapters/bellingham/chapter-007.jpg"
],
"zipcodes": [
"98225",
"98226",
"98227",
"98228",
"98229"
],
"lastUpdate": "2023-03-19T12:19:14.164090"
}

User

The User entity represents all of the people who have created an account with the system.

Note that not all Gardeners are users: commercial seed vendors won't generally have an account on the system.

Currently all Users are also Gardeners, though the design does not require this. In future, there may also be Users who are not Gardeners.

Every user is associated with a unique email address, which is their UserID.

For the beta release, the data model does not include information about the subscriptions, payments, credit card, etc associated with a gardener.

Each User entity provides the following information:

FieldTypeR/ODescription
userIDUserIDRA unique ID corresponding to the email address associated with this user.
chapterIDChapterIDRThe chapterID associated with this Gardener.
nameStringRThe users name. The user name is normally not provided in the UI.
usernameStringRThe username is what is normally used to identify the user in the UI.
imagePathStringOA path to the image to be associated with this user.
lastUpdateDateTimeRThe DateTime object indicating the last update.

To illustrate, here is an example document from the Gardener collection:

 {
"userID": "johnson@hawaii.edu",
"chapterID": "chapter-001",
"name": "Philip Johnson",
"username": "@fiveoclockphil",
"imagePath": "",
"lastUpdate": "2023-04-01T00:00:00.000Z"
}

Gardener

The Gardener entity is designed to represent two distinct classes of gardeners in GGC: (1) "normal" home gardeners and (2) commercial seed vendors.

The benefit of having the Gardener entity represent both "normal" gardeners as well as commercial seed vendors is that it results in a uniform mechanism in the app to support "seed providers": any Gardener (which can either be a normal home gardener or a commercial seed vendor) grows a Garden which contains Plantings which (may or may not) produce seeds that are available within the Chapter.

This does create some UI complexity, in that commercial seed vendors are not intended to be "Chapter members" in the normal sense. There is a boolean isVendor field that can be used to maintain two local caches of Gardener collections: one containing all of the "normal" home gardeners, and one containing the "vendor" gardeners.

Each Gardener entity provides the following information:

FieldTypeR/ODescription
gardenerIDGardenerIDRA unique ID corresponding to the email address of this user.
chapterIDChapterIDRThe chapterID associated with this Gardener.
isVendorboolRA flag indicating whether this entity instance represents a vendor (if true) or a home gardener (if false)
vendorNameStringOIf isVendor, then this string is present and specifies the full vendor name.
vendorShortNameStringOIf isVendor, then this string is present and specifies a short vendor name.
vendorURLStringOIf isVendor, then this string is present and specifies a URL to the vendor site.
masterGardenerbooleanOtrue if this gardener is a Master Gardener. (This is an example "badge". There could be many others.)
lastUpdateDateTimeRThe DateTime object indicating the last update.

To illustrate, here is an example document from the Gardener collection:

  {
"gardenerID": "jennacorindeane@gmail.com",
"chapterID": "chapter-001",
"isMasterGardener": true,
"isVendor": false,
"vendorName": "",
"vendorShortName": "",
"vendorURL": "",
"lastUpdate": "2023-03-19T12:19:14.164836"
}

Garden

The Garden entity represents a plot of land (or maybe even just some pots) that can hold Plantings over one or more years.

The Garden entity contains the following fields:

FieldTypeR/O/IDescription
gardenIDGardenIDRA unique ID with the format garden-<chapterNum>-<gardenNum>. Each <gardenNum> is unique within a Chapter and starts at 100.
chapterIDChapterIDRThe ChapterID.
nameStringRThe name of the Chapter. This should normally be unique within a Chapter.
ownerIDGardenerIDRThe single Gardener who "owns" this Garden, which gives them full management rights. This ID corresponds to their email.
profilePictureStringThe path to an image to be used as the profile picture for this garden.
picturesList<String>A list of image paths.
isVendorboolRIf true, then this is a commercial garden, not a home garden.
picturesList<Pictures>O(Public) Pictures of this garden.
climateVictoryGardenbooleanOAn example "badge" associated with this garden.
lastUpdateDateTimeRThe last update timestamp.

Here is an example Garden document:

  {
"gardenID": "garden-001-102",
"chapterID": "chapter-001",
"name": "Kale is for Kids",
"ownerID": "jbeck913360@hotmail.com",
"profilePicture": "/img/gardens/45ght3cf/garden-001.jpg",
"pictures": [
"/img/gardens/45ght3cf/garden-007-birds-eye-view.jpg",
"/img/gardens/45ght3cf/garden-002.jpg"
],
"isVendor": false,
"isClimateVictoryGarden": false,
"lastUpdate": "2023-03-20T15:45:56.856468"
}

Editor

In the beta release, the access control capability enables a Gardener to allow another Chapter member to edit one of their gardens. (There is no implementation of a "viewer", who can see more of someone else's garden than a normal Chapter member.)

This capability is implemented by the Editor entity, which implements a mapping between a Garden and a Gardener:

FieldTypeR/ODescription
editorIDStringRA unique ID with the format editor-<chapterNum>-<editorNum>
chapterIDChapterIDRThe ChapterID.
gardenIDGardenIDRThe garden for which editor access is being granted.
gardenerIDGardenerIDRThe gardener who is obtaining editor access to the above garden.
lastUpdateDateTimeRA DateTime instance that timestamps the last update.

Here is an example Editor document:

{
"editorID": "editor-001-001",
"gardenID": "garden-001-101",
"chapterID": "chapter-001",
"gardenerID": "jbeck913360@hotmail.com",
"lastUpdate": "2023-03-20T15:45:56.856359"
}

Bed

Each Garden consists of a number of Beds.

The Bed entity has the following conceptual structure.

FieldTypeR/ODescription
bedIDBedIDRA unique ID with the format bed-<chapterNum>-<gardenNum>-<bedNum>. BedNums are unique within a Chapter and Garden and start at 200.
chapterIDChapterIDRThe ChapterID.
gardenIDGardenIDRThe garden associated with this Bed.
nameStringRThe name associated with this Bed.
lastUpdateDateTimeRA DateTime instance that timestamps the last update.

Here is an example Bed document:

 {
"bedID": "bed-001-102-215",
"chapterID": "chapter-001",
"gardenID": "garden-001-102",
"name": "15",
"lastUpdate": "2023-03-20T15:45:56.856565"
}

Planting

A Planting is represents a set of plants of the same variety or crop, planted in a single bed, all with the same approximate timings (i.e. planting, transplanting, harvesting, etc.). If the same variety or crop is planted in two different beds, then this must be represented by two Planting instances. (Alternatively, you could define an additional, "virtual" Bed that conceptually represents the contents of two physical beds and put a single Planting in it.)

It is common during the garden planning process to first design the garden at the "crop" level, and then later refine the plan by specifying a specific variety of each crop. To support this incremental design process, the Planting entity only requires a Crop to be specified.

The Planting entity has the following conceptual structure.

FieldTypeR/ODescription
plantingIDPlantingIDRA unique ID with the format planting-<chapterNum>-<gardenNum>-<plantingNum>. PlantingNums are unique within a Chapter and Garden and start at 1000.
chapterIDChapterIDRThe ChapterID.
gardenIDGardenIDRThe garden associated with this Bed.
cropIDCropIDRThe Crop associated with this Planting.
cropNameStringRThe name associated with the above CropID.
yearNumberOThe year associated with a Garden. Not required when this Planting is associated with a vendor Garden.
bedIDBedIDOThe BedID.
varietyIDVarietyIDOThe VarietyID.
varietyNameStringOThe name associated with the above VarietyID.
outcomeIDOutcomeIDOThe outcomes associated with this planting.
seedIDSeedIDOThe seed that was used to create this planting.
startDateDateTimeOWhen the plant was started.
transplantDateDateTimeOWhen the plant was transplanted from greenhouse to bed (if that happened.)
firstHarvestDateDateTimeOWhen the plant first produced food.
endHarvestDateDateTimeOWhen the plant last produced food.
pullDateDateTimeOWhen the plant was pulled from the garden.
usedGreenhousebooleanOIf the planting was started in a greenhouse. Defaults to false.
isVendorbooleanOIf this planting is associated with a commercial seed grower. Defaults to false.
hasSeedsbooleanOIf this planting produced seeds. Defaults to false.
seedsAvailablebooleanOIf this planting produced seeds that the Gardener can provide to others. Defaults to false.
lastUpdateDateTimeRA DateTime instance that timestamps the last update.

Here is an example Planting document:

 {
"plantingID": "planting-001-101-1034",
"chapterID": "chapter-001",
"gardenID": "garden-001-101",
"cropID": "crop-001-544",
"cropName": "Tomatillo",
"year": 2023,
"bedID": "bed-001-101-218",
"varietyID": "variety-001-904",
"varietyName": "De Milpa",
"outcomeID": null,
"startDate": "2023-03-10T00:00:00.000",
"transplantDate": "2023-05-01T00:00:00.000",
"firstHarvestDate": null,
"endHarvestDate": null,
"pullDate": "2023-08-31T00:00:00.000",
"seedID": "seed-001-105-1048-103",
"usedGreenhouse": true,
"isVendor": false,
"hasSeeds": false,
"seedsAvailable": false,
"lastUpdate": "2023-03-20T15:45:56.872599"
}

Variety

Variety is a specific kind of Crop which has seeds. For example, a seed packet such as "Tomato (Sun Gold)" specifies the crop ("Tomato") and the Variety ("Sun Gold").

It is possible for multiple gardeners (either home or commercial) to produce Seeds of the same Variety.

The Variety entity has the following conceptual structure.

FieldTypeR/ODescription
varietyIDVarietyIDRA unique ID with the format variety-chapterNum-varietyNum. VarietyNums are unique within a Chapter and start at 900.
chapterIDChapterIDRThe ChapterID.
cropIDCropIDRThe Crop associated with this Variety.
cropNameStringRThe name associated with the above CropID.
nameStringRThe name associated with this Variety.
lastUpdateDateTimeRA DateTime instance that timestamps the last update.

Here is a sample Variety document:

 {
"varietyID": "variety-001-923",
"chapterID": "chapter-001",
"cropID": "crop-001-534",
"cropName": "Radicchio",
"name": "Pasqualino",
"lastUpdate": "2023-03-20T15:45:56.858247"
}

Crop

Crop specifies a type of plant independent of its Variety. For example, "Tomato" is a Crop.

Each Variety is associated with a single Crop.

The Crop entity has the following conceptual structure.

FieldTypeR/ODescription
cropIDCropIDRA unique ID with the format crop-<chapterNum>-<cropNum>. CropNums are unique within a Chapter and start at 500.
chapterIDChapterIDRThe ChapterID.
familyIDFamilyIDRThe plant Family associated with this Crop.
nameStringRThe name associated with this Crop.
lastUpdateDateTimeRA DateTime instance that timestamps the last update.

Here is an example Crop document:

{
"cropID": "crop-001-503",
"chapterID": "chapter-001",
"familyID": "family-411",
"name": "Asparagus",
"lastUpdate": "2023-03-20T15:45:56.857232"
}

Family

Family specifies the botanical family associated with one or more Crops (and implicitly, Varieties). For example, the "Nightshade" family groups together Tomatoes, Potatoes, and Peppers. Family data is useful during garden planning to facilitate planning issues including crop rotation and companion planting.

The Family entity is one of the few "global" collections in GGC. In other words, it does not include a ChapterID; every Chapter will download this collection in its entirety. (Which is not a hardship, there are only around a dozen Family documents.)

The Family entity has the following conceptual structure.

FieldTypeR/ODescription
familyIDFamilyIDRA unique ID with the format family-<familyNum>. FamilyNums are unique and start at 400.
formalStringRThe formal name associated with this Family.
commonStringRThe common name associated with this Family.
examplesStringRA documentation string providing examples of Crops within this Family.
lastUpdateDateTimeRA DateTime instance that timestamps the last update.

Note that computing the cropIDs or varietyIDs associated with a familyID requires specifying a chapterID.

Here is an example Family document:

{
"familyID": "family-406",
"formal": "Fabaaceae",
"common": "Legume",
"examples": "bean, pea, peanuts",
"lastUpdate": "2023-03-20T15:45:56.856873"
}

Outcome

Outcome data is gardener-supplied information about the result of a single planting. We want to specify results of a planting that is useful and actionable for gardeners, that captures the most important properties of a planting, that is relatively easy to provide, and that is specified in sufficient detail that we can create meaningful aggregations of outcome data for crops and varieties.

To support these requirements, we define five outcome types: germination, yield, flavor, pest and disease resistance, and appearance. Each planting can receive a "grade" for each of these outcome types on a five point scale. The following table presents the definitions for each scale value for each outcome type.

12345
GerminationFailure. No seeds germinated.Poor. Approximately a quarter of the seeds germinated.OK. Approximately half of the seeds germinated.Good. Approximately 3/4 of the seeds germinatedOutstanding. 90% or more of the seeds germinated.
YieldNone. The planting died and/or did not yield any food.Minimal. The planting yielded significantly less food than expected.OK. The planting yielded the expected amount of food.Good. The planting yielded somewhat more food than expected.Outstanding. The planting yielded significantly more food than expected.
FlavorBad. Not worth eating.Bland. Worth eating, but only a little.OK. Expected level of flavor.Good. Better than OK flavor, enjoyable to eat.Outstanding. Can't imagine it tasting better.
Pest and disease resistanceExtremely poor. 90% or more of the plantings have damage.Poor. More than half of the plantings have damage.OK. No more than a quarter of plantings have damage.Good. Only a few plantings have damage.Outstanding. No observable damage.
AppearanceAlmost all ugly. 90% or more of the crop is ugly.Mostly ugly. Over 50% of the crop is ugly.Mostly OK. Over 50% of the crop is OK.Mostly beautiful. Over 50% of the crop is beautiful.Almost all beautiful. 90% or more of the crop is beautiful.

The Family entity has the following conceptual structure.

FieldTypeR/ODescription
outcomeIDOutcomeIDRA unique ID with the format outcome-<chapterNum>-<gardenNum>-<outcomeNum>. OutcomeNums are equal to the PlantingNum of the Planting associated with this Outcome.
chapterIDChapterIDRThe ChapterID.
cropIDCropIDRThe Crop associated with this Outcome.
varietyIDVarietyIDRThe Variety associated with this Outcome.
plantingIDPlantingIDRThe Planting associated with this Outcome.
yearNumberRThe year associated with this Outcome.
appearancenumberOThe appearance outcome value, if available.
flavornumberOThe flavor outcome value, if available.
germinationnumberOThe germination outcome value, if available.
resistancenumberOThe resistance outcome value, if available.
yieldnumberOThe yield outcome value, if available.
lastUpdateDateTimeRA DateTime instance that timestamps the last update.

By design, an outcomeID's numerical suffix will always be the same as its associated plantingID.

Here is an example of an Outcome document:

{
"outcomeID": "outcome-001-101-1039",
"chapterID": "chapter-001",
"gardenID": "garden-001-101",
"cropID": "crop-001-546",
"varietyID": "variety-001-861",
"plantingID": "planting-001-101-1039",
"year": 2022,
"germination": 3,
"yield": 5,
"flavor": 5,
"resistance": 4,
"appearance": 5,
"lastUpdate": "2023-03-20T15:45:56.873320"
}

Seed

The ability to save and share seeds within a Chapter is a significant core value proposition for GGC.

Creating an effective UX for seed saving and sharing means (among other things) that we need to represent seeds explicitly within the data model. By "seed", we don't mean each individual, tiny seed. We mean the set of all seeds harvested from a planting in a garden in a particular season. We won't represent a "count" of the number of seeds available, as that seems too onerous. Instead, we'll just provide a flag (seedsAvailable) associated with a Planting that a Gardener can use to indicate that there exist (some number of) seeds to share.

Our data model enables us to represent both seeds that are locally produced by gardeners as well as seeds that are produced by vendors. One benefit of our design is the ability to represent the "provenance" of a seed. As a simple example:

PlantingOrigin of the seeds for this Planting
Bean (Scarlet Runner), "Alderwood" garden (2023)Bean (Scarlet Runner), "Alderwood" garden (2022)
Bean (Scarlet Runner), "Alderwood" garden (2022)Bean (Scarlet Runner), "Kale is for Kids" garden (2021)
Bean (Scarlet Runner), "Kale is for Kids" garden (2021)Bean (Scarlet Runner), "Johnny's Seeds" garden (vendor)
Bean (Scarlet Runner), "Johnny's Seeds" garden (vendor)unknown

In other words, our data model can represent a "chain" of Plantings, in which one Planting produces Seeds which are used to grow a subsequent Planting. When you add in the ability for a gardener to inspect this chain, and even learn about the history and observations of any of the Plantings in the chain, it becomes apparent that this has the potential to be an interesting resource for seed saving and sharing.

The Seed entity has the following conceptual structure:

FieldTypeR/ODescription
seedIDSeedIDRA unique ID with the format seed-<chapterNum>-<gardenNum>-<plantingNum>-<seedNum>. SeedNums are unique within a Chapter and Garden and start at 000.
chapterIDChapterIDRThe ChapterID.
gardenIDGardenIDRThe GardenID.
plantingIDPlantingIDRThe PlantingID.
cropIDCropIDRThe CropID.
varietyIDVarietyIDRThe VarietyID
gardenNameStringRThe name of the Garden associated with the above GardenID.
cropNameStringRThe name of the Crop associated with the above CropID.
varietyNameStringRThe name of the Variety associated with the above VarietyID.
seedsAvailableboolRThis field is true if this Seed is currently available for sharing.
lastUpdateDateTimeRA DateTime instance that timestamps the last update.

Here is an example Seed document:

{
"seedID": "seed-001-115-1035-104",
"chapterID": "chapter-001",
"gardenID": "garden-001-115",
"plantingID": "planting-001-115-1035",
"cropID": "crop-001-524",
"varietyID": "variety-001-905",
"gardenName": "Unknown vendor",
"cropName": "Lettuce",
"varietyName": "Mix",
"seedsAvailable": true,
"lastUpdate": "2023-03-20T15:45:56.891813"
}

In general, a Garden associated with a vendor will have a single Planting instance for each Variety that they offer. This Planting instance will have a single Seed instance, with seedsAvailable set to true. In reality, a vendor may or may not have seeds in stock for a given Variety at any given time. And, in reality, a vendor will produce their seeds from growing plants each year. But, we will not represent these "realities" about vendor gardens and seeds in our data model, at least for the beta release.

Observation

An observation is a note (and, typically, a picture) taken by a gardener regarding a planting at a specific point in time.

The Observation entity has the following conceptual structure.

FieldTypeR/ODescription
observationIDObservationIDRA unique ID with the format observation-<chapterNum>-<gardenNum>-<observationNum>. ObservationNums are unique within a Chapter and Garden and start at 700.
chapterIDChapterIDRThe ChapterID.
gardenIDGardenIDRThe Garden associated with this Observation.
gardenNameStringRThe Garden name associated with the above GardenID.
plantingIDPlantingIDRThe Planting associated with this Observation.
cropIDCropIDRThe Crop associated with this Observation.
cropNameStringRThe name associated with the above CropID.
varietyIDVarietyIDRThe VarietyID.
varietyNameStringRThe name associated with the above VarietyID.
observationDateDateTimeRThe time and date associated with this Observation.
tagsList<String>RA list of strings that tag this Observation.
descriptionStringRA textual description of this Observation.
pictureStringOA string that can be used to retrieve the picture associated with this Observation. Or the empty string.
lastUpdateDateTimeRA DateTime instance that timestamps the last update.

Here is an example Observation document:

  {
"observationID": "observation-001-101-707",
"chapterID": "chapter-001",
"gardenID": "garden-001-101",
"plantingID": "planting-001-101-1044",
"cropID": "crop-001-529",
"varietyID": "variety-001-812",
"gardenName": "Alderwood",
"cropName": "Pea",
"varietyName": "Sugar Snap",
"observationDate": "2022-05-20T00:00:00.000",
"tags": [
"phenology",
"first flower"
],
"description": "First pea flower! Peas looking very happy.",
"picture": "observation-004.jpg",
"lastUpdate": "2023-04-01T00:00:00.000Z"
}

If we provide a specific set of tags, rather than allow a gardener to enter free text, then the tag system will be much more useful. Here is a proposal for an initial set of tags:

TagDescription
PestThis observation is useful because: (a) the gardener might choose to rotate beds for this Planting in future seasons, and other gardeners will find it useful to know in real time that the pest is present in their community.
First Harvest, Last HarvestThese observations are useful to the gardener because it provides more detailed guidance on how long a particular PlantID needs to actually be in a bed. Community members also will find this of use in their own garden planning.
First Frost, Last FrostThese seem like they could be useful for planning purposes in future years to decide when it's safe to have certain plants in the garden They could also be used to validate weather station data against the actual climate situation in the garden. Interestingly, if we want to get chapter-wide info on frost dates, we might have to communicate that one gardener observed a frost event to all the other gardeners in the chapter and ask them to confirm/deny frost in their garden (this disambiguates the "no frost" from "no data" situation.)
DiseaseDifferent than pest, which is animal specific. Disease might be leaf curl, wilts etc.
CompanionI think it would be interesting to see examples of plants benefitting from each other. For example, plants vining on each other, providing shade, protecting from pests.
TechniqueJessie once brought up that she would love to see examples of how other gardeners trellis/support plants and I think there could be good learning from sharing of these systems. That makes me think about planting strategies in general. I just planted potatoes for the first time and went online to see how best to do that. There are many ways to plant potatoes. Unknown if there are ways more suited for my climate or the type of potato I planted. I could imagine using the app to peruse pictures of potato planting strategies, perhaps even filtering by variety, to get support for this process.
First leaf, First bud, First Flower, First seedThese phenology tags make patterns in climate change obvious as well as provide insight into how two garden's climates might differ. For example, Jessie and I both have the same kind of raspberry. Knowing if her raspberry flowers/fruits/leafs out etc earlier or later than mine can explain to me other differences in our garden's performance.
AestheticIn addition to the above "useful" tags, we also think it's important to have a final category of tag that indicates an observation that the gardener thinks is interesting even if it's not particularly actionable. (These observations can be filtered out by other gardeners if they don't want to see them.)

In addition, we might want to have tags that provide "meta" information:

TagDescription
PublicIndicate if an observation can appear on the public page.
Help wantedIndicates if the Observation describes an issue or problem for which the gardener needs help. For example, "What is this pest?"

Task

A task is a todo/reminder for the gardener. There are two types of tasks:

  1. A task generated from a planting, such as transplant or first harvest. Eventually, GeoGardenClub will generate these tasks automatically when a new planting is created.
  2. A task created by the gardener, such as Weed bed 1 or Water bed 2.

The Task entity has the following conceptual structure.

FieldTypeR/ODescription
taskIDTaskIDRA unique identifier for this Task with the format task-<chapterNum>-<gardenNum>-<plantingNum>-taskNum. If the task is created by the gardener the <plantingNum> is 0000.
chapterIDChapterIDRThe ChapterID.
gardenIDGardenIDRThe Garden associated with this Task.
taskTypeTaskTypeRThe type of task. There are 6 task types: sow, transplant, firstHarvest, endHarvest, pull, and other.
descriptionStringOA description of the task. This is used for other tasks.
dueDateDateTimeRThe date the task is due.
cropIDCropIDOThe CropID associated with this Task. This is used for planting generated tasks.
cropNameStringOThe name of the crop associated with this Task. This is used for planting generated tasks.
bedIDBedIDOThe BedID associated with this Task. This is used for planting generated tasks.
varietyIDVarietyIDOThe VarietyID associated with this Task. This is used for planting generated tasks.
varietyNameStringOThe name of the variety associated with this Task. This is used for planting generated tasks.
lastUpdateDateTimeRThe date the task was last updated.

Here's an example planting generated task.

{
"taskID": "task-001-101-1068-006",
"chapterID": "chapter-001",
"gardenID": "garden-001-101",
"taskType": "transplant",
"description": "",
"dueDate": "2023-07-25T00:00:00.000",
"cropID": "crop-001-516",
"cropName": "Dill",
"bedID": "bed-001-101-208",
"varietyID": "variety-001-848",
"varietyName": "Goldkrone",
"lastUpdate": "2023-04-01T00:00:00.000Z"
}

and a gardener generated task.

{
"taskID": "task-001-101-0000-008",
"chapterID": "chapter-001",
"gardenID": "garden-001-101",
"taskType": "other",
"description": "Weed bed 1",
"dueDate": "2023-07-25T00:00:00.000",
"cropID": "",
"cropName": "",
"bedID": "",
"varietyID": "",
"varietyName": "",
"lastUpdate": "2023-04-01T00:00:00.000Z"
}

Collections and business logic

As noted above, each entity is represented in the ggc_app as a Dart class, and made persistent as a document in Firebase.

Groups of entity instances of the same type are also represented in the ggc_app as a Dart class, and made persistent as a collection in Firebase. So, for example, in ggc_app, there is a Dart class called "Chapter" (to represent individual instances of that entity) and a Dart class called "ChapterCollection" (to manage a set of Chapter instances). On the Firebase side, there is a collection called Chapters, and each document in that collection has the same structure as the corresponding Dart class. We use freezed to support the translation between the Dart class instance for an entity and its persistent representation as a Firebase document in JSON format.

That said, not all Collections in the ggc_app are created equally! Consider the following typical query:

"What are the names of the crops that have been planted by johnson@hawaii.edu?"

The answer to this query involves finding all the Gardens owned by johnson@hawaii.edu, then retrieving all of the Plantings associated with those Gardens, then building a set of Crop entities from those Plantings, then mapping over that set of Crop entities to build a list of crop names, then sorting that list of names into betabetical order, and finally returning that list.

In this case, three different collections (Gardens, Plantings, and Crops) must be manipulated to satisfy the query. Other queries could require the manipulation of even more collections.

These kinds of queries represent the "business logic" of the application. In ggc_app, we want to follow the software engineering best practice of "separating business logic from user interface logic". To do that, ggc_app defines three "top-level" collections: UserCollection, GardenCollection, and ChapterCollection. Whenever possible, the UI can simply call a method on one of those top-level collections to obtain the data to present in the UI. So, if a UI component needs to present a list of crop names planted by a user, it can simply call users.getCrops(userID). The getCrops() method takes care of accessing all of the additional collections to obtain the desired data.

To make this more concrete, here are a sampling of the methods associated with the ggc_app "top-level" collections.

ChapterCollection

Method signatureReturn value
List<String> getChapterIDs()All chapter IDs
List<String> getAssociatedUserIDs(String chapterID)All users in this chapter
List<String> getChapterNames()Chapter names
String getChapterIDFromName(String name)(Since chapter names are unique.)

UserCollection

Method signatureReturn value
User getUser(String userID)Return the User entity
bool areUserNames(List<String> userNames)Verify the list of usernames
int getNumNews(userID)Number of news items for this user.
List<String> getAssociatedGardenNames(userID)Gardens associated with this user.

GardenCollection

Method signatureReturn value
List<Garden> getGardens({String? userID, String? chapterID})The gardens associated with either the user or the chapter.
String getOwnerUserID(String gardenID)The garden owner.
List<String> getEditorUserIDs(String gardenID)The garden editors.
bool _userIsAssociated(String gardenID, String userID)Is this user an owner or editor of this garden
void setGarden(Garden garden)Update the Firebase document associated with this garden.

When doing UI design, if you find yourself writing more than a couple of lines of code to produce the data for display, you should consider whether this code should be made "business logic" and provided as a method in either the ChapterCollection, GardenCollection, or UserCollection.

Other Data Model issues

Privacy

Our goals for GGC create a very particular and constrained approach to "privacy".

On the one hand, we want to preserve certain types of privacy:

  • Users can pick a "username" which is used in postings so that they do not have to reveal their real name.
  • The system does not reveal the precise location of gardens.
  • Users can tag an observation and/or photo as "private", and in that case it will not be visible to others.

On the other hand, we want to facilitate the creation of a community of practice, which is accomplished by making many aspects of garden planning and management public to all members of a chapter.

A significant goal for the beta release is to test the hypothesis that it is not problematic for users to share this kind of information with others.

A common approach to privacy is to make sharing "opt-in". In other words, your data is private unless you explicitly agree to share it. One concern with this approach is that if we allow some users to make their garden info private, it creates an "information asymmetry", where some users get to exploit the experiences of others while not offering up their experiences in return. That seems corrosive to the morale of the chapter and impedes the creation of a community of practice. It seems better to test the hypothesis that there is enough value from sharing to make it mandatory (outside the "privacy" mechanisms listed above.)

IDs

In NoSQL databases, each document is automatically provided upon creation with a unique string called a "docID" which looks something like this: tghHU4CVf.

In the GGC data model, there is a docID field, but it is ignored by the application. Instead, the application relies on the fact that each document has a ID field whose name and values are based on the associated collection. So, the Gardener collection has a field named "gardenerID" and the value of that field will be a string with the prefix gardener- and a suffix that consists of two numbers: the chapterID associated with the gardener and a number that uniquely identifies the gardener within the chapter. For example, gardener-001-301.

In my prior development experience, having a "user friendly" ID field for NoSQL documents improves the developer experience in two ways:

  • It is a little easier to remember that garden-001-101 is Jenna's garden than tghHU4CVf is Jenna's garden.
  • Certain bugs are easier to identity. For example, if a field named "gardenerIDs" contains the value "crop-001-503".

One design problem with explicitly creating and managing ID fields in this way is ensuring that they are unique. While the database itself can trivially ensure that it always provides a unique randomly generated docID for each document, our design requires clients to create the gardenID, plantingID, etc. locally and hope that there is not already a document with this ID in the database.

With FireBase, it is possible to create a security rule to prevent documents with duplicate field values. Thus, we can have clients create IDs, and in the event that there is a collision (i.e. a document with the same ID for that field already exists), then the client request will fail with an error.

We believe this "error due to pre-existing ID" situation to be a very unlikely scenario, because the GGC unique IDs are crafted to be as "local as possible". For example, when creating a Planting, the unique ID includes the chapter and garden IDs. This means that a collision is only possible if two gardeners try to create a Planting for the same Garden in the same Chapter at "almost" the exact same time. As a result of this design of GGC IDs, we expect collisions to rarely, if ever, occur in practice. If they do, then the above Firebase security rule will prevent the document creation request from succeeding. In the UI, we will catch this failure and ask the user to retry.

In addition, this Beta release data model implements a simple numbering convention to further improve the human readability of the unique IDs. The idea is to begin numbering entity documents of a given type at a different number. Here is the numbering system we are using:

Starting NumberEntities
000Chapter, Editor, Seed
100Garden
200Bed
400Family
500Crop
900Variety
1000Planting, Outcome

So, for example, a plantingID looks like this: planting-001-101-1034, and (if you recall the numbering convention), you can decode the ID as Chapter (0xx) followed by Garden (1xx) followed by the Planting (1xxx). Similarly, a bedID looks like bed-001-102-215 which is a Chapter (0xx) followed by a Garden (1xx) followed by a Bed (2xx).

Note that there is no implementation problem with an Entity having so many documents that the IDs eventually cross over into the next category. For example, there is definitely the possibility of more than 100 Chapters, at which point there could be a chapterNum of 101, which would be the same as a gardenNum. That doesn't create any conflicts or problems internally in the system: unique IDs do not depend upon entities "staying in" their starting range.

The goal of this numbering convention is simply to make the beta release database documents slightly easier to understand while the relative numbers of Chapters, Gardens, Crops, etc are low. Once we have hundreds of Chapters and tens of thousands of Gardeners, we will have outgrown the use of this simple partitioning to understand the data.

Note that Firebase recommends against creating documentIDs with lexicographically close ranges. However, this recommendation applies only to situations with high levels of reads or writes. Even at scale, GGC will not be experience "high" levels of reads or writes (from a database point of view), so I am hopeful we can implement this numbering scheme, at least for the Beta Release. (If necessary, we could easily migrate to a randomized string for IDs in future if this actually becomes an database bottleneck.)

Normalization and caching

A best practice for relational database design is "normalization", which means that a value should only occur in one place at a time. Normalization has a number of virtues, such as making updates and deletions more efficient and less error prone. But normalization has a substantial cost: queries can become very complicated, involving complex "joins" of data from a variety of tables.

The GGC app has the following design considerations that impact on the issue of normalization:

  • Updates and deletions are rare. GGC is mostly an "additive" database. While deletions and updates can occur, they are relatively rare and it's OK if they are "expensive" in time.
  • Reads are common, and we need local caches for certain kinds of Chapter data, and all of the user's Garden-related data.
  • In general, Gardeners do not access data outside their Chapter.

As a result of these design considerations, GGC collections are designed to facilitate caching by including chapterID and gardenID fields whenever relevant.

We also "denormalize" by occasionally providing "redundant" fields in a collection's documents. For example, in some cases a document will include a cropName field even though it already has a cropID field. We do this to simplify the developer experience: it simplifies construction of the UI by reducing the number of collection lookups, and it makes the contents of the database easier to understand and debug.

Local-first, caching, and disconnected operation

We intend to have a "local-first" approach to data. In other words, there will be a local cache of relevant collections on each user's device, so that Firebase queries will be minimized. I am not sure yet whether the local cache will be automatically synced in the background with the global store, or whether we will need to implement a "pull down to refresh" mode. I think either approach would be OK for the purposes of the beta release.

On the other hand, our approach to IDs makes completely disconnected operation problematic. Actually, it goes beyond that: imagine two gardeners working in the same garden in disconnected mode. The opportunities for problematic data entry are present even if we moved to more traditional forms of document IDs.

For that reason, at least for the beta release, we will require an internet connection in order to make the app fully functional. It should be straightforward to allow the app to work based on the locally cached data when disconnected, but when that occurs, provide some indication that certain operations will not be available until an internet connection is re-established.

- + \ No newline at end of file diff --git a/docs/develop/release-1.0/design-components/data-model.html b/docs/develop/release-1.0/design-components/data-model.html index 58d6bf4c9..fa444ea84 100644 --- a/docs/develop/release-1.0/design-components/data-model.html +++ b/docs/develop/release-1.0/design-components/data-model.html @@ -5,7 +5,7 @@ Data Model | Geo Garden Club - + @@ -14,7 +14,7 @@ This design does have a potential problem: what if a Chapter becomes wildly popular and grows to many hundreds of members? It is possible that the performance of the client application can degrade if the number of members (and thus gardens) in a single Chapter becomes too large.

To address this potential problem, the data model is designed to facilitate partitioning of large Chapters into multiple smaller Chapters in the event that the number of members becomes too large. For example, the initial definition of a Chapter may comprise 8 postal (zip) codes, corresponding to all the postal codes in that country. But if that Chapter becomes too large, we could split it into two Chapters, each defined with 4 postal codes (or one with 3 postal codes and one with 5 postal codes, depending upon the concentration of members in each postal code). Our data model does not currently allow Chapter definition "below" the level of a postal code, so the smallest possible Chapter in GeoGardenClub would be one defined by a single postal code.

We foresee an annual end-of-year review, where we see if any Chapters are reaching a size where it would be appropriate to split them up into smaller Chapters. By doing it in Winter (at least for the Northern Hemisphere), such Chapter reorganization should have less impact on the Gardeners.

To facilitate Chapter splitting, the IDs associated with Garden-level entities do not encode the chapterID, but instead the two character (alpha2) country code and the postal code. This allows Garden-level data to more easily migrate to new Chapters without needing to change their entity IDs.

Entity dependencies

The following diagram presents an alternative perspective on the entities. In this case, there is a line between two entities when there is a relationship between them; in other words, one of the entities refers to the other with a foreign key (i.e. ID) field.

The primary goal of this diagram is to make it clear that there is a fairly rich set of dependencies among the entities in this data model.

This is a positive thing, because it means that there are many different and interesting ways to "slice and dice" the data.

It also illustrates why we have chosen to implement the data model as a set of top-level collections. The many different relationships argue against the use of subcollections.

Let's now turn to a more detailed description of the entities in the data model.

Chapter

The Chapter entity defines a geographic region based on a country (represented as a two character (alpha-2) country code), and a set of one or more postal (zip) codes. GGC ensures that Chapter instances partition the world: every pair of (country code, postal code) is mapped to exactly one Chapter.

ChapterID management

A Firebase collection called ChapterZipMap will provide a default mapping of US postal (i.e. zip) codes to chapterIDs. This mapping initially defines a one-to-one correspondence between US counties and GGC Chapters.

Outside of the US, each (country code, postal code) pair will be its own Chapter. This is not optimal but it provides a way to make GGC immediately available to users outside the US without constructing a world-wide ChapterPostalCodeMap. We can add this later without any change to the data model.

Unlike most other entity IDs, the complete set of chapterIDs is defined in advance in GGC. In other words, we can compute all of the chapterIDs on earth, and they do not depend upon the number of users or their behavior. In contrast, there is no a priori limit to the number of (say) Planting IDs.

While chapterIDs are finite, they are not necessarily fixed in terms of their numbers and the geographic regions that they encompass. For US Chapters, we can change the set of chapters by changing the entries in the ChapterZipMap. For example, while our initial approach is to implement a one-to-one correspondence between US chapters and US counties, we could in future change the ChapterZipMap so that a single US county could have multiple Chapters, or multiple counties could be combined into a single Chapter, or some other approach. (Changing chapter geographic boundaries requires more than just changing the ChapterZipMap; the point here is that our representation does not lock us in to our initial definition for Chapters.) The only hard constraint is that each postal code is assigned to one and only one Chapter.

ChapterIDs have the format chapter-<country>-<chapterCode>. In the case of a US Chapter, an example Chapter ID is: "chapter-US-001". In the case of a non-US Chapter, an example Chapter ID is "chapter-CA-V6K1G8".

To support readability in this document, we will use US chapters and the chapterCodes will be numeric.

User registration and chapter assignment

New user registration works as follows. If they supply "US" as their country code, then the system will query the ChapterZipMap collection to determine their chapterID based on the postal (zip) code that they also supply. If no Chapter entity exists yet with that chapterID, it will be created with the chapterID provided by the ChapterZipMap collection.

If the new user supplies a non-US country code, then the ChapterZipMap is not consulted. Instead, the chapterID is defined as chapter-<country code>-<postal code>. If no Chapter entity exists yet corresponding to that ChapterID, then it will be created.

Note that some countries do not have a postal code. In this case, we will create a default postal code (i.e. "000") for those countries and not request it from the user if they select one of those countries. This implies that for those countries, there will be only one chapter for the entire country. Since most of those countries are pretty small, that seems like a reasonable design decision.

The beta release works differently

The Beta release will only be distributed to users in Whatcom Country, WA, and so the registration mechanism will be simplified. See below for details.

ChapterID as Firebase index

As will be seen, many entities contain a chapterID field. When a client retrieves data from Firebase, it will normally request all of the documents where the chapterID field is the one associated with their chapter. This is the primary way in which GGC can scale. For this to work effectively, we must define an index on the chapterID field for all collections in which the entities have that field.

Chapter entity representation

const factory Chapter(
{required String chapterID, // 'chapter-US-001', or 'chapter-CA-V6K1G8'
required String name, // 'Whatcom-WA', or 'CA-V6K1G8'
required String countryCode, // 'US', 'CA'
required List<String> postalCodes} // ['98225', '98226'], or ['V6K1GB']
)

Projected Release 2.0 changes

In Release 2.0, users will be able to see information about Chapters other than their own. To implement this, we will expand the representation of the Chapter entity with "cached" information, perhaps the number of gardeners, the Chapter badges awarded to that chapter, and so forth. Release 2.0 might also include climate-related features, which might result in associating a list of hardiness zones with each Chapter entity.

User

A User entity is created for all of the people who have created an account with the system.

Users vs Gardeners

Note that all User entities will also have a Gardener entity, but not vice-versa: not all Gardener entities have a corresponding User entity. This is because commercial seed vendors won't generally have an account on the system, but they are represented within the system as Gardener entities.

Every User is associated with a unique email address, which is their UserID. (Their email is also used for their gardenerID.)

UserID management

UserIDs are the email addresses of the user. We obtain the email as part of registration.

User onboarding

After a user successfully registers with the system using the Firebase authentication procedures, they are logged in. Whenever a user logs in, the system checks to see if there is a User document associated with the email address of the currently logged in user. If there is no User document for that email, then the system displays an Onboarding screen.

The onboarding screen is essentially a form that must be successfully filled out in order for the logged in user to proceed to their home page (as well as to any other areas of the application).

The form provides fields for the user's:

  • Name
  • Username
  • Country
  • Postal (Zip) code

In addition, the user can provide a picture at this time if they want.

Beta Release modifications

For the initial beta release:

  • The country field will be a read-only drop-down and "United States" will be selected. It returns the alpha2 code for the United States (i.e. "US")
  • The Postal (Zip) Code input field will be a pull-down list of postal codes associated with Whatcom, Washington.

These modifications to the Onboarding screen guarantee that beta test users will be associated with the Whatcom-WA Chapter, and allow us to avoid the need to design and implement the ChapterZipMap and associated processing.

Once the form is successfully filled out, a User and Gardener document is created for that email address. If those documents are created successfully, then the application displays the Home screen for that User.

User entity representation

const factory User(
{required String userID, // 'johnson@hawaii.edu'
required String chapterID, // 'chapter-US-001'
required String name, // 'Philip Johnson'
required String username, // '@fiveoclockphil'
required String country, // 'US'
required String postalCode, // '98225'
required String uid, // '22e9fe1b-445c-4523-89c2-4450244f1959'
String? pictureURL} // null, or 'https://firebasestorage.googleapis.com/v0/...'
)

Gardener

There is one Gardener entity for each Chapter member and vendor in GGC. This entity is designed to represent two distinct classes of gardeners: (1) "normal" home gardeners (who are Chapter members) and (2) commercial seed vendors (who are not (normally) Chapter members).

Chapter members vs Vendors

The benefit of having the Gardener entity represent both Chapter members as well as commercial seed vendors is that it results in a uniform mechanism in the app to support "seed providers". Any Gardener (which can either be a normal home gardener or a commercial seed vendor) owns a Garden which contains Plantings which (may or may not) produce seeds that are available within the Chapter.

This does create some UI complexity, in that commercial seed vendors do not appear in the list of "Gardeners" and instead appear in the UI as "Vendors". Underneath, however, commercial seed vendors will (like Chapter members) have a Gardener entity, a Garden entity, and for each seed that someone in the Chapter uses, there will be a Seed entity and a Planting entity. (To as great an extent as possible, all of this Vendor entity management is managed internally and hidden from the UI.)

The Gardener entity indicates that it is representing a Vendor by setting the isVendor flag to true. If that flag is true, then the vendorName, vendorShortName, and vendorUrl fields must be non-null.

The Vendors in a Chapter are crowd-sourced, which means any Chapter member can create a new Vendor. When a Vendor is created, they are given the country and postal code of the member who defined them. This is necessary so that their implicitly defined Garden and Plantings can have Chapter-appropriate ID strings.

Cached values

We want to provide information about Gardeners such as the crops and varieties that they are growing in the Index screens, and for performance reasons, we want to provide this information without having to retrieve all of the Planting instances associated with their gardens. To do this, we "cache" the cropIDs and varietyIDs associated with this gardener in this entity.

By "associated", we mean the crops and varieties in the garden(s) for which this gardener is an owner.

Badge attestations

Certain badges require Gardeners to "attest" to having performed activities. The Gardener entity contains an attestations field that holds strings indicating what has been attested to.

GardenerID management

GardenerIDs are the email addresses of the gardener. In the case of registered users, the UserID is the same as the GardenerID. In the case of Vendors, the GardenerID is the contact email for the vendor company (for example, info@johnnyseeds.com).

Gardener entity representation

const factory Gardener(
{required String gardenerID, // 'johnson@hawaii.edu'
required String chapterID, // 'chapter-US-001'
required List<String> cachedCropIDs, // ['crop-US-001-203-9987']
required List<String> cachedVarietyIDs, // ['variety-US-001-305-8765']
required String country, // 'US'
required String postalCode, // '98225'
required List<String> attestations, // ['PermacultureWorkshop']
(false) bool isVendor, // true, or false
String? vendorName, // null, or 'Johnnys Seeds and Supplies'
String? vendorShortName, // null, or 'Johnnys'
String? vendorURL} // true, or false
)

Garden

The Garden entity represents a plot of land (or maybe even just some pots) that can hold Plantings over one or more years.

GardenID management

GardenIDs are generated dynamically when a Chapter member defines a new Garden or when a Chapter member defines a new Vendor (which implicitly results in the creation of a new Garden).

GardenIDs have the format garden-<country>-<postalCode>-<gardenNum>-<millis>. Please see the ID Section for details regarding our approach to ID management.

The GardenID embeds the country code and postal code associated with the ownerID. Note that this might not be the same postal code as the one associated with the physical location of the garden! We do this in order to ensure that if a Chapter's set of postal codes is reorganized, then the Gardens owned by a Gardener will always end up in the same Chapter as their owner.

To support readability in this document and initial development, the gardenNum starts at "101" for each chapter.

Field Notes

The form field for vendor name entry imposes validation criteria. See validators.dart for details.

The Garden name must be unique within a Chapter.

The cachedYears value is based on the StartDate for the Plantings associated with the Garden.

Cached values

Each Garden entity caches the CropIDs, VarietyIDs, years, and the number of Plantings. This allows the Index screens to show this information about Gardens without needing to retrieve and process Plantings.

In addition, whenever there is a change to the Plantings associated with this Garden, the lastUpdated field is set to the current time. This allows the community to see which Gardens in their Chapter are active.

Badge attestations

Certain badges require Gardeners to "attest" to their Garden having certain properties. The Garden entity contains an attestations field with strings indicating the properties that they have attested to.

Garden entity representation

const factory Garden(
{required String gardenID, // 'garden-US-98225-101-4567'
required String chapterID, // 'chapter-US-001'
required String name, // 'Kale is for Kids'
required String ownerID, // 'jessie@gmail.com'
required List<String> cachedCropIDs, // ['crop-US-001-201-9876']
required List<String> cachedVarietyIDs, // ['variety-US-001-302-7865']
required List<int> cachedYears, // [2023, 2022]
required int cachedNumPlantings, // 231
required List<String> attestations, // ['ClimateVictory', 'PesticideFree', 'CommunityOrSchool']
String? pictureURL, // null, 'https://firebasestorage.googleapis.com/v0/...'
String? plotPlanURL, // null, 'https://firebasestorage.googleapis.com/v0/...'
DateTime? lastUpdate, // null (for vendors), '2023-03-19T12:19:14.164090'
(false) bool isVendor} // true, false
)

Editor

The owner of a Garden can add other Chapter members as "editors", which enables those users to edit the Plantings and other information associated with a Garden.

There are some things Editors cannot do. For example, they cannot delete the garden. Only the owner can do that.

To earn a Gardener Badge, only the data associated with Gardens that you own is used. Being an Editor on a Garden does not support Badge processing.

In addition, when displaying the Crops and Varieties associated with a Gardener, only those Crops and Varieties for the Gardens that you own are displayed. The Crops and Varieties for Gardens for which you are an Editor are not included.

EditorID management

Editor entities are created or deleted when the owner of a Garden edits the Editor field of the Garden Details form.

EditorIDs have the format editor-<country>-<postalCode>-<gardenNum>-<editorNum>-<millis>. Please see the ID Section for details regarding our approach to ID management.

EditorNums start at 001 for each garden.

Editor entity representation

const factory Editor(
{required String editorID, // 'editor-US-98225-102-001-5231'
required String gardenID, // 'garden-US-98225-102-6789'
required String chapterID, // 'chapter-US-001'
required String gardenerID} // 'johnson@hawaii.edu'
)

Bed

Each Garden consists of a number of Beds. An owner can edit the name of an existing Bed, and can add a new Bed to a Garden, but cannot delete a Bed if there are any Plantings associated with it.

BedID management

BedIDs have the format bed-<country>-<postalCode>-<gardenNum>-<bedNum>-<millis>. Please see the ID Section for details regarding our approach to ID management.

BedNums start at 001 for each garden.

Bed entity representation

 const factory Bed(
{required String bedID, // 'bed-US-98225-101-001-5634'
required String chapterID, // 'chapter-US-001'
required String gardenID, // 'garden-US-98225-101-6789'
required String name, // '02'
String? gardenerID, // The owner of the garden, i.e. 'johnson@hawaii.edu'.
}
)

Family

The Family entity specifies the botanical family associated with one or more Crops (and implicitly, Varieties). For example, the "Nightshade" family groups together Tomatoes, Potatoes, and Peppers. Each Crop is associated with exactly one Family.

Family data is useful to facilitate planning issues including crop rotation and companion planting. However, in Release 1.0, we do not provide any explicit support for rotation or companion planning.

The Family entity is a "global" collection in GGC. In other words, it does not include a ChapterID; every Chapter will download this collection, and it cannot be edited except by developers.

FamilyID management

FamilyIDs have the format family-<familyNum>. The set of Family entity documents is defined in advance by GGC developers, and editing this collection requires direct interaction with the database.

FamilyNums start at 001.

Family entity representation

const factory Family(
{required String familyID, // 'family-001'
required String formal, // 'Amryllidaceae'
required String common, // 'Allium'
required String examples} // 'onion, leek, garlic, shallot'
)

Crop

The Crop entity specifies a type of plant independent of its Variety. For example, "Tomato" is a Crop, while "Big Boy Tomato" is a specific Variety of Tomato.

Each Crop is associated with exactly one Family entity. A Crop can be associated with many Varieties.

Each Chapter is responsible for "crowd-sourcing" the set of Crop entities. This puts on burden on early Chapter members to define Crops. We estimate that most chapters will need to define between 50 and 100 Crop entities.

The reason we do not provide a global collection of Crops is because a single collection containing all the crops grown world-wide would have several hundred entities, many of which would not be relevant to the Chapter. We want each Chapter's UI to show only the Crops (and Varieties, and Seeds) that are actually being grown in that Chapter. We hypothesize that the benefits of focusing on what is actually being grown outweigh the cost of crowd-sourced management.

CropID management

CropIDs have the format crop-<country>-<chapterCode>-<cropNum>-<millis>. Please see the ID Section for details regarding our approach to ID management.

CropIDs embed the chapter's country code and chapterCode. (ChapterCodes could be a number like '001' in the case of a US Chapter, or a postal code like 'VNZ76T' in the case of a non-US chapter.)

In the event that a Chapter is divided into two or more smaller chapters, each of the new Chapters needs a copy of the Crop collection where the IDs have been changed to embed the new chapterCode. This will require a pass through all of the Garden-level entities to update the value of their cropID fields to the new string value.

CropNums start at 201 for each chapter.

Crop entity representation

const factory Crop(
{required String cropID, // 'crop-US-001-201-3452'
required String chapterID, // 'chapter-US-001'
required String familyID, // 'family-001'
required String name} // 'Tomato'
)

Variety

Variety is a specific kind of Crop which can actually be grown, i.e. it has seeds. For example, a seed packet such as "Tomato (Sun Gold)" specifies the crop ("Tomato") and the Variety ("Sun Gold").

In some cases, the Variety associated with a given seed might not be known. In those cases, by convention, the Variety name can be specified as "Unknown". (It is not, however, appropriate to create a Crop called "Unknown". If you plant some seeds that you know absolutely nothing about, you should wait until they germinate and you can identify their Crop before you can enter data about it into GGC!)

Note that it is possible (and common) for multiple gardeners (either home or commercial vendors) to produce seeds of the same Variety.

VarietyID management

VarietyIDs have the format variety-<country>-<chapterCode>-<varietyNum>-<millis>. Please see the ID Section for details regarding our approach to ID management.

Like CropIDs, VarietyIDs embed the country code and chapterCode. (Like CropIDs, ChapterCodes could be a number like '001' in the case of a US Chapter, or a postal code like 'VNZ76T' in the case of a non-US chapter.)

In the event that a Chapter is divided into two or more smaller chapters, each of the new Chapters needs a copy of the Variety collection with the updated chapterCode. This will require a pass through all of the Garden-level entities to update their varietyID fields to the new string value.

VarietyNums start at 301 for each chapter.

Field notes

Note that we cache the Crop Name because it will rarely, if ever, change and it is useful to have it in the Variety document so that we can return the full name without needing the Crop collection.

That implies, however, that if the name of a Crop is ever changed, then we must find all of the Variety documents associated with that cropID and update the cachedCropName field. This is an acceptable trade-off.

Variety entity representation

const factory Variety(
{required String varietyID, // 'variety-US-001-302-7654'
required String chapterID, // 'chapter-US-001'
required String cropID, // 'crop-US-001-203-2354'
required String cachedCropName, // 'Asparagus'
required String name} // 'Jersey Knight'
)

Planting

A Planting represents a set of plants of the same variety (or crop), planted in a single bed, all with the same approximate timings (i.e. sow date, transplant date, first harvest date, etc.).

If the same variety (or crop) is planted in two different beds, then this must be represented by two Planting instances.

It is common during the garden planning process to first design the garden at the "crop" level, and then later refine the plan by specifying the specific variety to be planted. To support this incremental planning process, you can create a Planting instance and specify only the Crop, not the Variety.

Plantings and seeds

One innovative feature of GGC is that we provide an explicit representation of the seeds grown by a Planting. Here is how it manifests in the Planting entity.

In each Planting document, we two optional fields called sowSeedID and harvestSeedID. The sowSeedID represents the seeds from which this Planting was grown (if known), and the harvestSeedID represents the seeds produced by this Planting (if any were produced).

Finally, there is a boolean field called seedsAvailable. If true, this means not only that the Planting grew seeds (and thus there is a harvestSeedID), but that this gardener is willing to share these seeds with others in the Chapter. When seedsAvailable is true, then other Gardeners looking at the Variety associated with this planting will see that they can contact the owner of this Garden to request seeds from this Planting. They might also be able to see the Outcome data for this Planting, which provides some evidence for the future success of these seeds when grown.

PlantingID management

PlantingIDs have the format planting-<country>-<postalCode>-<gardenNum>-<plantingNum>-<millis>. Please see the ID Section for details regarding our approach to ID management.

The country and postal code fields in the ID must match those fields in the gardenID associated with this Planting.

Since, over a period of years, a single garden can result in over a thousand plantings, we generally use a four digit number for the plantingNum.

PlantingNums start at 1001 for each garden.

Field notes

Validators should guarantee that startDate < transplantDate < firstHarvestDate < endHarvestDate < pullDate.

All Plantings must have a startDate, a pullDate, and a bedID. These values are required so that the Planting can be displayed as a horizontal bar in the Garden details view.

If a Gardener wants to indicate that seeds are available, they must provide the Variety for this Planting.

If the gardener sets usedGreenhouse to true, then they should (eventually) record a transplantDate, although this is not mandatory.

Note that if both a cropID and varietyID is provided, then the varietyID must "match" the cropID. Put another way, the associated Variety's cropID field should match the Planting's cropID field. (Put yet another way, this would be illegal: a Planting in which the Crop is "Corn" but the Variety is "Big Boy (Tomato)"). The UI for defining and managing Planting entities will enforce this by only showing the Varieties associated with the currently selected Crop.

Planting entity representation

factory Planting(
{required String plantingID, // 'planting-US-98225-102-1001-7645'
required String chapterID, // 'chapter-US-001'
required String gardenID, // 'garden-US-98225-102-5678'
required String cropID, // 'crop-US-001-202-9432'
required String cachedCropName,// 'Bean'
required String bedID, // 'bed-US-98225-102-003-4823'
required String cachedBedName, // '02'
required DateTime startDate, // '2023-03-19T12:19:14.164090'
required DateTime pullDate, // '2023-07-19T12:19:14.164090'
String? gardenerID, // The owner of the garden, i.e. 'johnson@hawaii.edu'.
String? varietyID, // null, 'variety-US-001-310-7645'
String? cachedVarietyName, // null, 'Big Boy'
String? outcomeID, // null, 'outcome-US-98225-102-1001-3472'
DateTime? transplantDate, // null, '2023-04-19T12:19:14.164090'
DateTime? firstHarvestDate, // null, '2023-05-19T12:19:14.164090'
DateTime? endHarvestDate, // null, '2023-06-19T12:19:14.164090'
String? sowSeedID, // null, 'seed-US-98225-102-001-3563'
String? harvestSeedID, // null, 'seed-US-98225-102-005-2185'
(false) bool usedGreenhouse, // true, false
(false) bool isVendor, // true, false
(false) bool seedsAvailable} // true, false
)

Outcome

Outcome data is gardener-supplied information about the result of a single Planting. We want to specify planting results in a way that:

  • Is useful and actionable for gardeners,
  • Captures important properties of a planting,
  • Is relatively easy to provide,
  • Is interpreted in a relatively consistent manner by different gardeners,

To support these requirements, we define five outcome types: germination, yield, flavor, pest and disease resistance, and appearance. Each planting can receive a "grade" for each of these outcome types on a five point scale. The following table presents the definitions for each scale value for each outcome type.

12345
GerminationNone. No germination.Poor. ~25% germination.OK. ~50% germination.Good. ~75% germination.Excellent. >90% germination..
YieldNone. Died and/or no foodPoor. Less food than expectedOK. Expected amount of foodGood. More food than expectedExcellent. TWay more food than expected
FlavorBad. Unappealing flavorPoor. Bland flavorOK. Expected flavor.Good. Enjoyable flavorExcellent. Awesome flavor.
Pest and disease resistanceVery poor. >90% damagedPoor. ~50% damagedOK. <25% damagedGood. Very few damagedExcellent. No damage.
AppearanceVery poor. >90% uglyPoor. ~60% uglyOK. ~60% not uglyGood. ~60% beautifulExcellent. >90% beautiful

In addition, an Outcome type can have a value of "0", which means there is no data regarding that type of outcome.

OutcomeID management

OutcomeIDs have the format outcome-<country>-<postalCode>-<gardenNum>-<outcomeNum>-<millis>. Please see the ID Section for details regarding our approach to ID management.

Each Outcome entity is associated with exactly one Planting entity. (Note that the converse is not true: a Planting entity need not be associated with an Outcome entity, since the Gardener might not choose to record any Outcome data.)

Field notes

Outcomes cache the cropID and varietyID associated with their Planting. This is to allow Index and View widgets to display Outcome data without having to retrieve Plantings from the database.

Outcome value must be integers between 0 (indicating no data) and 5 (indicating Excellent).

Outcome entity representation

const factory Outcome(
{required String outcomeID, // 'outcome-US-98225-102-1001-5218'
required String chapterID, // 'chapter-US-001'
required String gardenID, // 'garden-US-98225-102-6789'
required String plantingID, // 'planting-US-98225-102-1001-9213'
required String cachedCropID, // 'crop-US-001-245-4376'
required String cachedVarietyID, // 'variety-US-001-321-3214'
(0) int germination, // 0-5
(0) int yieldd, // 0-5 (yield is a reserved word)
(0) int flavor, // 0-5
(0) int resistance, // 0-5
(0) int appearance} // 0-5
)

Seed

The ability to save and share seeds within a Chapter is a significant core value proposition for GGC.

By "seed", we don't mean each individual, tiny seed. We mean the set of all seeds harvested from a planting in a garden in a particular season, or the set of seeds in a seed packet from a commercial vendor.

Our data model enables us to represent both seeds that are locally produced by gardeners as well as seeds that are produced by vendors. Because a Planting can represent both the seeds that were used to grow it (in the field sowSeedID) as well as the seeds that it produced and could be used to grow a new Planting in a subsequent season (in the field harvestSeedID), we get the ability to track the "provenance" of a seed:

SeedID management

SeedIDs have the format seed-<country>-<postalCode>-<gardenNum>-<seedNum>-<millis>. Please see the ID Section for details regarding our approach to ID management.

SeedNums start at 001.

The country and postal code fields are taken from the Planting that this seed was harvested from. This is to ensure that if a Chapter is reorganized, the Seed will move with the Planting it was harvested from.

Chapter reorganization and seeds

Note that Seeds harvested from one postal code in a Chapter can be sowed in another postal code in a Chapter. This means that if a Chapter is split up into two sub-Chapters, there is the possibility that the original Seed will need to be "cloned" into the two sub-Chapters.

Field notes

Seed instances cache the cropID, varietyID, and the seedsAvailable field from the Planting from which they were harvested.

A Seed instance is always associated with the Planting from which it was harvested, as it's ID will appear in the harvestSeedID field of the Planting. In this case, the Seed instance's GardenID and the Planting instance's GardenID must be the same.

A Seed instance can also be associated with one or more additional Plantings as the seed from which the Planting was grown. In this case, the Seed's ID appears in the Planting in the sowSeedID field. Those Plantings do not have to be in the same Garden (in fact, they will often be in a different garden).

The Seed entity provides information about where it was harvested from (but not about where it was used to sow new Plantings). This information includes the gardenerID, cropID, cropName, varietyID, varietyName and seedsAvailable. Providing this information in the Seed entity simplifies presentation of Seed data in Index and View pages.

Finally, in order to safely delete a Seed instance, it must not have been used to sow any Plantings. So that we don't have to search through all the Plantings across an entire chapter, the Seed entity provides a field called sowSeedCount. This field is initialized to zero and incremented whenever a Seed instance is referenced in the sowSeedID field of a new Planting. A Seed instance can only be deleted when the sowSeedCount is zero.

::: warning In the beta release, sowSeedCount is incremented only a Planting creation and decremented only on Planting deletion. It does not track Planting updates. Because of this, in the beta release, once a Seed is created it is never deleted because we can't safely rely on the sowSeedCount.) :::

Seed entity representation

const factory Seed(
{required String seedID, // 'seed-US-98225-102-001-3218'
required String chapterID, // 'chapter-US-001'
required String gardenID, // 'garden-US-98225-102-6789'
(0) int sowSeedCount, // 0, 1, 2
required String cachedGardenerID, // 'info@heritageseeds.com'
required String cachedCropID, // 'crop-US-001-201-3462'
required String cachedVarietyID, // 'variety-US-001-303-6534'
required String cachedCropName, // 'Tomato'
required String cachedVarietyName, // 'Cherokee Purple'
(true) bool cachedSeedsAvailable} // true, false
)

Seed caveats

In GGC, a Garden associated with a Vendor has a single Planting instance for each Variety that they offer Seeds for. This single Planting instance will have a single Seed instance in the harvestSeedID field, with seedsAvailable set to true.

In reality, a vendor may or may not have seeds in stock for a given Variety at any given time. And, in reality, a vendor will produce their seeds from new Plantings each year. But, GGC will not attempt to keep track of real-time inventory.

Observation

An Observation is a textual comment (and, typically, a picture) provided by a Gardener regarding a specific Planting at a specific point in time.

If a Gardener wishes to make a comment about a non-Planting issue (i.e. their Garden, or the Chapter, or whatever), they can use the Chat Rooms for Gardens and Chapters.

The essential difference is that an Observation will be "carried along" with a Planting---in other words, when the Gardener retrieves a View of a specific Planting, they will also see all of the Observations associated with that Planting. We hope that this will help create a useful historical record of a Planting.

ObservationID management

ObservationIDs have the format observation-<country>-<postalCode>-<gardenNum>-<observationNum>-<millis>. Please see the ID Section for details regarding our approach to ID management.

ObservationNums start at 4001 and are incremented chapter-wide.

The country and postal code fields are taken from the Planting associated with this Observation.

Field notes

Observations cache several values in order to allow the Observation card to present information without having to retrieve the Planting.

Observations are presented in reverse chronological order by lastUpdate. When someone adds a comment, that sets the lastUpdate field.

Observation entity representation

const factory Observation(
{required String observationID, // 'observation-US-98225-102-4001-5634'
required String chapterID, // 'chapter-US-001'
required String gardenID, // 'garden-US-98225-102-6789'
required String gardenerID, // 'johnson@hawaii.edu'
required String plantingID, // 'planting-US-98225-102-1002-9432'
required DateTime observationDate, // '2023-03-19T12:19:14.164090'
required DateTime lastUpdate, // '2023-03-19T12:19:14.164090'
required List<String> tagIDs, // ['tag-001-501']
required List<ObservationComment> comments, // ['observation-US-98225-102-4001-001-9876']
required String description, // 'First harvest of the season'
String? pictureURL, // null, 'https://firebasestorage.googleapis.com/v0/...'
(false) bool isPrivate, // true, false
required String cachedCropID, // 'crop-US-001-243-3425'
required String cachedVarietyID, // 'variety-US-001-323-9654'
required String cachedBedName, // '03'
required String cachedCropName, // 'Tomato'
required String cachedVarietyName, // 'Cherokee Purple'
required String cachedGardenName, // 'Kale is for Kids'
required DateTime cachedStartDate} // '2023-03-19T12:19:14.164090'
)

Observation Comments

As shown above, each Observation entity includes an embedded (potentially empty) list of ObservationComments, which have this structure:

const factory ObservationComment(
{required String observationCommentID, // 'observationcomment-US-98225-102-4001-001-4532'
required String gardenerID, // 'johnson@hawaii.edu'
required String description, // 'Is that an aphid on the left leaf?'
required DateTime lastUpdate} // '2023-03-19T12:19:14.164090'
)

The lastUpdate field indicates when the comment was made or updated.

Tag

The Tag entity provides "meta-data" that a gardener can use to provide information about the nature of an Observation. Tags serve two basic purposes:

  1. Filtering. A user can specify a set of Tags and filter the Observations by those that satisfy either (both?) of them.

  2. Badge achievement. Many Badges are earned, at least in part, by posting (public) Observations with specific Tags.

Tags, like Badges, Families, and Chapters, are "global" entities that are not Chapter-specific. Therefore, they can only be managed by system admins.

TagID management

TagIDs have the format tag-<tagNum>. Please see the ID Section for details regarding our approach to ID management.

TagIDs start at 001.

Tag entity representation

const factory Tag(
{required String tagID, // 'tag-001'
required String name, // '#Biodiversity'
required String description} // 'Use of practices to increase biodiversity...'
)

Task

A Task specifies an activity to perform for a specific Planting in a specific Garden. There are two types of tasks:

  1. An automatically created Task that is generated from the dates associated with a Planting, such as transplant date or first harvest date. Whenever the Gardener adjusts the dates associated with a Planting, the associated Task is updated. Conversely, if a Gardener adjusts the date associated with a Task, then the associated Planting date is updated as well.

  2. A manually created Task created by a gardener, such as Weed cucumbers or Add top dressing to radishes.

Tasks are ephemeral. When a Gardener indicates that a task has been completed, it is deleted from the system. For automatically created Tasks that are associated with a Planting date, the system prompts the gardener to verify the completion date prior to deleting the Task. This prompt is used to update the date in the Planting instance. This is an important form of "quality assurance" for Planting dates, since the Gardener typically specifies these dates early in the season during planning. The ability of Tasks to help ensure that Planting dates are accurate can make Chapter data more useful.

Non-ephemeral (manually generated) tasks would be cool

Currently, all tasks are ephemeral. It would be potentially useful for a Gardener to be able to mark a manually generated Task as "non-ephemeral". This would mean that if the Gardener plans a future Garden, that task could be retrieved and associated with a new Planting.

We will leave this as a feature for a future release.

TaskID management

TaskIDs have the format task-<country>-<postalCode>-<gardenNum>-<plantingNum>-<taskNum>-<millis>. Please see the ID Section for details regarding our approach to ID management.

TaskIDs start at 001.

The country, postal code, gardenNum, and plantingNum fields are taken from the Planting associated with this Task.

Task Types

Each Task has a TaskType:

enum TaskType { start, transplant, firstHarvest, endHarvest, pull, other }

The first five correspond to the Planting dates. "Other" is used for manually created Tasks.

Task titles and descriptions

For automatically generated tasks, the title is automatically generated using the task type plus the variety, for example "Start Tomato (Big Boy)". Automatically generated tasks are not created with a description.

For manually generated tasks, the Gardener must specify the title and can also supply a description if desired.

Task entity representation

factory Task(
{required String taskID, // 'task-US-98225-101-1003-001-7634'
required String chapterID, // 'chapter-US-001'
required String gardenID, // 'garden-US-98225-101-6789'
required String taskType, // 'start'
required String title, // 'Start Tomato (Big Boy)'
String? gardenerID, // The owner of the garden, i.e. 'johnson@hawaii.edu'.
String? description, // null, 'Clean up ground cherries.'
required String cropID, // 'crop-US-001-203-5412'
required String varietyID, // 'variety-US-001-101-304-6534'
required String bedID, // 'bed-US-98225-101-003-8956'
required String plantingID, // 'planting-US-98225-101-1003-3214'
required DateTime dueDate, // '2023-03-19T12:19:14.164090'
required String cachedBedName, // '02'
required String cachedCropName, // 'Tomato'
required String cachedVarietyName} // 'Kale is for Kids'
)

Badge

GGC provides a game mechanic called "Badges". These are designations for Gardens, Gardeners, and (in future) Chapters that recognize the use of best practices for gardening (such as composting), or significant experience with a specific crop, or other behaviors that we wish to encourage.

The Badge game mechanic is implemented through two entities: "Badge" and "BadgeInstance". The Badge entity is a global entity (i.e. independent of any Chapter and defined by the system), and defines the game mechanic. The BadgeInstance entity represents the achievement of a Badge by a Garden, Gardener, or (in future) Chapter.

BadgeID and BadgeInstanceID management

BadgeIDs have the format badge-<badgeNum>. Please see the ID Section for details regarding our approach to ID management.

BadgeIDs start at 001.

BadgeInstanceIDs have the format badgeinstance-<country>-<chapterCode>-<badgeinstanceNum>-<millis>.

BadgeInstanceNums start at 001.

The country and chapterCode fields are taken from the Chapter associated with this Garden or Gardener (and in the future, Chapter).

There is a BadgeType enum represented as follows:

enum BadgeType { garden, gardener, chapter }

Badge entity representation

Badges:

const factory Badge(
{required String badgeID, // 'badge-001'
required String type, // 'garden'
required String name, // 'Climate Victory'
required String criteria, // 'A climate victory garden has been...'
required String level1, // 'The garden is present...'
required String level2, // 'The garden is present..., and...'
required String level3, // 'The garden is present..., and..., and...'
required List<String> tagIDs} // ['tag-024', 'tag-037']
)

Badge Instances:

const factory BadgeInstance(
{required String badgeInstanceID, // 'badgeinstance-US-001-001-5634'
required String chapterID, // 'chapter-US-001'
required String badgeID, // 'badge-001'
required int level, // 1
required String id, // 'johnson@hawaii.edu', 'garden-US-98225-101-6789', 'chapter-US-001'
required String type, // 'gardener', 'garden', 'chapter'
required String cachedName, // 'Climate Victory'
String? data, // null, 'supplementary data'
String? data2, // null, 'supplementary data2'
String? data3} // null, 'supplementary data3'
)

Collections and business logic

As noted above, each entity is represented as a Dart class, and made persistent as a document in Firebase.

Groups of entity instances of the same type are also represented as a Dart class, and made persistent as a collection in Firebase. So, for example, there is a Dart class called "Chapter" (to represent individual instances of that entity) and a Dart class called "ChapterCollection" (to manage a set of Chapter instances). On the Firebase side, there is a collection called Chapters, and each document in that collection has the same structure as the corresponding Dart class. We use freezed to support the translation between the Dart class instance for an entity and its persistent representation as a Firebase document in JSON format.

The client-side collection classes (ChapterCollection, GardenCollection, etc) are intended to encapsulate the "business logic" for the application.

Privacy

On the one hand, we want to preserve certain types of privacy:

  • Users pick a unique "username" which is used in postings so that they do not have to reveal their true name.
  • The application does not reveal (and does not know) the precise location of gardens, only their country and postal code.
  • Users can tag an Observation as "private", and in that case it will not be visible to users outside of the garden's owner and editors. This allows users to take photos regarding the garden for their personal data collection without feeling inhibited about it becoming "public". For example, the photo might reveal faces or locations.

On the other hand, we want to facilitate the creation of a community of practice. For this reason, all garden data (plantings, etc) are available, in at least a read-only format, to all members of a chapter.

A significant goal for the beta release is to test the hypothesis that it is not problematic for users to share these kinds garden details with others in the chapter.

A broader question, that we will not explore in the beta release, is what kinds of data could be made available across Chapters.

IDs

In NoSQL databases, it is common for each document to be automatically provided upon creation with a unique string called a "docID" which looks something like this: tghHU4CVfxHGB. The docID is generated by the server and is guaranteed to be unique. It serves as the primary key for entities in that collection.

In GGC, we use a different approach. There is no "docID" field. Instead, the Crop collection has a unique ID called "cropID", the Chapter collection has a unique ID called "chapterID", and so forth. We tell the NoSQL database (in our case, Firebase) that these various ID fields should be used as the primary key (i.e. the docID) for each of the collections.

Importantly, non-global entities are generally created by clients, and in GGC, clients (not the server) are responsible for generating the primary keys for non-global entities. (The global entities, such as Chapter, Family, Badge, etc. are constructed by the system, not clients.)

We have clients generate the primary keys for non-global entities for the following reasons:

  • Rather than a server-generated random string, our client-generated primary keys are "human-readable". You can look at an ID string and know what kind of entity it is associated with (all GGC IDs have a prefix like "chapter-", "crop-", etc). Since many entities have fields containing the IDs of other entities, human-readable IDs help in development and system understanding.
  • In many cases, an update to the database can involve the creation of a new entity (or entities) as well as updates to other entities to include the primary key of the newly created entity (or entities). If primary keys are generated by the server, such updates would become a complex, multi-step process. Since primary keys are generated by the client, these updates are much more simple to accomplish.

However, client-generated primary keys have one significant drawback:

  • It becomes technically possible for two clients to generate a "primary key collision", i.e. an attempt by different clients to create two entities with the same primary key value at the same time.

To deal with this drawback, we have carefully designed the primary keys in GGC to make it extremely unlikely for primary key collisions to occur.

First, primary keys are constructed to include one or more of the chapterID, the country code, the postal code, or the gardenID. This means, for example, that rather than it being possible for a primary key collision to occur by any two GGC users anywhere in the world, it is becomes only possible for it to occur between the owner and editors of a single garden.

Second, client-generated primary keys are constructed with a "millis" field. This is a four digit number representing the millisecond value at the time the client created the primary key.

We believe that these two properties of primary keys mean that collisions will not occur in practice, even when clients are operating in disconnected mode.

Finally, let's say that this exceedingly unlikely event actually occurs. In that case, because we have told Firebase that the plantingID (for example) is the primary key, Firebase will reject the second plantingID creation. In this case, the application can simply report the error and instruct the user to try again in a few seconds. By this time, the local cache should be updated and the request to create the new entity should succeed.

Note that Firebase recommends against creating documentIDs with lexicographically close ranges. We expect that the inclusion of the millis field mitigates this potential performance issue.

Normalization and caching

A best practice for relational database design is "normalization", which means that a value should only occur in one place at a time. Normalization has a number of virtues, such as making updates and deletions more efficient and less error prone. But normalization also has a cost: queries can become very complicated, involving complex "joins" from a variety of data sources.

The GGC app has the following design considerations that impact on the issue of normalization:

  • Updates and deletions are (relatively) rare. GGC is mostly an "additive" database. While deletions and updates can occur, it's OK if they are "expensive".
  • Reads are common, and to make these reads fast, GGC implements client-side caches (using Riverpod) for many of the entities.
  • Gardeners do not access to data outside their Chapter, so client-side caches are not impacted if the number of Chapters in GGC becomes large.

To simplify retrieval and caching of the appropriate chapter or garden-level "slice" of the database by a client, almost all GGC entities include a chapterID and gardenID field.

We also "denormalize" by providing "redundant" fields in certain entities. For example, in some cases a document will include a cropName field even though it already has a cropID field. We do this avoid having to download large numbers of documents (i.e. Plantings for all Gardens in the Chapter) in order to perform a calculation. These redundant field names have the prefix "cached" in order to make this denormalization explicit in the data model.

Root collections vs subcollections

In Firebase, you can organize the data into root collections or subcollections, as explained in Choose a data structure.

Since GGC involves many many-to-many relationships, we choose to organize all of our data as root collections.

In Firebase, there are no performance differences between root collections and subcollections, so we do not gain or lose anything by making this choice.

Chat rooms

We use the Flutter Chat UI package to implement Chat rooms and users. This results in the addition of some collections to Firebase. We do not document this here.

- + \ No newline at end of file diff --git a/docs/develop/release-1.0/design-components/data-mutation.html b/docs/develop/release-1.0/design-components/data-mutation.html index 6ef076f36..ea53174a8 100644 --- a/docs/develop/release-1.0/design-components/data-mutation.html +++ b/docs/develop/release-1.0/design-components/data-mutation.html @@ -5,13 +5,13 @@ Data Mutation | Geo Garden Club - +

Data Mutation

Prelude: AsyncValue

When your code interacts with the database (or some other external service), you are generally in one of two situations:

  1. Reading data: In this case, use a with widget to retrieve the appropriate data for display, and separate the asynchronous code (to retrieve data from the database) from the synchronous code (to display it in the UI.)
  2. Writing data: In this case you must write asynchronous code to update the contents of the database.

The Flutterverse is filled with articles and example code on how to accomplish (2). For GGC, we will use the "Riverpod" design pattern, which involves:

  1. Define a Riverpod provider (using the @riverpod annotation) to perform the manipulation.
  2. Handle the resulting AsyncValue's three possible states: loading, error, data.

Here are some useful readings to get you started:

Now let's look at how we implement data mutation in GGC

Data mutation in GGC

In GGC, "data mutation" refers to creating, updating, and deleting entities from the database. In some cases, mutating one entity (i.e. deleting a Garden) requires the implicit mutation of many other entities (i.e. deleting the Garden's associated Beds, Plantings, Observations, Outcomes, and Tasks).

Accomplishing a data mutation involves a complex interaction between the front-end user interface and the back-end database. There are many potential ways to accomplish this interaction, but we will follow a design pattern documented by Andrea Bizzotti in his various Code With Andrea tutorials, with some additional customizations to suit our own GGC architecture.

The CreateGardenScreen and MutateGardenController classes illustrate our data mutation design pattern.

Here is a walkthrough of some of the Garden code to illustrate the basic ideas of this design pattern.

1. The data mutation widget

A "Data mutation widget" (for example, UpdateGardenScreen) presents a user interface for performing a data mutation. The actual UI component displayed at any moment in time by the widget is determined by an associated controller (for example, MutateGardenController). The controller indicates which of four UI components to present: (1) an initial UI component (typically a form), (2) a loading indicator UI component (while waiting for an asynchronous action to complete, (3) a "success" component (displayed if the asynchronous action completes successfully) or (4) an error UI component (displayed if the asynchronous completes with an error).

Here's an excerpt of UpdateGardenScreen illustrating the basic way in which the controller controls the UI state of the screen:

lib/features/garden/presentation/update_garden_screen.dart
  AsyncValue asyncUpdate = ref.watch(mutateGardenControllerProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Update Garden'),
actions: [HelpButton(routeName: AppRoute.updateGarden.name)],
),
body: asyncUpdate.when(
data: (_) => updateGardenForm(),
loading: () => const GgcLoadingIndicator(),
error: (e, st) => GgcError(e.toString(), st.toString())));
}

2. The onSubmit() method

If the initial UI component is a form, then it should have an async onSubmit() callback method. This method typically involves a sequence of three phases. The first phase checks that the form field values pass any validation criteria. If so, the second phase creates domain model entities as indicated by the form values. The third phase calls the appropriate mutate controller method, passing it the domain entities and an onSuccess() callback, which tells the controller which page to go to if the data mutation is successful.

Here's an example:

lib/features/garden/presentation/edit_garden_screen.dart
   onSubmit() async {
// 1. Check that form fields are valid.
bool isValid = _formKey.currentState?.saveAndValidate() ?? false;
if (!isValid) return;
// 2. Create domain objects to send to controller.
String name = FieldKey.gardenTextField.currentState?.value;
List<dynamic> xFiles =
FieldKey.singleImagePicker.currentState?.value ?? [];
String editorsString =
FieldKey.editorsTextField.currentState?.value ?? '';
Garden garden = gardens.getGarden(gardenID);
List<String> updatedEditorUserIDs = users.parseUsernames(editorsString);
List<Editor> editorsToAdd = gardens.editors.makeNewEditors(
gardenID: gardenID,
chapterID: garden.chapterID,
gardenerIDs: updatedEditorUserIDs);
List<Editor> editorsToDelete = gardens.editors.getEditors(gardenID);
// Only update Editors collection if the field has changed.
if (gardens.editors.sameEditorList(editorsToAdd, editorsToDelete)) {
editorsToAdd = [];
editorsToDelete = [];
}
String profilePictureUrl = (xFiles.isNotEmpty && xFiles[0] is XFile)
? await ImageStorage.cropAndUploadImage(
xFile: xFiles[0], entityID: gardenID, context: context)
: garden.profilePicture;
Garden updatedGarden = Garden(
gardenID: gardenID,
name: name,
profilePicture: profilePictureUrl,
chapterID: chapters.currentChapterID,
cropIDs: garden.cropIDs,
sharedSeedIDs: garden.sharedSeedIDs,
lastUpdate: DateTime.now(),
ownerID: users.currentUserID,
pictures: []);
// 3. Use controller to invoke updates on database.
ref.read(mutateGardenControllerProvider.notifier).updateGarden(
garden: updatedGarden,
editorsToAdd: editorsToAdd,
editorsToDelete: editorsToDelete,
onSuccess: () {
context.pop();
GlobalSnackBar.show('Garden "$name" updated.');
},
);
}
Don't pass Collection classes to the controller method

To maintain separation of concerns, the values passed to mutate controller methods should be individual domain entities (i.e. Garden, Editor), lists of domain entities (i.e. List<Garden>, List<Editor>), or primitive types (String, int, etc). Don't pass collections (i.e. GardenCollection, EditorCollection). Use these collection classes within the onSubmit() method to determine the domain entities to pass.

3. Mutate controller create, update, delete methods

The Mutate Controller class typically implements create, update, and delete methods to handle the associated mutation. These methods will often need to make multiple asynchronous calls to the backend database. To do this efficiently, and also to provide atomicity, the controller should use the Firestore batched write facility.

Here is an example from MutateGardenController for creating a new Garden. Note that both the Garden and Editor databases are mutated:

lib/features/garden/presentation/mutate_garden_controller.dart
 Future<void> createGarden({
required Garden garden,
required List<Editor> editors,
required VoidCallback onSuccess,
}) async {
state = const AsyncLoading();
AsyncValue nextState = const AsyncLoading();
GardenDatabase gardenDatabase = ref.watch(gardenDatabaseProvider);
EditorDatabase editorDatabase = ref.watch(editorDatabaseProvider);
final WriteBatch batch = FirebaseFirestore.instance.batch();
gardenDatabase.setGardenBatch(batch, garden);
editorDatabase.addEditorsBatch(batch, editors);
await batch
.commit()
.then((_) => nextState = const AsyncValue.data(null))
.catchError((e, st) => nextState = AsyncValue.error(e, st));
if (mounted) {
state = nextState;
}
if (!state.hasError) {
onSuccess();
}
}

Following the CodeWithAndrea guidelines, this method first sets the controller state to AsyncLoading. Then it gets the databases of interest, creates a batch variable, and adds mutations to that batch variable by passing it into the appropriate methods in the variable database classes. Finally, it invokes the batch.commit() method to do all of the mutations at once, and either sets the state to AsyncData() if everything went well or AsyncError() if a problem occurred. A nice feature of batched writes is that they are performed as a transaction---either all of the writes succeed, or none of them do.

4. Database methods

The final part of this coding standard involves the appropriate definition of database methods. As shown above, database methods should be written to accept a batch parameter, and result in that parameter being updated with additional operations to perform. Here is an example:

"lib/features/garden/data/editor_database.dart
 void createEditorsBatch(WriteBatch batch, List<Editor> editors) {
for (Editor editor in editors) {
_service.setDataBatch(
batch: batch,
path: FirestorePath.editor(editor.editorID),
data: editor.toJson());
}
}

A template for the controller class

There is some boilerplate code for controllers. To make it a little easier to create new controllers, here is a template. See the TODO comments for places where code needs to be added, and replace all occurrences of "TEMPLATE" by the entity being controller (i.e. Garden, User, Task, etc).

Note that we'll use "create" rather than "add" to conform to the CRUD acronym. This means that the associated screens should be changed from "AddX" to "CreateX".

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'mutate_TEMPLATE_controller.g.dart';


class MutateTEMPLATEController extends _$MutateTEMPLATEController {
bool mounted = true;


FutureOr<void> build() {
ref.onDispose(() => mounted = false);
state = const AsyncData(null);
}

Future<void> createTEMPLATE({
/// TODO: Pass in domain object here.
required VoidCallback onSuccess,
}) async {
state = const AsyncLoading();
AsyncValue nextState = const AsyncLoading();
// TODO: Watch the appropriate database instances here.
final WriteBatch batch = FirebaseFirestore.instance.batch();
// TODO: Invoke the database batch methods here.
await batch
.commit()
.then((_) => nextState = const AsyncValue.data(null))
.catchError((e, st) => nextState = AsyncValue.error(e, st));
if (mounted) {
state = nextState;
}
if (!state.hasError) {
onSuccess();
}
}


Future<void> updateTEMPLATE({
/// TODO: Pass in domain data here
required VoidCallback onSuccess,
}) async {
state = const AsyncLoading();
AsyncValue nextState = const AsyncLoading();
/// TODO: ref.watch the appropriate databases here.
final WriteBatch batch = FirebaseFirestore.instance.batch();
/// TODO: Invoke the appropriate database batch methods here.
await batch
.commit()
.then((_) => nextState = const AsyncValue.data(null))
.catchError((e, st) => nextState = AsyncValue.error(e, st));
if (mounted) {
state = nextState;
}
if (!state.hasError) {
onSuccess();
}
}

Future<void> deleteTEMPLATE({
/// TODO: Pass in the appropriate domain objects here
required VoidCallback onSuccess,
}) async {
state = const AsyncLoading();
AsyncValue nextState = const AsyncLoading();
/// TODO: Watch the appropriate databases here.
final WriteBatch batch = FirebaseFirestore.instance.batch();
/// TODO: Invoke the appropriate database batch methods here.
await batch
.commit()
.then((_) => nextState = const AsyncValue.data(null))
.catchError((e, st) => nextState = AsyncValue.error(e, st));
if (mounted) {
state = nextState;
}
if (!state.hasError) {
onSuccess();
}
}
}
Caveats and gotchas

Here are some issues:

  1. Batched writes are limited to 500 operations. Our current database organization will result in exceeding that limit for gardens of reasonable size (i.e. hundreds of plantings). This means we really need to reorganize the database to use subcollections. Then, for example, deleting a garden will delete all of its associated plantings in one batch operation.
  2. Collection classes shouldn't access the database methods at all. We should remove those methods.
  3. Remove database fields from collection classes. We should access databases using Riverpod provider variables.
  4. Database methods should return Futures, and not implement then() or catchError() clauses.
  5. WithGarden now provides access to Observations, Tasks, and Outcomes. The "extended" WithGarden widgets might no longer be necessary.
- + \ No newline at end of file diff --git a/docs/develop/release-1.0/design-components/input-fields.html b/docs/develop/release-1.0/design-components/input-fields.html index 69439db36..a9812da2d 100644 --- a/docs/develop/release-1.0/design-components/input-fields.html +++ b/docs/develop/release-1.0/design-components/input-fields.html @@ -5,13 +5,13 @@ GGC Input Fields | Geo Garden Club - +

GGC Input Fields

Motivation

GGC uses the Flutter Form Builder package to support data collection from gardeners. Flutter Form Builder simplifies form-based data collection by reducing the code needed to: (a) build a form, (b) validate fields, (c) react to changes, and (d) collect final user input.

While this is great, Flutter Form Builder does not, by itself, accomplish two additional important design goals for GGC:

  • Provide specialized widgets for commonly used GGC data input fields. For example, a dropdown displaying the names of all gardens associated with this user; and
  • Provide a single location for specifying the look-and-feel for input fields. We want to minimize the amount of duplicated code (and hopefully eliminate look-and-feel code) when creating a form to collect data in a screen.

There is a third design goal as well. GGC sometimes wants to use input fields outside the context of a "form"---i.e. a context in which data is gathered but not made available to the system until a "Submit" button is pressed. For example, the Outcome screen has input fields to select a garden, crop, and/or variety, and as these fields are manipulated by the user, the screen immediately refreshes to show Outcome data filtered by the values of the input fields. There is no "Submit" button in this screen, and so some of the Flutter Form Builder mechanisms are not used. The third design goal is:

  • Support both in-form and outside-form contexts without having to create two separate Garden dropdown widgets (for example).

To support these three design goals, GGC provides a set of custom input fields (in the lib/features/common/input-fields directory). We call these "GGC Input Fields" to distinguish them from "Form Builder Input Fields".

The goal of this page is to document how GGC Input Fields are created and used in order to facilitate their future evolution.

Background: Form Builder Input Fields

A good overview of Form Builder Input Fields and their use is available in the Flutter Form Builder Readme. As noted in the Parameters section, there are several attributes that all Form Builder Input Fields support. In many cases, a GGC Input Field will provide a value for these standard attributes:

Form Builder Input Field AttributeGGC Input Field Value
nameThe input field name, i.e. "New Garden Name", "Garden Dropdown", etc.
initialValueNot typically needed.
enabledSame default (true)
decorationProvided: implements standard border, icons, and styles across all GGC input fields
validatorProvided as needed. For example, the "New Garden Name" input field will validate that the provided string does not match any other garden name (case-insensitive, spaces and special characters removed).
onChangedMade available in case input field is used outside of a form
valueTransformerA function might be provided for some GGC Input Fields, not sure yet.

Let's look at a simple Form using Form Builder, which displays two text fields ("Email" and "Password") and a "Login" button.

final _formKey = GlobalKey<FormBuilderState>();

FormBuilder(
key: _formKey,
child: Column(
children: [
FormBuilderTextField(
key: _emailFieldKey,
name: 'email',
decoration: const InputDecoration(labelText: 'Email'),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.email(),
]),
),
const SizedBox(height: 10),
FormBuilderTextField(
name: 'password',
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
]),
),
MaterialButton(
color: Theme.of(context).colorScheme.secondary,
onPressed: () {
if (_formKey.currentState?.saveAndValidate() ?? false) {
debugPrint('validation succeeded');
debugPrint(_formKey.currentState?.value.toString());
} else {
debugPrint('validation failed');
}
},
child: const Text('Login'),
)
],
),
),

Form Builder provides pre-defined input fields for the following types of input controllers: Checkbox, Radio Button, Date Picker, Dropdown, Slider, Toggle, and Text Field. In addition, the Form Builder Extra Fields package provides input controllers for: Color Picker, Rating, Searchable Dropdown, Signature Pad, Spinnable Number Selector, and Text Field with Auto-Complete.

If you want to build a custom field, there is a set of Example Custom Fields, as well as two how-to articles: Building a Custom Field with FormBuilder Flutter Package and Turn any widget into a Form Input.

Custom Field Example

Here's a simple example of a custom field, built inline:

FormBuilderField<String?>(
name: 'name',
builder: (FormFieldState field) {
return Autocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return const Iterable<String>.empty();
}
return _kOptions.where((String option) {
return option.contains(textEditingValue.text.toLowerCase());
});
},
onSelected: (String selection) {
field.didChange(selection);
},
);
},
autovalidateMode: AutovalidateMode.always,
validator: (valueCandidate) {
if (valueCandidate?.isEmpty ?? true) {
return 'This field is required.';
}
return null;
},
),

FormBuilderField has two required fields: name, and builder. There are many optional fields, including: onSaved, initialValue, autovalidateMode, decoration, enabled, validator, valueTransformer, onChanged, and onReset.

The above example has the required fields plus two fields to implement validation. The field can return either a String or null.

GGC Input Fields

The above section provides a brief introduction to generic Form Builder and Input Fields. Here is how we are building GGC-specific abstractions to address the three design requirements.

Predefined Field Keys

One assumption we can make in GGC is that a given GGC form (i.e. GardenDropdown, CropDropdown, TitleField, etc) appears only once in any given form. That means we can reduce the amount of code required to build a form by predefining field keys. We do this in the FieldKey class, which contains a set of static fields that are initialized to a field key:

/// The FieldKey associated with each GGC Input Field type.
/// This assumes each GGC Input Field type can occur only once in a form.
class FieldKey {
static GlobalKey<FormBuilderFieldState<FormBuilderField<dynamic>, dynamic>>
gardenDropdown = GlobalKey<FormBuilderFieldState>();
static GlobalKey<FormBuilderFieldState<FormBuilderField<dynamic>, dynamic>>
gardenTextField = GlobalKey<FormBuilderFieldState>();
}

This means that when you build a form using the GGC Input Fields, you do not have to create or pass a field key to an input field, as the input field will use one of these values according to the input field type. Then, in the submit callback, you can retrieve the input field value this way:

String gardenID = FieldKey.gardenDropdown.currentState?.value;

GGC Input fields in forms

Using a GGC Input Field in a form is really easy. For example, here is how to add a required dropdown where the user must specify a Garden:

GardenDropdown(gardens: widget.gardens, chapters: widget.chapters, required: true);

As implied above, the value that you can retrieve from this dropdown in the submit callback is the gardenID. From this, you can easily get the garden name (or any other garden details). For example:

String gardenID = FieldKey.gardenDropdown.currentState?.value;
String gardenName = widget.gardens.getGarden(gardenID).name;

GGC Input field outside of forms

We can also use the GardenDropdown Input Field in a non-form context. For example, in the Outcomes screen accessible from the Drawer, there is a Garden dropdown such that the displayed outcomes update immediately each time a new garden is selected.

To do this, the Outcomes screen must provide an onTap function which is called each time the dropdown is manipulated. Here's is how the GardenDropdown can be called to provide this functionality:

GardenDropdown(
gardens: widget.gardens,
chapters: widget.chapters,
gardenID: gardenID,
initialValue: gardenID ?? 'All',
addAll: true,
enabled: widget.gardenID == null,
onTap: (value) => setState(() {
gardenID = (value == 'All') ? null : value;
cropID = null;
varietyID = null;
}),
);

In this situation, we pass in an onTap method that calls setState() to update local state variables for gardenID, cropID, and varietyID. This forces a rebuild of the screen with those new state values, which in turn recomputes the outcomes to be displayed.

This example illustrates how GardenDropdown achieves the three design goals:

  • It is specialized for a given GGC entity. The client just passes in the gardens and chapters collection instances and GardenDropdown does the work of extracting garden names and IDs and building the dropdown object.
  • The invocation of the GardenDropdown has no "look-and-feel" code associated with it. All of the decoration and theme data is internal.
  • The GardenDropdown can be used both within a form (where the data is extracted using a FormKey) or outside a form (where the data is extracted using an onTap callback).

(More documentation to come)

- + \ No newline at end of file diff --git a/docs/develop/release-1.0/design-components/with-widgets.html b/docs/develop/release-1.0/design-components/with-widgets.html index 099c18728..a17544a59 100644 --- a/docs/develop/release-1.0/design-components/with-widgets.html +++ b/docs/develop/release-1.0/design-components/with-widgets.html @@ -5,13 +5,13 @@ "With" widgets | Geo Garden Club - +

"With" widgets

Why "with"?

One important issue to address in a client-server architecture is the asynchronous nature of client-server communication. In other words, when the client needs data from the server, it makes a request that can take time to complete, and may not complete successfully. The client UI should not simply "freeze" during this time, and should "fail gracefully" if the request does not complete successfully.

Making things even more complicated is the desire for modern UIs to be "reactive". This means that if the server's database content changes (for example, one user creates a new garden), then all the other clients currently connected should see their UI automatically refresh with updated information (for example, the number of gardens in the Chapter should increase by one for all other users.)

Making things yet more complicated in GGC is the desire to support a "Storybook" style design system like Monarch, in which individual Views as well as entire Screens can be displayed with sample data values without requiring a database connection.

In GGC, we address all of these issues through a design pattern that starts with a set of widgets which we call the "With" widgets: WithAllData, WithCoreData, WithGardenData, WithMonarchData, WithObservationData, and so on.

In this design pattern, all of the data that a Widget needs to build a user interface component can always be found somewhere within three "top-level" client-side classes: ChapterCollection, GardenCollection, and UserCollection.

In addition, UI widgets come in two basic flavors, "Screens" and "Views". Screens are a kind of "top-level" UI widget which must take responsibility for building the ChapterCollection, GardenCollection, and UserCollection classes. They accomplish this by invoking a "With" widget. Views are always a "child" of a Screen widget, and must be passed ChapterCollection, GardenCollection, and UserCollection instances from their parent Widget. So, database access always happens at the Screen-level, and from then on the Views all receive locally cached data.

To support the use of Monarch, each Screen is implemented by two Widgets: the Widget that calls (for example) WithCoreData in its build method, and then invokes an "Internal" widget with populated instances of ChapterCollection, GardenCollection, and UserCollection. The two-widget structure is necessary in order to support Monarch storybooks, as will be demonstrated later below.

Let's see a simple example of the use of WithCoreData:

class ChaptersScreen extends StatelessWidget {
const ChaptersScreen({
super.key,
});


Widget build(BuildContext context) {
return WithCoreData(whenCoreData: (
{required ChapterCollection chapters,
required GardenCollection gardens,
required UserCollection users}) {
return ChaptersScreenInternal(
chapters: chapters, gardens: gardens, users: users);
});
}
}

In a nutshell, when the ChaptersScreen widget's build method is called, it will call WithCoreData. WithCoreData will retrieve the "core" Chapter, Garden, and User data from Firebase (the first time it is called during a session---after that, the local Riverpod cache of the documents will be used). Note that core data includes documents from a variety of Firebase collections, including chapters, gardens, users, crops, badges.

While this retrieval process is going on, this "With" widget will display the CircularProgressIndicator widget. Once all of the core data is successfully retrieved, then the ChaptersScreenInternal widget's build method will called and passed these fully populated collection class instances, and the intended screen UI will appear. If an error occurs during database retrieval, the "With" widget will display a generic error page.

The net effect is that the UI code is insulated from technicalities resulting from the asynchronous nature of data retrieval. It just wraps the code for the "happy path" inside a "With" widget and proceeds. For example, here's an elided version of the "Internal" widget:

class ChaptersScreenInternal extends StatelessWidget {
const ChaptersScreenInternal({
Key? key,
required this.chapters,
required this.gardens,
required this.users,
}) : super(key: key);
final ChapterCollection chapters;
final GardenCollection gardens;
final UserCollection users;


Widget build(BuildContext context) {
return Scaffold(
drawer: DrawerView(currentUser: users.currentUser),
appBar: AppBar(
title: Text('Chapters (${chapters.size()})'),
actions: [HelpButton(routeName: AppRoute.chapters.name)],
),
body: ListView(children: [...]),
bottomNavigationBar: BottomNavigationBar(...),
);
}
}

What's nice is that ChaptersScreenInternal is a StatelessWidget that gets passed three collections: Chapters, Gardens, and Users, and this is all the data that it (or any of its component Views) needs to render the Screen.

As you look through the code, you will see that Screen widgets generally follow this design pattern: a "top-level" Widget that calls WithCoreData, along with a callback that calls the corresponding "Internal" widget with the three collection classes (and potentially some other data).

WithMonarchData

The decomposition of a Screen into a top-level widget and an internal widget is an important design pattern in ggc_app because it makes it easy to implement Monarch stories. You can do this by writing a story that first calls WithMonarchData, and then calls the "Internal" widget with the collections created by WithMonarchData.

For example:

Widget showChaptersScreen() {
return WithMonarchData(whenMonarchData: (
{required ChapterCollection chapters,
required GardenCollection gardens,
required UserCollection users}) {
return ChaptersScreenInternal(
chapters: chapters, gardens: gardens, users: users);
});
}

The difference between WithCoreData and WithMonarchData is that WithCoreData builds the Chapters, Gardens, and Users collections by accessing Firestore, while WithMonarchData builds the Chapters, Gardens, and Users collections from sample data stored in the assets/monarch directory.

What makes Monarch so useful for UI development is that it makes it really easy to display a UI component in different states. For example, here is an example of displaying the Drawer UI component with data from two different users (one with a profile picture, one who does not):

Widget showDrawer() {
return WithMonarchData(whenMonarchData: (
{required ChapterCollection chapters,
required GardenCollection gardens,
required UserCollection users}) {
return DrawerView(currentUser: users.getUser('jennacorindeane@gmail.com'));
});
}

Widget showDrawer2() {
return WithMonarchData(whenMonarchData: (
{required ChapterCollection chapters,
required GardenCollection gardens,
required UserCollection users}) {
return DrawerView(currentUser: users.getUser('johnson@hawaii.edu'));
});
}

To view these two states using the emulator, you would have to login and logout multiple times.

danger

These "Screen" and "View" design patterns in ggc_app have some important constraints:

  1. Only Screen widgets call a "With" widget. All View widgets should be passed the Chapter, User, and Garden collections from their parents.
  2. Neither Screen nor View widgets call Riverpod providers. All of the Riverpod providers are called within the "With" widgets.

WithGardenData, etc

WithCoreData is responsible for retrieving "core" data, which means the data that is necessary to build the initial set of Screens that the user sees after logging in. We don't want to retrieve all of the data that the user might ever want to see immediately upon logging in, as that might require the UI to pause for several-to-many seconds, degrading the user experience. Instead, upon logging in, only the minimum "core" data is retrieved from the database so that the wait time is minimal.

Now, consider the situation where the user wants to navigate to the Garden Details screen. This screen will require (among other things) all of the Planting data associated with that specific garden. To retrieve additional data beyond the core data, we will provide additional "With" widgets, of which WithGardenData is an example.

Here's an example invocation of WithGardenData:

class GardenDetailsScreen extends StatelessWidget {
const GardenDetailsScreen({Key? key, required this.gardenID})
: super(key: key);

final String gardenID;

Widget build(BuildContext context) {
return WithGardenData(
gardenID: gardenID,
whenGardenData: (
{required ChapterCollection chapters,
required GardenCollection gardens,
required UserCollection users}) {
return GardenDetailsScreenInternal(gardenID: gardenID,
chapters: chapters, gardens: gardens, users: users);
});
}
}

Notice that WithGardenData takes two arguments, a gardenID (used to determine which garden's detailed data to retrieve), plus the standard callback that will be passed filled out instances of ChapterCollection, GardenCollection, and UserCollection. For convenience, the GardenDetailsScreenInternal widget is passed the gardenID as well.

It is important to note that "extended" With widgets like WithGardenData call WithCoreData internally, so the resulting collection instances include all of the core data, plus (in this case) the garden details data. As a result, the client code never needs to nest multiple With widgets.

Due to the wonders of Riverpod, data is cached and reactive. The user can navigate away from this garden and return to it later and the system will build the collections from local copies of the data. Even better, Riverpod will keep its local copies in sync with Firebase, so that if other users add data, the current user will see the updates when they redisplay the page.

- + \ No newline at end of file diff --git a/docs/develop/release-1.0/goals.html b/docs/develop/release-1.0/goals.html index 0acc7d78b..51ef9e5be 100644 --- a/docs/develop/release-1.0/goals.html +++ b/docs/develop/release-1.0/goals.html @@ -5,13 +5,13 @@ Goals | Geo Garden Club - +

Goals

Here are a proposed set of goals for the 1.0 (Beta) release. Some of these goals are motivated by Champion Building: How to successfully adopt a developer tool. Although this blog post focuses on how to get developers in an organization to adopt a new tool, the recommendation seem very applicable to getting gardeners in a community to adopt GGC.

1. Provide a fast, reliable, robust app that satisfies the Core Value Propositions

By the end of the 1.0 release period, we need to have an app that satisfies the Core Value Propositions while being fast, easy to use, and not prone to crashing.

Evaluation:

  • Checklist of CVP functions satisfied.
  • Time to retrieve each page in app using DevTools Performance View
  • Crashlytics data
  • Usability Evaluation of 1.0 user base with respect to app usability.

2. Determine what it means to be a champion/chapter chair

Successful chapters will need one or more champions. We hope to use the 1.0 release period to develop an understanding of what it means to be champion: what their responsibilities are, how to carry out their responsibilities, and how the GGC organization should support and/or compensate them.

Evaluation criteria:

  • User-facing documentation for Chapter Chairs.

3. Determine how to provide high quality documentation

We hope to use the 1.0 release to determine what needs to be documented, and how it should be documented. Some possible documentation sources include:

  • In-app documentation (the ? icon)
  • YouTube (shorts and/or regular videos)
  • User Guide (within geogardenclub.com)
  • GGC Discord Server: channels visible to those in the "User" role.

Evaluation criteria:

  • Usability evaluation of 1.0 user base with respect to app documentation.

4. Gather evidence-based "testimonials"

We hope that the 1.0 release period will result in examples of "successful" use of GGC to solve real world problems for gardeners. Example use cases include:

  • The gardener can plan/manage their garden more easily
  • The gardener makes better choices for their seeds and/or management of plants
  • The garden produces more food
  • The garden/gardener incorporates new best practices due to the app

The more empirical this data, the better. Much of this will require some information about what the garden/gardener was doing before the introduction of GGC.

Evaluation:

  • Analysis of "pre" and "post" questionnaires. The pre questionnaires establish what each gardener was doing prior to GGC: what their technologies were, what their pain points were, etc. The post questionnaires establish their feelings after using GGC: what worked, what didn't work, what they wish would be changed about the app.
  • Documentation of use cases.
  • Evaluation by entreprenurial board members and/or other external stakeholders.

5. Determine how to usefully integrate AI

AI technologies like ChatGPT impact the viability of our business model. We must be able to show users why they should pay for our app instead of using a generic AI. One way to answer this question is to provide access to AI capabilities within the app, and provide concrete use cases of how using AI within the app is better than using AI without the app.

Evaluation:

  • Test results of AI within app vs. AI external to app on basic gardening scenarios.
- + \ No newline at end of file diff --git a/docs/develop/release-1.0/installation.html b/docs/develop/release-1.0/installation.html index f68003a83..867db4c98 100644 --- a/docs/develop/release-1.0/installation.html +++ b/docs/develop/release-1.0/installation.html @@ -5,13 +5,13 @@ Installation | Geo Garden Club - +

Installation

Flutter

Follow the Flutter Installation instructions.

The Flutterpalooza module has some additional documentation.

It is important that you are able to run flutter doctor without error:

% flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[] Flutter (Channel stable, 3.10.0, on macOS 13.3.1 22E772610a darwin-arm64, locale en-US)
[] Android toolchain - develop for Android devices (Android SDK version 33.0.1)
[] Xcode - develop for iOS and macOS (Xcode 14.3)
[] Chrome - develop for the web
[] Android Studio (version 2021.3)
[] IntelliJ IDEA Ultimate Edition (version 2023.1)
[] Connected device (3 available)
[] Network resources

• No issues found!

XCode 14.3 configuration

To my great dismay, ggc_app does not build for the iOS simulator using XCode 14.3 without some additional configuration. The only way I have found to get the ggc_app to run on the iOS simulator is by installing the libarclite library manually into XCode. Here are the steps:

  1. Open the Terminal app and go to the XCode library folder:
cd /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/
  1. Allow the Terminal app to create directories in "protected" areas such Applications/. To do this, go to System Preferences > Security & Privacy > Privacy > Full Disk Access, and add the Terminal app to the list of apps that have full disk access by toggling the radio button next to the Terminal app. You will need to enter your password to make this change. Afterwards, the Terminal app will need to restart for these changes to take effect.

  2. Add the libarclite files to the XCode library folder:

sudo mkdir arc
cd arc
sudo git clone https://github.com/kamyarelyasi/Libarclite-Files.git .
sudo chmod +x *
Additional shenanigans

The above information should be enough for you to proceed. However, I want to document some additional details in case they become relevant in the future.

First, after doing the above, when trying to deploy to the iOS Simulator, I got an XCode error that CFBundleVersion was invalid. I fixed this (and related errors) by editing the ios/Runner/Info.plist file manually, and setting both CFBundleVersion and CFBundleShortVersionString to "1". Since this file is version controlled, and does not appear to be affected by running pod install etc, I don't think you need to worry about it.

Second, this approach to resolving XCode 14.3 problems was found here. What you see is that the above instructions only resolving building, but not "archiving". Additional steps are provided to support archiving. Since I don't know anything about archiving, I didn't do this additional step, but I want to note it while I am thing about it.

Finally, in case we need to look at these changes more closely in the future, the commit is here.

ggc_app

To install the app, first clone the sources from https://github.com/geogardenclub/ggc_app.

Next, cd into the ggc_app directory and run flutter pub get. For example:

% flutter pub get
Running "flutter pub get" in ggc_app...
Resolving dependencies... (1.4s)
_fe_analyzer_shared 58.0.0 (59.0.0 available)
analyzer 5.10.0 (5.11.1 available)
async 2.10.0 (2.11.0 available)
build_daemon 3.1.1 (4.0.0 available)
build_runner 2.3.3 (2.4.1 available)
characters 1.2.1 (1.3.0 available)
collection 1.17.0 (1.17.1 available)
flex_color_scheme 7.0.3 (7.0.4 available)
flutter_form_builder 7.8.0 (8.0.0 available)
flutter_riverpod 2.3.5 (2.3.6 available)
flutter_svg 1.1.6 (2.0.5 available)
go_router 6.5.7 (6.5.8 available)
intl 0.17.0 (0.18.1 available)
js 0.6.5 (0.6.7 available)
matcher 0.12.13 (0.12.15 available)
material_color_utilities 0.2.0 (0.3.0 available)
meta 1.8.0 (1.9.1 available)
monarch 3.0.1 (3.4.0 available)
path 1.8.2 (1.8.3 available)
path_provider_windows 2.1.5 (2.1.6 available)
petitparser 5.1.0 (5.4.0 available)
riverpod 2.3.5 (2.3.6 available)
source_span 1.9.1 (1.10.0 available)
sqflite 2.2.6 (2.2.7 available)
sqflite_common 2.4.3 (2.4.4 available)
synchronized 3.0.1 (3.1.0 available)
test_api 0.4.16 (0.5.2 available)
vm_service 11.3.0 (11.4.0 available)
win32 3.1.4 (4.1.3 available)
xml 6.2.2 (6.3.0 available)
Got dependencies!

Next, to check that the ggc_app actually runs in your environment, the simplest thing to do is to invoke flutter run and select Chrome:

% flutter run
Multiple devices found:
macOS (desktop) • macos • darwin-arm64 • macOS 13.3.1 22E261 darwin-arm64
Chrome (web) • chrome • web-javascript • Google Chrome 112.0.5615.137
[1]: macOS (macos)
[2]: Chrome (chrome)
Please choose one (To quit, press "q/Q"): 2
Launching lib/main.dart on Chrome in debug mode...
Waiting for connection from debug service on Chrome... 16.5s
This app is linked to the debug service: ws://127.0.0.1:58007/FT3-VNs7AGk=/ws
Debug service listening on ws://127.0.0.1:58007/FT3-VNs7AGk=/ws

💪 Running with sound null safety 💪

🔥 To hot restart changes while running, press "r" or "R".
For a more detailed help message, press "h". To quit, press "q".

An Observatory debugger and profiler on Chrome is available at: http://127.0.0.1:58007/FT3-VNs7AGk=
WARNING: found an existing <meta name="viewport"> tag. Flutter Web uses its own viewport configuration for better compatibility with
Flutter. This tag will be replaced.
The Flutter DevTools debugger and profiler on Chrome is available at: http://127.0.0.1:9100?uri=http://127.0.0.1:58007/FT3-VNs7AGk=

If all goes well, you should see a window similar to the following appear:

At this point, you can login as one of the existing users to make sure communication with Firebase is working correctly. Contact Philip for credentials.

Editor

There are three good choices for your Editor: Visual Studio, Android Studio, or IntelliJ IDEA Ultimate (with the Dart and Flutter plugins, which makes it almost equivalent to Android Studio).

With IntelliJ IDEA Ultimate, after bringing up the project, you should see a run toolbar at the top which gives you (on a Mac) the option of opening the iOS simulator:

After opening the simulator, it should appear and you should be able to emulate the system on an iOS device:

It takes a couple of minutes to do all of the XCode shenanigans the first time you run it, but eventually you should see something like the following:

As before, consult with Philip for login credentials.

Monarch

According to their home page, Monarch is a "tool for building Flutter widgets in isolation. It makes it easy to build, test and debug complex UIs." Monarch is basically a Flutter port of React Storybook, which is tremendously popular in React UI development.

I have begun using Monarch and believe it will be very helpful for GGC UI development.

Follow the Monarch installation instructions to install the tool.

Then, invoke monarch run --reload hot-restart (or, for less typing, the ./run_monarch.sh shell script).

You will see the Monarch UI appear, which enables you to view all of the GGC UI elements individually, and (where useful) in different states:

Note that you need to manually select our theme (currently, "Green Theme: Light"). Monarch defaults to the Material Light theme when it is first invoked.

For the design and development of basic UI elements, Monarch appears to be faster, easier, and more efficient than running the iOS simulator. Creating Monarch stories also creates an easy to browse "catalog" of UI elements which are far easier to review than paging through the emulated system to get to the correct state.

- + \ No newline at end of file diff --git a/docs/develop/release-1.0/scripts.html b/docs/develop/release-1.0/scripts.html index fd2572930..bbef0b9d6 100644 --- a/docs/develop/release-1.0/scripts.html +++ b/docs/develop/release-1.0/scripts.html @@ -5,13 +5,13 @@ Scripts | Geo Garden Club - +

Scripts

GGC development is supported by a number of Unix shell scripts. All scripts are named starting with "run" and use snake case to separate words in the script name.

NameAction
run_build_runner.shInvokes the builder running code generation facility, needed for entities using Freezed, JSON Serializable, and Riverpod.
run_flutter_clean.shAfter upgrading Flutter or pub.dev packages, the build might fail either during the pod install or xcode build steps. When this happens, google searches based on the error message often provide bad advice for Flutter developers. Try this script instead.
run_monarch.shA standard way to invoke the Monarch UI display system.
run_pub_add.shIf you are rebuilding the ggc_app with a fresh install of Flutter, this script installs all of the packages in one command. Before running it, make sure the list of packages is up to date!
run_tool_versions.shPrints out the names and versions of important tech stack components. Useful when trying to diagnose why the system builds and runs for one developer but produces errors for another.
- + \ No newline at end of file diff --git a/docs/develop/release-2.0/chapterzipmap.html b/docs/develop/release-2.0/chapterzipmap.html index 4d3c4e8b7..e3f7f090b 100644 --- a/docs/develop/release-2.0/chapterzipmap.html +++ b/docs/develop/release-2.0/chapterzipmap.html @@ -5,13 +5,13 @@ ChapterZipMap | Geo Garden Club - +

ChapterZipMap

In the 2.0 release, users will be able to register with the system from any geographic area and, based on their country and postal code, be automatically placed into a GGC Chapter.

The geographic region associated with a (US) chapter will be defined through a collection called "ChapterZipMap" that maps US zip codes to their corresponding chapter name and chapterID, where the chapter name is constructed from the county and state associated with the zip code. For example, the zip code "96822" maps to the chapter named "Honolulu-HI". We want chapter names to be unique, and so we add the state to the county name because there are many county names that occur in more than one state (i.e. 31 states have a "Washington" county). This collection also defines the chapterID. A portion of the documents in the collection might look like this:

zipcodechapterNamechapterID
"96822""Honolulu-HI""Chapter-023"
"96734""Honolulu-HI""Chapter-023"
"98225""Whatcom-WA""Chapter-013"
"98226""Whatcom-WA""Chapter-013"

The ChapterZipMap collection is pre-constructed and loaded into the database, which means that app is pre-initialized with the names of all chapters possible in the United States, and defines a chapter for every one of its geographic regions. We can also extend this collection to define chapters for other countries in future.

Here is an example excerpt of the JSON file for initializing the ChapterZipMap collection:

[
{
"zipcode": "96822",
"chapterName": "Honolulu-HI",
"chapterID": "Chapter-023"
},
{
"zipcode": "96734",
"chapterName": "Honolulu-HI",
"chapterID": "Chapter-023"
},
{
"zipcode": "98225",
"chapterName": "Whatcom-WA",
"chapterID": "Chapter-013"
},
{
"zipcode": "98226",
"chapterName": "Whatcom-WA",
"chapterID": "Chapter-013"
}
]
- + \ No newline at end of file diff --git a/docs/develop/roadmap.html b/docs/develop/roadmap.html index 77bf88051..eac6ed40d 100644 --- a/docs/develop/roadmap.html +++ b/docs/develop/roadmap.html @@ -5,13 +5,13 @@ Roadmap | Geo Garden Club - +

Roadmap

This roadmap documents our plans for incremental development and release of our technology, with the goal of revenue starting in 2026. While this pace seems glacial, note that we are bootstrapping this technology ourselves without any external investment and with no paid staff. This approach gives us the benefit of being able to delay requiring users to pay for the technology until we have documentation that our technology provides proven benefits.

DatesSubprojectGoal
2021 - 2022Mockup DevelopmentDesign and implement an executable mockup to illustrate design innovations.
Mockup Evaluation (Customers)Evaluate the business concept through interviews with experienced gardeners.
Mockup Evaluation (Entrepreneurs)Evaluate the business concept through interviews with entrepreneurs.
20231.0 (Beta) release developmentBuild a mobile application implementing the Core Value Propositions.
20241.0 (Beta) release evaluationEvaluate the ability of GGC to fulfill its Goals.
2.0 (Public) release developmentBuild 2.0 release incorporating improvements identified through Beta release evaluation.
20252.0 (Public) release evaluationGather evidence for GGC business viability through deployment and evaluation the 2.0 (public) release. During 2025, we will market GGC to Whatcom County gardeners. The 2.0 release will allow gardeners from any geographic region to sign up, but we do not intend to explicitly market the app anywhere outside of Whatcom County. By the end of 2025, the business viability of GGC will be evaluated based on the size of the Whatcom County chapter, and the presence and/or size of any other Chapters.
- + \ No newline at end of file diff --git a/docs/home/innovations.html b/docs/home/innovations.html index d2605924a..cc73ff6bd 100644 --- a/docs/home/innovations.html +++ b/docs/home/innovations.html @@ -5,13 +5,13 @@ Design Innovations | Geo Garden Club - +

Design Innovations

tl;dr

Geo Garden Club includes the following design innovations:

  1. Garden data is aggregrated within local geographic regions called "Chapters".
  2. Access control enables collaborative garden planning and management.
  3. Multi-year garden timelines facilitate experience-based improvement.
  4. Chapter timelines facilitate discovery of local "best practices".
  5. Notifications and observations provide context-specific chapter communication.
  6. Outcome data supports improvement within a single garden and across the chapter.
  7. Support for seed saving and seed sharing.
  8. The public view provides garden owners with controlled, public, read-only access.
Mockup screenshot alert

To illustrate our design innovations, this page uses excerpts from our web-based mockup.

See the Mobile App Sneak Peek page for screenshots of our mobile app, now under development!

1. Garden data is aggregated within local geographic regions called "Chapters".

Each GGC garden is associated with a "Chapter", which collects together a set of gardens that share the same geographic region and (mostly) similar climate. Just as important, the gardeners associated with a Chapter share the same geographic region: they are within walking (or biking) distance of each other.

Chapters are used by GGC to organize and limit the kinds of data sharing. Garden data is only shared within a Chapter. This means that data about plants, outcomes, and timing are all local to your garden's immediate geographical region.

We anticipate that a Chapter can be "viable" with as little as a few dozen members. By viable, we mean that the collective data gathered and shared among Chapter members is sufficient to improve decision making and garden improvement, and that communication among Chapter members succeeds in creating a local "community of practice".

On the other hand, we anticipate that if a Chapter grows beyond a few hundred members, then it might be advantageous to subdivide it into two smaller Chapters.

2. Access control enables collaborative garden planning and management.

Similar to other cloud-based document management systems, GGC enables collaborative access and management of garden data. Gardeners can be have one of three roles: "owner" (with full access to the garden, including the ability to add other gardeners, modify roles, and delete the garden), "editor" (allowing the gardener to add and edit data), and "viewer" (allowing read-access only).

3. Multi-year garden timelines facilitate experience-based improvement.

GGC is oriented to the needs of gardeners who want to improve their gardens over multiple seasons, and thus need to compare and contrast their efforts over multiple years. An important way to represent a garden is via a timeline, which specifies the contents of the garden for each year as well as important dates during the lifecycle of a planting (such as start date, first harvest, and pull date.)

The following image shows a timeline view of a portion of a garden in Bellingham, WA during 2022:

Timeline data can provide many insights, particularly when multiple years of garden data are available. For example, it is useful to rotate the crops planted in a bed each year in order to mitigate certain pests, diseases, and soil nutrient imbalances. The "Bed" timeline view makes it easy to review what plants have been in a particular bed over time:

Notice that GGC color codes each plant variety according to its family (i.e. pink for the Gourd Family, brown for the Legume family, etc). The above timeline illustrates how this gardener rotated crops in Bed 11 over the past three years, ensuring that different plant families were grown in the bed each successive year.

4. Chapter timelines facilitate discovery of local "best practices".

One way to improve garden productivity is by learning best practices in your local geographic region for the timing of planting. GGC Chapter Timelines provide a simple way to view timing data for your own garden, then compare it to timing data across the entire chapter.

In the example image above, we can see that this gardener has planted broccoli only during Week 16 (i.e. between April 15-21) and the latest they left their broccoli was Week 29 (July 22-30). The Chapter Timeline shows that there are gardeners in the Chapter who have planted broccoli as early as Week 7 and left the broccoli in the ground until the end of the year.

This chart alone is not enough information for the gardener to decide what to do, but it is enough information to start a conversation within the Chapter about the timing of broccoli if the gardener wants to change their practices.

5. Notifications and observations provide context-specific chapter communication.

GGC allows gardeners to make "observations" regarding a planting of a plant variety on a specific day.

Observations can include phenomena such as successful germination, first flower, first harvest, diseases, or pests.

Observations can be automatically converted into "Notifications", which are made available to other gardeners in the same chapter growing the same plant variety. For example, this Observation regarding Matina Tomatoes could produce a notification for other gardeners growing Matina Tomatoes in that chapter to inform them that leaf curl has been found to be a problem. This, in turn, could lead to communication between gardeners in this chapter if an effective approach to management of leaf curl for Matina Tomatoes is known.

6. Outcome data supports improvement within a single garden and across the chapter.

An important mechanism for improvement is assessment of outcomes: How well did a single planting do? And what insights can be gained from aggregating outcome data from multiple plantings during a single season, or multiple plantings over multiple seasons, or multiple plantings across the entire chapter?

Outcome data is always created with respect to a single planting. For example, this image shows a summary of a single planting of Rainbow Chard during 2021, including the outcome data that the gardener assigned to it.

Up to five outcome types can be associated with a planting: Appearance, Flavor, Germination, Resistance (to pests and/or disease), and Yield.

Every outcome type is assigned a value based on a five point scale: 1 is the worst, and five is the best. This image provides a visualization of outcome data using stars. In this case, Appearance was assigned 5 (the highest value), and Germination was assigned 2. If the gardener had chosen to not assign a value to one or more of the outcome types, then all the stars would be grey, indicating no outcome data of that type is available.

In order to combine outcome data together and produce meaningful results, it's crucial to define criteria for each numeric rating for each outcome type so that gardeners assign outcomes in a consistent manner. The following table provides the GGC criteria for assigning 1, 2, 3, 4, or 5 for each of the five outcome types.

Once outcome data exists for a set of plantings, then they can be combined to show the spectrum of outcomes associated with a plant variety (or crop) for the current garden or across all gardens in a chapter. GGC provides a visualization of the spectrum of outcome data as a horizontal stacked bar chart, where dark red is 1, light red is 2, grey is 3, light green is 4, and dark green is five. Here is an example for all of the Bean plant varieties:

So, the above chart reveals that bad Bean outcomes are unlikely but have still occurred for Appearance, Flavor, and Yield. Beans show uniformly good Resistance, and pretty good Germination.

Selecting subsets of years makes it possible to see how outcomes are distributed in time and if the distributions of outcomes are different depending upon the year.

7. Support for seed saving and sharing

We believe that an important step toward food resiliency is to develop local networks for seed production and sharing.

To that end, GGC enables gardeners to indicate whether or not they are saving seeds from a particular planting, and if so, whether they have enough seeds that they are willing to share them with the local chapter.

The planting card at left indicates that this gardener has both saved seeds from a specific planting of Lettuce, and they have enough seeds to share some with the Chapter.

Seed saving and sharing has another implication: when growing a plant for seeds, you will sometimes need to leave it in the garden after there is nothing more to harvest. So, in GGC, there is the ability to indicate and "End Harvest" date as well as an "End" (i.e. Pull) date.

We can see this in the planting card above, as well as in the timeline view for that planting of lettuce:

The timeline bar is blue from January to mid-June, indicating that this gardener was actively harvesting lettuce for that entire period. But from mid-June to mid-July, the timeline bar switches to green, indicating that there is no longer any harvest but the plant is still growing (in this case, to produce seed). Reference to the planting card reveals that the harvest ended on 6/15/22 and the lettuce was pulled on 7/20/22.

GGC can thus provide a new insight to gardeners: how long does it take not just to grow a seed to first harvest (which is typically provided on the seed packet) but also how long that plant yields harvest and, significantly, how long is required to yield seeds?

8. The public view provides garden owners with controlled, public, read-only access.

We are currently developing a mobile app that members of Geo Garden Club will use to view and enter garden data and communicate with other members of the Chapter associated with each Garden.

In addition, we will implement access control mechanisms so that owners of a garden can control which other members of GGC can interact with garden data.

The requirement to download and install a mobile app, join GGC, and obtain access from the owner in order to see garden data creates a fairly high barrier to garden data. While this might be necessary and appropriate for active participants in a garden, it also erects a "walled garden". What if a gardener simply wants to ask a question in the Reddit "vegetablegardening" group and needs to provide some details about their garden?

GGC allows the owner of each garden to enable a web-based "public view" of the garden (and its associated chapter). In fact, all of the images on this documentation page were taken from a public view. The public view is designed to allow the gardener to provide details about a garden without revealing its exact location or the identity of gardeners associated with it.

Here is an example of a portion of a public view which is available at https://agilegardenclub.com/public-garden/?name=45ght3cf

Garden owners opt-in to the public view, it is not enabled by default. While this site provides access to a couple of public views for documentation purposes, GGC will not provide a directory of public views, and so it is not likely that a person can find a public view without having been given the URL to it.

- + \ No newline at end of file diff --git a/docs/home/motivation.html b/docs/home/motivation.html index a47fc0109..a8a3a28a7 100644 --- a/docs/home/motivation.html +++ b/docs/home/motivation.html @@ -5,13 +5,13 @@ Motivation | Geo Garden Club - +

Motivation

tl;dr

Food insecurity is an important problem in the U.S. and globally.

Home gardens are an important, underutilized resource for addressing food insecurity.

Geo Garden Club is designing and implementing collaborative technologies to improve the efficiency and effectiveness of home gardeners.

Food security, home gardens, and GGC

Food security, as defined by the United Nations’ Committee on World Food Security, means that all people, at all times, have physical, social, and economic access to sufficient, safe, and nutritious food that meets their food preferences and dietary needs for an active and healthy life. In the coming decades, food security will become an increasingly critical issue due to population growth in combination with climate change, the latter of which which will negatively impact agricultural water availability, arable land availability, and the diversity and distribution of agricultural plant, insect, and animal species (Kwasek, 2012).

Food insecurity is not only an issue for the distant future or for underdeveloped countries. In 2019, an estimated 1 in 8 Americans were food insecure, equating to over 38 million Americans, including almost 12 million children (Coleman-Jensen, 2019).

(Galhenia et al, 2013) provides evidence that home gardens can improve food security: "... Benefits of home gardens include enhancing food and nutritional security in many socio-economic and political situations, improving family health and human capacity, empowering women, promoting social justice and equity, and preserving indigenous knowledge and culture." In addition, "the most fundamental social benefit of home gardens stems from their direct contributions to household food security by increasing availability, accessibility, and utilization of food products". According to (Rai, 2020), home gardens can also strengthen numerous ecosystem serviecs, including plant biodiversity, microclimate, water runoff, urban soil restoration, and water quality. Finally, home gardens can play a significant role in combatting "food deserts", areas in which it is difficult to buy affordable or good-quality fresh food (Palar et al., 2019).

Community gardens are similar to home gardens in scale and the types of food products grown, but community gardens create and foster "communities of practice" with significant health consequences: In a study by (Alaimo et al, 2008), community gardeners consumed fruits and vegetables 5.7 times per day, compared with home gardeners (4.6 times per day) and nongardeners (3.9 times per day). Moreover, 56% of community gardeners met national recommendations to consume fruits and vegetables at least 5 times per day, compared with 37% of home gardeners and 25% of nongardeners.

A fundamental goal of Geo Garden Club (GGC) is to address food insecurity by increasing: (a) the numbers of home gardens (and home gardeners), (b) the productivity of home gardens, and (c) the ability of home gardens to improve human health. To accomplish this, we are designing technology to not just facilitate home garden planning and implementation, but also to facilitate the creation of local "communities of practice" for home gardening. If successful, GGC home gardeners will (among other things) reap the health benefits currently enjoyed by community gardeners.

Our target demographic: the "serious" gardener

We view food production as a spectrum of activities and levels of commitment, as shown in the following diagram:

On the far left side are "recreational" gardeners. These are people who are either just getting into gardening, and/or are relatively uncommitted to gardening. There are a variety of technologies (websites and applications) oriented to the needs of "recreational" gardeners.

On the right side are "farmers": those who make most or all of their living from growing food. Unlike gardeners, farmers cannot operate at a loss. There are also a variety of technologies available to support the needs of small scale farmers (i.e. "urban agriculture") as well as large scale farmers ("industrial agriculture").

We call our target demographic the "serious gardener": a gardener who hopes to grow significant amounts of food, to improve their garden on a season-by-season basis, and who is open to sharing their experiences with other gardeners and learning from other gardener's experiences. A serious gardener is not necessarily an "expert" gardener. In fact, one can be both a serious gardener and an absolute beginner! Serious gardeners are defined by intent, not skill level.

Improving a garden from year to year has multiple facets, including:

  • Better choice of plant varietals to improve yield or pest/environmental resistance
  • Better planning of bed contents (soil/amendments and plant varietals) and sequencing of planting to improve outcomes (yield, flavor, timing of harvest, reduced pests, etc.)
  • Better use of resources (i.e. growing season, bed size, water, nutrients)

There are two basic approaches used by a serious gardener to improve their garden:

  1. Individual experimentation and record keeping. A serious gardener tries to learn from their experience over multiple growing seasons. They may keep informal records to provide a more data-driven approach to improvement.

  2. Collective interaction with a "community of practice". Most serious gardeners develop some sort of informal community of fellow-minded gardeners to whom they discuss issues and share experiences in hopes of improving their collective garden experiences. Traditionally, these communities of practice took the form of garden clubs, such as the Garden Club of America. More recently, communities of practice can take the form of local Facebook groups, or even global forums like the Reddit r/vegetablegardening forum. Interaction with others can also increase the enjoyment of gardening and provides motivation.

Interestingly, this classification scheme reveals a technology gap: there is no technology designed to address the needs of gardeners who have more sophisticated goals than recreational gardeners, but who are not interested in running a business based on growing and selling food. Geo Garden Club is targeting this market and technology niche.

The impact of improved garden knowledge

Improving the ability of gardeners to learn effective gardening practices has been shown to facilitate participation in gardening. A study of the socio-behavioral drivers of growing produce at home (Grebitus, 2021) found that knowledge of gardening practices was a significant factor. "...increased knowledge leads to increased participation in home and community gardens. Hence, we need to educate future gardeners, to increase their knowledge and ability to participate safely in small-scale urban agriculture, as stressed by Kortright and Wakefield, who suggested that home food gardeners could be supported with regard to acquiring ecological gardening skills and to general learning opportunities. Lack of knowledge can increase the risk for those who are unaware of safe gardening practices, for example the risk of soil contaminants."

- + \ No newline at end of file diff --git a/docs/home/related-work.html b/docs/home/related-work.html index a49e2fdaf..3a1e03ed4 100644 --- a/docs/home/related-work.html +++ b/docs/home/related-work.html @@ -5,13 +5,13 @@ Related work | Geo Garden Club - +

Related work

Garden Planning Tools

If you search for "garden planning tools" on the Internet, you'll find dozens of applications. Most of those are essentially "landscape architecture" tools for people who want to design the visual look of their (flower) gardens. This is an interesting design problem, but not the problem addressed by Geo Garden Club.

If you narrow the search to say, "vegetable garden planning tools", you'll still find many that focus on the visual look of the garden bed, but there are a few that focus on the kinds of issues of interest to GGC. Here are the most relevant applications we have found:

Name/URLUsersCost
GPGarden Planner500K+$29-$40/year
TSTerritorial Seed?$29-$40/year
VPVegPlotter?Free
GPPGarden Plan Pro20K+Free version (1 bed), $19.99 one time purchase, $1.99/month subscription
GMGarden Manager20K+Planner: $0. Coach: $6/mo, Coach+Online Library, webinars, members only chat forum: $7.5/mo
GIGrow It!700KOut of business (?)
PMPlants Map?Free plan, or $49-$99/year
SGSmart Gardener?$10/3 months; $30/year
GSGoogle Sheets?Free

Some general observations about garden planning tools:

  • Many of these sites focus primarily the needs of "recreational" or "beginner" gardeners, and/or focus on garden construction.
  • The social media integration for the some of the apps is questionable. Why "like" a picture of a plant?
  • The gardener-to-gardener communication channels are quite primitive, consisting of posting to Facebook or publishing journal entries.
  • Most tools have a very limited free tier, with a typical paid subscriber base at $1-$3/month.
  • Some tools tend to be underwritten by seed vendors, and so the planning tool is oriented toward marketing and seed sales.

As noted before, a popular tool for serious gardeners is a spreadsheet such as Google Sheets or Excel, perhaps in conjunction with a document editor (Google Docs or Word). This combination of tools is free and very flexible, but lacks any domain-specific functionality.

Urban Agriculture Tools

"Urban Agriculture" is a general term for cultivating, processing, and distributing food in or around urban areas. These tools are distinguished from home garden planner tools by a focus on more professional, market-oriented approach to small-scale farming.

Name/URLUsersCost
LFLiteFarm, wiki, github1000sFree, Open Source
COGCOG-Pro?$79-$159/year
VTVeggieTables?$89/year + $19/additional user
ASAgSquared SimpleFarm1000s$10/user/month
FBFarmbrite1000s$15-$30/month
TTend?$30-39/month
FSFarmStatistics?$20/year
ADAgritecture Designer120$30-80/month

Some general observations about urban agriculture tools:

  • These tools all emphasize (and provide support for) commercial, for-profit farming (albeit on a small scale).
  • Several focus on record-keeping required for organic certification.
  • Several focus on people management.
  • None have mechanisms to share data with neighboring farms.

Citizen Science technologies

There are several tools available to support citizen science as it relates to climate change:

Name/URLUsersCost
NNNature's Notebook1000sFree
SFSmartFin?Free

Our goal is for GGC to complement existing approaches to Citizen Science. We would like to work with these organizations to determine the best wa for GGC to collect data to augment current data sets and make them more valuable to researchers.

How does GGC fit in?

Analysis of the technology landscape reveals that there are basically two clusters of features: "Novice" features that are associated with the garden planning tools, and "professional" features that are associated with the urban agriculture tools.

The market niche for GGC is between these two areas:

  • "Beyond Novice". GGC gardeners have generally solved the "layout problem", and are interested in more sophisticated record keeping than is available in current garden planning tools.

  • "Non-professional". GGC gardeners do not require people management technology. In addition, in a professional setting, local data sharing could be undesirable to farmers as it might reveal competitive secrets. GGC gardeners are in a non-competitive environment where data sharing within the community has little downside.

- + \ No newline at end of file diff --git a/docs/home/sneak-peek.html b/docs/home/sneak-peek.html index 47a7db283..7e4bceff4 100644 --- a/docs/home/sneak-peek.html +++ b/docs/home/sneak-peek.html @@ -5,13 +5,13 @@ Mobile App Sneak Peek | Geo Garden Club - +

Mobile App Sneak Peek

We are working on the alpha release of the GeoGardenClub mobile app, with an expected release date of early 2024. While the app is not yet ready for prime time, we thought it would be fun to show you some selected screen shots so you can get an idea of where we're heading.

Login

As with all mobile apps, you will be asked to sign in and/or register when you download the app.

Home (Tasks View)

After logging in, you will come to your home screen. The home screen has a bottom navigation bar providing four views: Tasks, Gardens, Chat, and Observations. Let's look at each of these in turn.

The Home Screen "Tasks" View provides a kind of "To Do" list. Most entries are automatically generated from your garden plan(s), although you can add Tasks manually if you wish.

Tasks in red are "overdue" according to your garden plan. If your garden isn't growing according to your current plan, you can easily correct the task date. Easily maintaining accurate records of important planting events (sowing, first harvest, pull date, etc) is a design goal of GGC.

Home (Garden Summary View)

The Home Screen "Gardens" View provides a summary of all of the gardens that you own or have been given access to by the owner.

You can click the "Details" button to get more information about a specific garden. (More below.)

Home (Chat View)

The Home Screen "Chat" View provides access to a set of system-managed Chat rooms to facilitate communication between gardeners.

Gardeners cannot create their own chat rooms. Instead, the system defines one Chat room for the Chapter, with access granted to all members of the Chapter. In addition, the system creates a chat room for each Garden in the Chapter, and access to each of those chat rooms is granted to the owners and editors of that garden.

Home (Observations View)

The Home Screen "Observations" View provides a kind of "Instagram-ish" scrolling list of photos made by yourself and other gardeners in the Chapter.

Observations allow gardeners to document interesting events in their garden, post questions to other gardeners in the chapter, or simply post beautiful pictures for all to enjoy.

Garden Details (Timeline View)

The Garden Details Screen "Timeline" View provides a perspective on the chronological ordering of plantings in your garden.

You can zoom in to display the garden at 6 month or 1 month views. Each planting can be in one of four phases: in the greenhouse, growing in a bed, available for harvest, or being left to produce seeds. Any of these phases are optional.

Garden Details (Filter View)

The Garden Details Screen "Filter" View provides a perspective on your garden over multiple years.

For example, this screenshot allows the gardener to review what varieties of beans they have planted over the past four years, and the timings associated with each of them.

There is interesting information here, from the range of times when beans were planted, to which years the beans were left to seed!

Garden Details (Outcomes View)

The Garden Details Screen "Outcomes" View provides access to the Outcome data associated with the plantings in your garden.

This can help you experiment and refine the varieties you choose to plant and the way you plant them in order to optimize one or more outcome measures.

Garden Details (Tasks View)

The Garden Details Screen "Tasks" View provides access to the tasks associated with this specific garden. (The Home Page Tasks View shows all of the tasks associated with all of your gardens.)

Chapter Summary Screen

The Chapter Summary Screen provides a summary of the gardens, gardeners, and other information associated with the Chapter.

Every GeoGardenClub user is a member of a Chapter. Chapters are organized based on a small number of adjacent zip codes.

By organizing users into chapters, we believe that it will be easier to share useful information with each other, and enable activities such as seed sharing that support food resilience.

Chapter Gardeners Screen

The Chapter Gardeners Screen provides a kind of "Directory" for the members of the chapter

For privacy purposes, users get to pick a unique "username" when they register, which is the only way they are identified to other gardeners in the Chapter. User can optionally provide a photo (which may or may not be a headshot).

Chapter Gardens Screen

Like the Chapter Gardeners Screen, the Chapter Gardens Screen also provides a kind of "Directory", but this is for the Gardens in the Chapter, not the Gardeners.

there's more to come...

We hope you enjoyed this sneak peek of the GGC app. We want you to know that we plan on providing a variety of other interesting features in the alpha release, such as Badges, Seeds, Chat Rooms, and Themes. Stay tuned!

- + \ No newline at end of file diff --git a/docs/home/team.html b/docs/home/team.html index 0f7e8e7f4..4fcc1646d 100644 --- a/docs/home/team.html +++ b/docs/home/team.html @@ -5,13 +5,13 @@ The Team | Geo Garden Club - +

The Team

Jenna Deane

Jenna Deane has been a "serious" gardener and garden educator for over 15 years, and is currently Zero Waste Program Manager for Sustainable Connections in Bellingham, WA. Previously, she was the Education Program Manager at Common Threads Farm, where she supervised and trained over 20 Americorps volunteers each year to design and implement garden education programs in local schools. Prior to that, she was the Garden Coordinator at Adelante Spanish Immersion School in Redwood City, CA where she managed their school garden program. Jenna received a B.A. in Environmental Studies from Western Washington University.

Philip Johnson

Philip Johnson is a Professor of Information and Computer Sciences at the University of Hawaii. He has over 30 years of experience in software engineering research and education. He has co-founded two startups, participated in multiple business accelerator and incubator programs, and led a variety of sustainability-related research initiatives. Philip received B.S. degrees in Biology and Computer Science from the University of Michigan and a Ph.D. in Computer Science from the University of Massachusetts.

Carleton (Cam) Moore

Cam Moore is a Professor of Information and Computer Sciences at the University of Hawaii. His background includes extensive experience in both academia and industry. His prior industry experience includes software engineering positions at Lockheed Martin and Orincon and co-founder of a software startup. His academic experience includes ACUE certification in College Education. Cam received a B.S. degree in Electrical Engineering and Computer Science from the University of Colorado and a Ph.D. in Computer Science from the University of Hawaii.

Joseph Dane

Joseph Dane is a lawyer at Goodsill Anderson Quinn and Stifel in Honolulu, HI. He has advised business and non-profit organizations on tax matters and on corporate law questions of internal governance and structure. He has also advised local companies on issues involving intellectual property, copyright law, and general commercial contracting. Prior to becoming a lawyer, Joseph was a software engineer for NOAA and co-founded a software startup. Joseph received a B.S. in Physics from the University of Irvine, an M.S. in Computer Science from the University of Hawaii, and a J.D. from the University of Hawaii William Richardson School of Law.

Advisory Board

  • Mercedez Castro is a software engineer in Seattle, WA.

  • Charlie Reppun is a farmer and owner of Waianu Farm in Waiahole, Oahu.

  • Katie Amberg-Johnson is a Scientist at Schrodinger in New York City.

  • Jessie Beck is a home gardener in Bellingham, WA.

- + \ No newline at end of file diff --git a/docs/user-guide/adding-vendors-crops-varieties.html b/docs/user-guide/adding-vendors-crops-varieties.html index f40c7b11d..b7c6b7575 100644 --- a/docs/user-guide/adding-vendors-crops-varieties.html +++ b/docs/user-guide/adding-vendors-crops-varieties.html @@ -5,13 +5,13 @@ Adding Vendors, Crops, and Varieties | Geo Garden Club - +

Adding Vendors, Crops, and Varieties

The varieties in each chapter's database are crowd sourced from the chapter's members. This ensures that the varieties listed are varieties that have been grown in the chapter's area and are varieties that have been grown by the chapter's members. To add a vendor, crop, or variety navigate to the appropriate index screen from the side navigation menu.

Adding a Vendor

To add a vendor, navigate to the Vendors Index Screen from the side navigation menu. Click the "+ Vendor" button in the lower right corner of the screen. Enter the vendor's information and click the Submit button.

Adding a Crop

To add a crop, navigate to the Crops Index Screen from the side navigation menu. Click the "+ Crop" button in the lower right corner of the screen. Enter the crop's information and click the Submit button.

Adding a Variety

To add a variety, navigate to the Varieties Index Screen from the side navigation menu. Click the "+ Variety" button in the lower right corner of the screen. Enter the variety's information and click the Submit button.

- + \ No newline at end of file diff --git a/docs/user-guide/badges.html b/docs/user-guide/badges.html index c245e5b36..6ee63b3ff 100644 --- a/docs/user-guide/badges.html +++ b/docs/user-guide/badges.html @@ -5,13 +5,13 @@ Badges | Geo Garden Club - +

Badges

Why Badges?

Badges are a fun way to track all of your gardening practices and skills. They are also a way to share your level of gardening experience with other users and an incentive for making observations. Badges are displayed on your profile and are visible to other users.

Different types of Badges

Badges are specific to either the garden, gardener, or chapter. Currently, there are three levels for each badge, each requiring increased experience and documentation through observations.

For example, the Compost Champion Badge is a gardener specific badge. It requires that the gardener document experience with composting by tagging observations with the tags #Compost, #CompostTea, #HugelCulture, #VermiCulture, or #Worms.

  • Level 1: The gardener has documented one observation with an associated tag during the current or previous year.
  • Level 2: The gardener has documented one observation with an associated tag during the current and two or three prior years.
  • Level 3: The gardener has documented one observation with an associated tag during the current at least four prior years.

Badge Index

Open the side navigation menu and select Badges to view the Badge Index Screen. The Badge Index Screen displays all badges, requirements, and which gardeners have that badge. Use the Show/Hide Sections to have the information you are interested in automatically displayed for each badge. The bottom menu allows you to toggle between Garden, Gardener, and Chapter badges.

How to earn Badges

Tag your observations with the appropriate tags to earn badges. For example, to earn the Compost Champion Badge, tag your observations with #Compost, #CompostTea, #HugelCulture, #VermiCulture, or #Worms. The app will automatically track your progress and award you the badge when you have met the requirements.

You can add or subtract tags to your observations by Updating the observation after it has been created.

- + \ No newline at end of file diff --git a/docs/user-guide/chat-rooms.html b/docs/user-guide/chat-rooms.html index 0c0466eb2..3295fbd5b 100644 --- a/docs/user-guide/chat-rooms.html +++ b/docs/user-guide/chat-rooms.html @@ -5,13 +5,13 @@ Chat Rooms | Geo Garden Club - +

Chat Rooms

GGC allows gardeners to communicate with each other through chat rooms. Chat rooms are a great way to ask questions, share knowledge, and build community. Chat rooms are organized by chapter or garden.

Where to find chat rooms

Chat rooms are found in the Home section. From the side navigation menu select Home. Chat is located in the bottom navigation menu of the Home Screens. Tap Chat to view chat rooms for your chapter and the gardens you are an owner or editor of.

Chat room etiquette

  • Be respectful and kind.
  • Keep the conversation on topic and mostly gardening related.
  • If you have a question, ask it! If you have an answer, share it!
- + \ No newline at end of file diff --git a/docs/user-guide/downloading.html b/docs/user-guide/downloading.html index 13a35b89b..7b64128d0 100644 --- a/docs/user-guide/downloading.html +++ b/docs/user-guide/downloading.html @@ -5,13 +5,13 @@ Downloading | Geo Garden Club - + - + \ No newline at end of file diff --git a/docs/user-guide/guided-tour.html b/docs/user-guide/guided-tour.html index d2031caa5..078ce3012 100644 --- a/docs/user-guide/guided-tour.html +++ b/docs/user-guide/guided-tour.html @@ -5,13 +5,13 @@ Guided Tour | Geo Garden Club - +

Guided Tour

Geo Garden Club was created to increase the performance of home, community, and school gardens and efficiency of the gardener by answering questions a gardener might have as they plan or work through the gardening season. Here are some of the questions that Geo Garden Club can help answer:

I'm new to the area, what should I plant here?

Navigate to the Crops Index Screen and select the Outcomes check box to display outcomes for each crop. Scroll through the list of crops to see how each crop has been rated and how many times it has been planted in a chapter garden.

After you've chosen a crop to grow, you can see which varieties have been most successfully grown by chapter gardeners. Expand the Varieties section for that crop, or use the Outcomes Variety filter to see which varieties have been grown and how well they've performed.

You can see which gardens have grown this crop and which gardeners have experience growing this crop by expanding those sections.

When should I plant beets?

You can see the timelines and outcomes related to a crop by navigating to the Garden Details Screen and then selecting Filter from the bottom navigation menu. Select Crops and the planting bars for that crop will be displayed for the years you grew that crop. Tap the planting bar to see timing specifics and outcomes, as well as any observations.

Chapter timeline data coming soon!

When I grew beets two years ago, how did that work out?

You can see the outcomes related to a specific planting by navigating to the Garden Details Screen and then selecting Timeline from the bottom navigation menu. From there, scroll to the year you are looking for information for, and tap the relevant planting bar. It will display the timing and outcomes data for that specific planting.

Or, you can see the outcomes related to a specific planting by navigating to the Garden Details Screen and then selecting Filter from the bottom navigation menu. Select Filter By Crop and select Beets to see all of your beet plantings listed by year. Tap the appropriate planting bar to see timing specifics and outcomes, as well as any observations.

I had a great crop of beets last year, how can I replicate that success?

You can see find information about successful plantings by navigating to the Garden Details Screen and then selecting Timeline from the bottom navigation menu. From there, scroll to the year you are looking for information for, and tap the planting bar for beets. It will display the timing and outcomes data for that specific planting.

Or, you can see the outcomes related to a specific planting by navigating to the Garden Details Screen and then selecting Filter from the bottom navigation menu. Select Filter By Crop and select Beets to see all of your beet plantings listed by year. Tap the appropriate planting bar to view timing specifics and outcomes, as well as any observations.

Copy that planting to this year's timeline by selecting copy icon from the top menu. Update fields as necessary. For any dates, update the year to the current year. Once complete, select Submit.

Who in my chapter is really good at growing tomatoes?

To identify who in your chapter is really good at growing tomatoes, navigate to the Badges Index Screen and select Gardener from the bottom navigation menu. Then select Crop Whisperer from the top filter, and choose the crop you are interested in from the new dropdown menu that appears. The app will display the gardeners who have the Crop Whisperer badge for that crop.

How can I replicate the success of another gardener in my chapter?

You can replicate the practices of another gardener by finding them in the Gardener Index Screen Then, select the garden you are interested in learning specifics about. From there, you can select Details to see information about their timing and outcomes for each crop they have grown.

Copy plantings from other gardener's gardens into your timeline coming soon!

I like to rotate my crops. What did I grow in a specific garden bed over the last three years?

You can see the history of what was grown in a garden bed by first navigating to that garden's Garden Details Screen. Then, select Filter from the bottom navigation menu and at select Filter by Bed. Select the bed you are interested in to see the crops that have been grown in that bed for all seasons of data you have entered into the app.

More coming soon!

- + \ No newline at end of file diff --git a/docs/user-guide/observations.html b/docs/user-guide/observations.html index 02a5f31ae..7975fdf13 100644 --- a/docs/user-guide/observations.html +++ b/docs/user-guide/observations.html @@ -5,13 +5,13 @@ Observations | Geo Garden Club - +

Observations

GGC allows gardeners to make "observations" regarding a planting of a plant variety on a specific day. Observations can include phenomena such as successful germination, first flower, first harvest, diseases, or pests, document outcomes like yields, or simply record the progress of a planting.

Viewing all Chapter Observations

From the side navigation menu, tap Home then tap Observations in the bottom navigation menu. A scrollable display of all observations from all chapter gardens will be displayed. The garden, gardener, and tag chips are all tapable.

  • Garden chip: Tap the garden chip to go that garden's Garden Summary Screen.
  • Gardener chip: Tap the gardener chip to go to that gardener's Gardener Summary Screen.
  • Tag chip: Tap the tag chip to see other observations that include that tag.

Viewing Observations for a specific planting

All observations connected to a planting will be displayed in the Planting Details Screen. To view the Planting Details Screen, navigate to the garden in which the planting is located. Tap the planting bar for the planting you are interested in.

The Planting Details Screen will be displayed, first showing info about that planting, followed by a scrollable list of the planting observations.

Adding an Observation

To add an observation, first navigate to the garden in which the observation is happening.

Then click the planting bar for the planting you are adding an observation for. Tap the camera icon to go to the Create Observation Screen.

Fill out the Create Observation Screen. The app will fill in some fields automatically, such as Garden, Bed, Crop, and Variety.

  • Picture: Tap the camera icon to take a picture of the planting or tap the gallery icon to select a picture from your device's gallery.
  • Description: Describe the observation. What is happening? Do you have theories or questions other gardeners might be able to help with?
  • Observation Date: If you are adding an observation for a date other than today, tap the date to select a different date.
  • Tags: Add tags to make your observation easily filterable. Tags are also important if you want that observation to satisfy a requirement of a badge. For more on badges, see the Badges section.
  • Is this a private observation? If you don't want other chapter gardeners to see your observation, select this box.

Tap Submit to add the observation to the planting or Cancel to return to the Planting Details Screen.

Editing an Observation

If you'd like to edit an observation that has already been created, tap the three dots on the upper right of the Observation Card and select Update Observation.

Note that if you need to change the garden, bed, crop, or variety of the observation, you will need to delete the observation and create a new one.

Edit the title, description, tags, observation date, or privacy settings and then hit Submit to save your changes or Cancel to return to the Planting Details Screen.

Deleting an Observation

If you'd like to delete an observation that has already been created, tap the three dots on the upper right of the Observation Card and select Delete Observation.

A confirmation box will appear. Tap Delete to delete the observation or Cancel to return to the Planting Details Screen.

- + \ No newline at end of file diff --git a/docs/user-guide/outcomes.html b/docs/user-guide/outcomes.html index 9fd9faad0..c5c44317c 100644 --- a/docs/user-guide/outcomes.html +++ b/docs/user-guide/outcomes.html @@ -5,14 +5,14 @@ Outcomes | Geo Garden Club - +

Outcomes

Why Outcomes?

Outcomes are a way to track the success of your gardening practices for each planting. They are a way to build a local database of what varieties grow successfully with other users and a requirement for some badges like Crop Whisperer. Outcome categories are Germination, Yield, Flavor, Pest and Disease Resistance, and Appearance.

Plantings can be rated on a five point scale: 1 is the worst, and five is the best. Leaving a score of 0 means that you did not observe that quality. For example, if you had poor germination you may not have a rating for Flavor.

How is Outcome data used?

By recording outcome data for your plantings, you are contributing to a local database of what varieties grow successfully in your area. Gardeners can reflect on the outcomes data for their garden(s) and chapter-wide to make more informed decisions on what varieties to grow and when to grow them. What insights can be gained from aggregating outcome data from multiple plantings during a single season, or multiple plantings over multiple seasons, or multiple plantings across the entire chapter?

Where to find Outcomes

Outcomes are embedded into the Gardens, Crops, Varieties Index Screens. Find these index screens by opening the side navigation menu.

Let's look at outcomes by crop. Tap Crops in the side navigation menu to open the Crops Index Screen. In the Show/Hide Sections box, check Outcomes to have all of the outcomes displayed for each crop, or scroll to the crop and unhide the outcomes section.

You can find Outcomes specific to your gardens by navigating to the Garden Details Screen and selecting Outcomes from the bottom navigation menu.

How to use Outcomes

Let's look at Amaranth. It shows a rating of "good" for germination, one "good" and one rating of "excellent" for yield, and flavor, resistance, and appearance all have "excellent" ratings. A gardener could infer that is a good crop to grow in this area.

How to record Outcomes

Navigate to the Home/Gardens screen by selecting Home in the side navigation menu. You will be taken to your Home/Tasks Screen.

In the bottom navigation menu select Gardens.

Then find the garden in which the planting is located and tap Details. You will be taken to that garden's timeline screen. Select the planting bar to open the Planting Details Screen.

Select the pencil icon in the top right corner to open the Update Planting Screen.

Scroll down to the Outcomes section and update the Outcomes sliders based on the success of the planting. You can update some or all of the outcomes. If you did not observe a quality, leave the slider at 0.

When you are finished, select the Submit button at the bottom of the form to save your changes.

- + \ No newline at end of file diff --git a/docs/user-guide/overview.html b/docs/user-guide/overview.html index dfd4da355..55d040ebe 100644 --- a/docs/user-guide/overview.html +++ b/docs/user-guide/overview.html @@ -5,13 +5,13 @@ Overview | Geo Garden Club - +

Overview

Geo Garden Club is an application that connects you with other gardeners in your community to share garden plans, observations, and seeds. It provides practical garden planning support and easy record keeping so that you can easily learn from your successes and failures. Geo Garden Club was built upon the beliefs that:

Local data is what matters

When you register your Geo Garden Club app you will be placed in a local chapter based on your zip code. You will only see gardening data and observations from those in your chapter.

Discover local best practices by viewing chapter-wide outcome data for crops and varieties. Chapter timelines provide answers to when is best time in your area to grow a crop.

Easily track of your garden's outcomes and stop making the same mistakes twice.

Collaboration is essential

Work collaboratively with other gardeners by granting "edit" access to those who share in the task load.

Notifications and observations encourage communication between chapter members. Garden and chapter based chat rooms organize comversations to easily ask questions to the right group of gardeners.

Gardens are long term

Garden timelines support long term garden planning, winter crops, and biennial crops.

Seed saving and sharing builds resiliency

Seed saving is supported in garden timelines. Gardeners with seeds to share can add them to the chapter database. Gardeners looking for seeds can review available seeds and see the outcomes and planting timeline from their parent plant.

- + \ No newline at end of file diff --git a/docs/user-guide/registration.html b/docs/user-guide/registration.html index 21febbd35..349a2214d 100644 --- a/docs/user-guide/registration.html +++ b/docs/user-guide/registration.html @@ -5,13 +5,13 @@ Registration | Geo Garden Club - +

Registration

After downloading the app, open it to see the login screen. Tap Register to create a new account.

In the Register Screen, enter your email address, password, and password confirmation. Your email address will be your UserID. Tap Register to create your account.

A verification email will be sent to your email address. Please check your email and click the link to verify your account. Once clicked, you will be taken to the Create User Profile Screen.

  • UserID: Enter your email address.
  • Country: Enter the country your garden is located in.
  • Postal Code: Enter your zip code. If outside of the United States, enter your postal code. This will be private and not shared with other users.
  • Picture: Tap the camera icon to take a picture of your garden or tap the gallery icon to select a picture from your device's gallery.
  • Username: Choose a unique username. This will be displayed to other users. For example @GardenLover123.
  • Full Name: Enter your full name. This will be private and not shared with other users.
  • Completed Permaculture Workshop: If you have formal education in permaculture, select this box. Completion of a permaculture workshop is required for those wanting the Permaculture Pro badge. For more on badges, see the Badges section.
  • Is Vendor?: If you represent a company that sells seeds or plant starts, select this box and fill out the next three text fields.
    • Vendor Name: Enter the name of your company.
    • Vendor Short Name: Enter the name you'd like displayed for your company. This may be the same as the Vendor Name.
    • Vendor URL: Enter the website of your company.
  • Tap the Submit button to create your user profile.
- + \ No newline at end of file diff --git a/docs/user-guide/seeds.html b/docs/user-guide/seeds.html index 8642e24f2..0969ffe7d 100644 --- a/docs/user-guide/seeds.html +++ b/docs/user-guide/seeds.html @@ -5,13 +5,13 @@ Seeds | Geo Garden Club - + - + \ No newline at end of file diff --git a/docs/user-guide/tasks.html b/docs/user-guide/tasks.html index 73a2173b9..0ccc381d9 100644 --- a/docs/user-guide/tasks.html +++ b/docs/user-guide/tasks.html @@ -5,13 +5,13 @@ Tasks | Geo Garden Club - +

Tasks

What are Tasks?

Tasks convert your garden plans into action. Tasks help you stay on top of gardening activities while also keeping your gardening timelines up to date. Tasks can be autogenerated from planting timelines or you can create a custom task. Task Screens can be specific to one garden or include tasks from all the gardens you own or have edit privileges for.

Where do I find a list of tasks for all gardens I'm an owner or editor of?

The Task Screen visible after selecting Home from the side navigation menu includes tasks for all the gardens you own or have editing privileges for.

Where do I find a list of tasks for a specific garden?

To only see tasks for a specific garden and to add new tasks, from Home/Gardens, tap Details and then tap Tasks from the bottom navigation bar.

How are Tasks created?

Tasks are created from garden's planting timelines or you can create custom tasks.

Autogenerated Tasks

When you add a planting to your garden, tasks are automatically generated from required data about the planting:

  • Start date
  • Pull date

More tasks can be automatically generated by adding optional information about the planting:

  • Transplant date
  • First harvest date

Custom Tasks

To create a new task for a garden, navigate into that garden's "Details" section

What if I want to reschedule a Task?

You can change the due date of a task from the Tasks Screen or Planting Details.

Update Task from Task Screen

From the Task Screen, tap the three dots at the top right of the Task Card. Select Update Task.

From the Update Task Screen you can change the:

  • Task title
  • Description
  • Due date

Once you've made the desired changes hit Submit. The Task Screen and Planting Timeline will be updated.

You cannot change the garden, crop/variety type or bed from this screen. At this time, the best way to make these changes is to make a new planting in the appropriate garden with the desired bed, crop, and variety.

Update Task from Planting Details

Tasks are also updated if you update the Planting. To update a planting, navigate to the Planting Timeline Screen and tap the plant timeline bar.

From Update Planting, you can change the planting dates, seed supplier, seed availability data, and if a greenhouse was used.

How do I create a custom Task?

To add a custom task, navigate to the garden's "Details" section and tap Tasks from the bottom navigation bar. From the Tasks Screen, tap the +Task button at the bottom right of the screen. Hit Submit to save the task.

How do I delete a Task?

Select the three dots icon at the top right of the Task Card. Select Delete Task from the menu. Hit Delete to delete the task.

- + \ No newline at end of file diff --git a/docs/user-guide/your-first-garden.html b/docs/user-guide/your-first-garden.html index b71704af2..baf3c48b9 100644 --- a/docs/user-guide/your-first-garden.html +++ b/docs/user-guide/your-first-garden.html @@ -5,13 +5,13 @@ Your first garden | Geo Garden Club - +

Your first garden

Find your home screen

To add your first garden navigate to the Home Screen. You can always access the Home Screen by tapping the menu icon at the top left of your screen. Tap on Home to access your gardens, tasks, observations, and chat rooms.

Add a garden

By tapping Home you will be now be in your Tasks Screen.

In the bottom menu bar tap Gardens. You'll see a large +Garden button onn the bottom right of the screen.

Tap +Garden to open the Create Garden Screen.

Fill out the form in the Create Garden Screen

  • Name: Give your garden a name. This name is public to others.
  • Picture (optional) add a picture to be associated with your garden. This could be a garden map or photo.
  • Editors (optional): If there are other people who help complete tasks in your garden you'll want to make them an editor. Add their email and they'll get an invite.
  • Bed Names: Add the names of your garden beds. Beds can represent literal garden beds, pots/containers, a greenhouse, or other locations where you grow food. You can come back and add more beds at any time. Bed names must be:
    • 5 characters long or less
    • Made of letters, numbers, and spaces. No special characters.
    • You don't need to include the word "bed", we've done that for you!
  • Community or school garden? Check this box if this is a community or school garden. There are special badges for gardeners that participate in these group efforts!
  • Pesticide free garden? Check this box if you are committed to not using pesticides in your garden. There are special badges for gardeners who choose to be pesticide free.

Hit Submit and you are ready to start adding plantings.

Add plantings

Now that you have a garden with beds you can add plantings. You can add plantings to your garden by navigating into the Details Screen for that garden.

Select +Planting to open the Create Planting Screen.

Create Planting Screen

Fill out the form in the Create Planting Screen Note that you don't need to fill out every section, only the ones marked required with an *

  • Garden: This is autopopulated for the garden you chose Details for. If you want to add a planting to a different garden, you can navigate back using the arrow on the top left of the screen and choosing a different garden's Details page.
  • Bed: Choose the bed you want to add this planting to. If you don't see the bed you want, you can add it by navigating back one screen to the My Garden Details Screen and selecting the pencil icon at the top of the screen.
  • Crop: Choose the crop you are planting. If you don't see the crop you want, you can find instructions for adding crops in the Add Vendors, Crops, and Varieties section.
  • Variety: Choose the variety you are planting. If you don't see the variety you want, you can find instructions for adding varieties in the Add Vendors, Crops, and Varieties section.
  • Seed Supplier (optional): If you want to track where you got your seeds from, you can add a seed supplier. If you don't see the supplier you want, you can find instructions for adding suppliers in the Add Vendors, Crops, and Varieties section.
  • Start Date: Add the date you expect to plant your seeds. You can update this to the actual date the seeds were planted when you complete that task.
  • Pull Date: Add the date you expect to pull your crop. You can update this to the actual date the crop was pulled when you complete that task.
  • Transplant Date (optional): If you are starting your crop indoors, in a greenhouse, or using store bought starts, you can add the date you transplanted the crop into the garden here. You can add your expected transplant date if you want a transplant task to populate in your Tasks Screen. You can update this to the actual date the crop was transplanted when you complete that task.
  • First Harvest Date (optional): Once you have your first harvest, you can add the date here. You can add your expected date of first harvest if you want a harvest task to populate in your Tasks Screen. You can update this to the actual date the crop was harvested when you complete that task.
  • End Harvest Date (optional): If you have a crop that you expect to save seeds from and has some time between the final harvest and seed collection time, you can add the date you expect to stop harvesting here. You can update this to the actual date the crop finished harvested when you complete that task. Otherwise the Pull Date is assumed to be the End Harvest Date.
  • Used Greenhouse: If the seeds were started in a greenhouse, check this box.
  • Has Seeds: If you saved seeds from this crop, check this box.
  • Are Seeds Available: If seeds are available for others in the chapter, check this box.
  • Outcomes: Complete this section as the crop grows. You can update this section at any time.
    • Germination Rate: Add the percentage of seeds that germinated.
    • Plant Health: Add the health of the plant. You can add a note if you want to add more detail.
    • Pest Pressure: Add the pest pressure on the plant. You can add a note if you want to add more detail.
    • Harvest: Add the amount of harvest you got from the plant. You can add a note if you want to add more detail.
    • Outcomes: Rate the crop on it's qualities to inform future garden planning and add information to the chapter database. You can update this section at any time. See Outcomes for more information.

Hit Submit and you have successfully added your first planting. Continue adding plantings or add more later.

Explore your garden

At the bottom of the Garden Details Screen you have 4 options: Timeline, Filter, Outcomes, and Tasks.

Timeline Screen

The Timeline Screen shows you all the plantings in your garden for a section of time (one year, six months, three months, or one month). You can change the timeframe using the Time Interval drop down menu. Use the left and right arrows to move forward or back in time. You can also add a planting from the Timeline Screen by tapping the +Planting button on the bottom right of the screen.

Filter Screen

The Filter Screen allows you to filter the plantings in your garden by crop, bed, family or variety.

Outcomes Screen

The Outcomes Screen shows you the outcomes of all the plantings in your garden. You can filter the outcomes by crop or variety. Learn more about outcomes in the Outcomes section.

Tasks Screen

The Tasks Screen shows you all the tasks you have to do in your garden. You can add a task from the Tasks Screen by tapping the +Task button on the bottom right of the screen. Learn more about tasks in the Tasks section.

- + \ No newline at end of file diff --git a/index.html b/index.html index 1bba39448..44c37c5da 100644 --- a/index.html +++ b/index.html @@ -5,13 +5,13 @@ Geo Garden Club | Geo Garden Club - +

Growing better gardens, gardeners, and communities, one plant at a time

Unlock insights from your personal experience

Home gardening over multiple seasons yields many useful insights. GGC provides new ways to gather and reflect on your home garden's history in order to improve your future gardening outcomes.

Unlock the collective wisdom of your community

GGC facilitates the creation and management of local "communities of practice" allowing members to more easily share garden outcome data and best practices with each other.

Improve local food production and practices

Home gardens are an important and underutilized resource for increasing community resilience, health, and emotional well-being. GGC provides new ways for home gardens and gardeners to create a "virtual" local community garden.

- + \ No newline at end of file diff --git a/markdown-page.html b/markdown-page.html index 8effa08b5..a5432cbc6 100644 --- a/markdown-page.html +++ b/markdown-page.html @@ -5,13 +5,13 @@ Markdown page example | Geo Garden Club - + - + \ No newline at end of file