diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000000..e69de29bb2 diff --git a/404.html b/404.html new file mode 100644 index 0000000000..47a53f5307 --- /dev/null +++ b/404.html @@ -0,0 +1 @@ + Tangerine Documentation

404 - Not found

\ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 0000000000..d555a2085e --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +docs.tangerinecentral.org \ No newline at end of file diff --git a/CONTRIBUTING/index.html b/CONTRIBUTING/index.html new file mode 100644 index 0000000000..6160e30a5b --- /dev/null +++ b/CONTRIBUTING/index.html @@ -0,0 +1,12 @@ + How to Contribute Documentation - Tangerine Documentation
Skip to content

How to Contribute Documentation

Documentation in Tangerine is managed using the same process as all code contributions. In short, all changes should be completed within a feature-branch or fork of Tangerine and submitted as a pull request to the "next" branch.

Documentation Overview

Tangerine documentation is written using Markdown as the standard source. Documentation is compiled using MkDocs and is available within GitHub Pages. Links are as follows:

Documentation Standards

All documentation must be created and published using Markdown (.md) files and must reside in the docs/ folder or a subdirectory of the docs folder.

Adding your Document to the Navigation

Please follow the instructions on the MkDocs Documentation for adding pages to the navigation. The mkdocs.yml file can be found at the root level of the Tangerine repository.

...
+nav:
+    - Home: index.md
+    - About: about.md
+...
+

Setting up your Environment for Local Documentation Development

Since Tangerine documentation is written in Markdown it's not necessary to have a full local development environment setup to add or modify documentation. That said, if you're making significant changes you may desire to have the ability to build the documentation locally. If you are on Mac OS, you will first need to install python 3. This tutorial worked great for RJ. Make sure to follow the "What to do" section, not the others. Then in the top level tangerine directory, run the following commands to install dependencies. If any of the commands fail, try running the failed command again (that worked for R.J.).

pip install mkdocs
+pip install mkdocs-material
+pip install mkdocs-git-revision-date-localized-plugin
+pip install mkdocs-awesome-pages-plugin
+pip install mkdocs-minify-plugin
+

Now you have everything installed, get started viewing content by running the following in the tangerine root directory (not the tangerine/docs/ directory!)...

mkdocs serve
+

Contribution Guide

TODO: Replace this video with an updated version to reflect the new process

\ No newline at end of file diff --git a/about/index.html b/about/index.html new file mode 100644 index 0000000000..0901484bb8 --- /dev/null +++ b/about/index.html @@ -0,0 +1 @@ + About Tangerine - Tangerine Documentation
Skip to content

About Tangerine

Tangerine is electronic data collection software designed for use on Android mobile devices. Its primary use is to enable offline data capture in low-resource areas.

Tangerine was first developed to capture student responses in in oral early grade reading and mathematics skills assessments, specifically Early Grade Reading Assessment (EGRA) and Early Grade Mathematics Assessment (EGMA). As well as capture interview responses from students, teachers and principals on home and school context information. Tangerine's capabilities have been expanded for data capture and management for rural health intervention projects.

Using Tangerine improves data quality and the efficiency of data collection and analysis by simplifying the preparation and implementation of field work, reducing measurement and data entry errors, and eliminating manual data entry from paper forms.

Tangerine was developed in 2011 by RTI International with its own internal research funds, and made available to the public through a GNU General Public License. RTI redesigned Tangerine and developed a new codebase using latest technologies in 2018 with funding support from Google.org. As an open source software platform Tangerine's source code is available for anyone who wishes to install and use Tangerine on their own web server. Tangerine's source code and related documentation is available on Github, a commonly used repository for open source software. To learn more and have a look under the hood, check out Tangerine's Code Repositories on Github.

How it works

How it works

\ No newline at end of file diff --git a/artwork/icon-source/tangy-checkboxes.png b/artwork/icon-source/tangy-checkboxes.png new file mode 100644 index 0000000000..39be635b7e Binary files /dev/null and b/artwork/icon-source/tangy-checkboxes.png differ diff --git a/artwork/icon-source/tangy-checkboxes.xcf b/artwork/icon-source/tangy-checkboxes.xcf new file mode 100644 index 0000000000..d327974ed0 Binary files /dev/null and b/artwork/icon-source/tangy-checkboxes.xcf differ diff --git a/artwork/icon-source/tangy-gps.png b/artwork/icon-source/tangy-gps.png new file mode 100644 index 0000000000..3132b5c472 Binary files /dev/null and b/artwork/icon-source/tangy-gps.png differ diff --git a/artwork/icon-source/tangy-gps.xcf b/artwork/icon-source/tangy-gps.xcf new file mode 100644 index 0000000000..53bfb2d36e Binary files /dev/null and b/artwork/icon-source/tangy-gps.xcf differ diff --git a/artwork/icon-source/tangy-location.png b/artwork/icon-source/tangy-location.png new file mode 100644 index 0000000000..747b190ac9 Binary files /dev/null and b/artwork/icon-source/tangy-location.png differ diff --git a/artwork/icon-source/tangy-location.xcf b/artwork/icon-source/tangy-location.xcf new file mode 100644 index 0000000000..5aede1cfaf Binary files /dev/null and b/artwork/icon-source/tangy-location.xcf differ diff --git a/artwork/icon-source/tangy-timed.png b/artwork/icon-source/tangy-timed.png new file mode 100644 index 0000000000..66826fc1d4 Binary files /dev/null and b/artwork/icon-source/tangy-timed.png differ diff --git a/artwork/icon-source/tangy-timed.xcf b/artwork/icon-source/tangy-timed.xcf new file mode 100644 index 0000000000..87a6d0cd63 Binary files /dev/null and b/artwork/icon-source/tangy-timed.xcf differ diff --git a/artwork/icons/index.html b/artwork/icons/index.html new file mode 100644 index 0000000000..8b0f97645a --- /dev/null +++ b/artwork/icons/index.html @@ -0,0 +1 @@ + Icons for v3 - Tangerine Documentation
Skip to content

Icons for v3

Gimp source and examples of icons are in the icon-source directory adjacent to this file.

\ No newline at end of file diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 0000000000..1cf13b9f9d Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/javascripts/bundle.1e8ae164.min.js b/assets/javascripts/bundle.1e8ae164.min.js new file mode 100644 index 0000000000..212979889b --- /dev/null +++ b/assets/javascripts/bundle.1e8ae164.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var _i=Object.create;var br=Object.defineProperty;var Ai=Object.getOwnPropertyDescriptor;var Ci=Object.getOwnPropertyNames,Ft=Object.getOwnPropertySymbols,ki=Object.getPrototypeOf,vr=Object.prototype.hasOwnProperty,eo=Object.prototype.propertyIsEnumerable;var Zr=(e,t,r)=>t in e?br(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,F=(e,t)=>{for(var r in t||(t={}))vr.call(t,r)&&Zr(e,r,t[r]);if(Ft)for(var r of Ft(t))eo.call(t,r)&&Zr(e,r,t[r]);return e};var to=(e,t)=>{var r={};for(var o in e)vr.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&Ft)for(var o of Ft(e))t.indexOf(o)<0&&eo.call(e,o)&&(r[o]=e[o]);return r};var gr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Hi=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of Ci(t))!vr.call(e,n)&&n!==r&&br(e,n,{get:()=>t[n],enumerable:!(o=Ai(t,n))||o.enumerable});return e};var jt=(e,t,r)=>(r=e!=null?_i(ki(e)):{},Hi(t||!e||!e.__esModule?br(r,"default",{value:e,enumerable:!0}):r,e));var ro=(e,t,r)=>new Promise((o,n)=>{var i=c=>{try{s(r.next(c))}catch(p){n(p)}},a=c=>{try{s(r.throw(c))}catch(p){n(p)}},s=c=>c.done?o(c.value):Promise.resolve(c.value).then(i,a);s((r=r.apply(e,t)).next())});var no=gr((xr,oo)=>{(function(e,t){typeof xr=="object"&&typeof oo!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(xr,function(){"use strict";function e(r){var o=!0,n=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(C){return!!(C&&C!==document&&C.nodeName!=="HTML"&&C.nodeName!=="BODY"&&"classList"in C&&"contains"in C.classList)}function c(C){var ct=C.type,Ne=C.tagName;return!!(Ne==="INPUT"&&a[ct]&&!C.readOnly||Ne==="TEXTAREA"&&!C.readOnly||C.isContentEditable)}function p(C){C.classList.contains("focus-visible")||(C.classList.add("focus-visible"),C.setAttribute("data-focus-visible-added",""))}function l(C){C.hasAttribute("data-focus-visible-added")&&(C.classList.remove("focus-visible"),C.removeAttribute("data-focus-visible-added"))}function f(C){C.metaKey||C.altKey||C.ctrlKey||(s(r.activeElement)&&p(r.activeElement),o=!0)}function u(C){o=!1}function h(C){s(C.target)&&(o||c(C.target))&&p(C.target)}function w(C){s(C.target)&&(C.target.classList.contains("focus-visible")||C.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),l(C.target))}function A(C){document.visibilityState==="hidden"&&(n&&(o=!0),Z())}function Z(){document.addEventListener("mousemove",J),document.addEventListener("mousedown",J),document.addEventListener("mouseup",J),document.addEventListener("pointermove",J),document.addEventListener("pointerdown",J),document.addEventListener("pointerup",J),document.addEventListener("touchmove",J),document.addEventListener("touchstart",J),document.addEventListener("touchend",J)}function te(){document.removeEventListener("mousemove",J),document.removeEventListener("mousedown",J),document.removeEventListener("mouseup",J),document.removeEventListener("pointermove",J),document.removeEventListener("pointerdown",J),document.removeEventListener("pointerup",J),document.removeEventListener("touchmove",J),document.removeEventListener("touchstart",J),document.removeEventListener("touchend",J)}function J(C){C.target.nodeName&&C.target.nodeName.toLowerCase()==="html"||(o=!1,te())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",A,!0),Z(),r.addEventListener("focus",h,!0),r.addEventListener("blur",w,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var zr=gr((kt,Vr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof kt=="object"&&typeof Vr=="object"?Vr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof kt=="object"?kt.ClipboardJS=r():t.ClipboardJS=r()})(kt,function(){return function(){var e={686:function(o,n,i){"use strict";i.d(n,{default:function(){return Li}});var a=i(279),s=i.n(a),c=i(370),p=i.n(c),l=i(817),f=i.n(l);function u(D){try{return document.execCommand(D)}catch(M){return!1}}var h=function(M){var O=f()(M);return u("cut"),O},w=h;function A(D){var M=document.documentElement.getAttribute("dir")==="rtl",O=document.createElement("textarea");O.style.fontSize="12pt",O.style.border="0",O.style.padding="0",O.style.margin="0",O.style.position="absolute",O.style[M?"right":"left"]="-9999px";var I=window.pageYOffset||document.documentElement.scrollTop;return O.style.top="".concat(I,"px"),O.setAttribute("readonly",""),O.value=D,O}var Z=function(M,O){var I=A(M);O.container.appendChild(I);var W=f()(I);return u("copy"),I.remove(),W},te=function(M){var O=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},I="";return typeof M=="string"?I=Z(M,O):M instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(M==null?void 0:M.type)?I=Z(M.value,O):(I=f()(M),u("copy")),I},J=te;function C(D){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?C=function(O){return typeof O}:C=function(O){return O&&typeof Symbol=="function"&&O.constructor===Symbol&&O!==Symbol.prototype?"symbol":typeof O},C(D)}var ct=function(){var M=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},O=M.action,I=O===void 0?"copy":O,W=M.container,K=M.target,Ce=M.text;if(I!=="copy"&&I!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(K!==void 0)if(K&&C(K)==="object"&&K.nodeType===1){if(I==="copy"&&K.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(I==="cut"&&(K.hasAttribute("readonly")||K.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Ce)return J(Ce,{container:W});if(K)return I==="cut"?w(K):J(K,{container:W})},Ne=ct;function Pe(D){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Pe=function(O){return typeof O}:Pe=function(O){return O&&typeof Symbol=="function"&&O.constructor===Symbol&&O!==Symbol.prototype?"symbol":typeof O},Pe(D)}function xi(D,M){if(!(D instanceof M))throw new TypeError("Cannot call a class as a function")}function Xr(D,M){for(var O=0;O0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof W.action=="function"?W.action:this.defaultAction,this.target=typeof W.target=="function"?W.target:this.defaultTarget,this.text=typeof W.text=="function"?W.text:this.defaultText,this.container=Pe(W.container)==="object"?W.container:document.body}},{key:"listenClick",value:function(W){var K=this;this.listener=p()(W,"click",function(Ce){return K.onClick(Ce)})}},{key:"onClick",value:function(W){var K=W.delegateTarget||W.currentTarget,Ce=this.action(K)||"copy",It=Ne({action:Ce,container:this.container,target:this.target(K),text:this.text(K)});this.emit(It?"success":"error",{action:Ce,text:It,trigger:K,clearSelection:function(){K&&K.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(W){return hr("action",W)}},{key:"defaultTarget",value:function(W){var K=hr("target",W);if(K)return document.querySelector(K)}},{key:"defaultText",value:function(W){return hr("text",W)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(W){var K=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return J(W,K)}},{key:"cut",value:function(W){return w(W)}},{key:"isSupported",value:function(){var W=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],K=typeof W=="string"?[W]:W,Ce=!!document.queryCommandSupported;return K.forEach(function(It){Ce=Ce&&!!document.queryCommandSupported(It)}),Ce}}]),O}(s()),Li=Mi},828:function(o){var n=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,c){for(;s&&s.nodeType!==n;){if(typeof s.matches=="function"&&s.matches(c))return s;s=s.parentNode}}o.exports=a},438:function(o,n,i){var a=i(828);function s(l,f,u,h,w){var A=p.apply(this,arguments);return l.addEventListener(u,A,w),{destroy:function(){l.removeEventListener(u,A,w)}}}function c(l,f,u,h,w){return typeof l.addEventListener=="function"?s.apply(null,arguments):typeof u=="function"?s.bind(null,document).apply(null,arguments):(typeof l=="string"&&(l=document.querySelectorAll(l)),Array.prototype.map.call(l,function(A){return s(A,f,u,h,w)}))}function p(l,f,u,h){return function(w){w.delegateTarget=a(w.target,f),w.delegateTarget&&h.call(l,w)}}o.exports=c},879:function(o,n){n.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},n.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||n.node(i[0]))},n.string=function(i){return typeof i=="string"||i instanceof String},n.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(o,n,i){var a=i(879),s=i(438);function c(u,h,w){if(!u&&!h&&!w)throw new Error("Missing required arguments");if(!a.string(h))throw new TypeError("Second argument must be a String");if(!a.fn(w))throw new TypeError("Third argument must be a Function");if(a.node(u))return p(u,h,w);if(a.nodeList(u))return l(u,h,w);if(a.string(u))return f(u,h,w);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function p(u,h,w){return u.addEventListener(h,w),{destroy:function(){u.removeEventListener(h,w)}}}function l(u,h,w){return Array.prototype.forEach.call(u,function(A){A.addEventListener(h,w)}),{destroy:function(){Array.prototype.forEach.call(u,function(A){A.removeEventListener(h,w)})}}}function f(u,h,w){return s(document.body,u,h,w)}o.exports=c},817:function(o){function n(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),p=document.createRange();p.selectNodeContents(i),c.removeAllRanges(),c.addRange(p),a=c.toString()}return a}o.exports=n},279:function(o){function n(){}n.prototype={on:function(i,a,s){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var c=this;function p(){c.off(i,p),a.apply(s,arguments)}return p._=a,this.on(i,p,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),c=0,p=s.length;for(c;c{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var Va=/["'&<>]/;qn.exports=za;function za(e){var t=""+e,r=Va.exec(t);if(!r)return t;var o,n="",i=0,a=0;for(i=r.index;i0&&i[i.length-1])&&(p[0]===6||p[0]===2)){r=0;continue}if(p[0]===3&&(!i||p[1]>i[0]&&p[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function V(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],a;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(s){a={error:s}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(a)throw a.error}}return i}function z(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o1||s(u,h)})})}function s(u,h){try{c(o[u](h))}catch(w){f(i[0][3],w)}}function c(u){u.value instanceof ot?Promise.resolve(u.value.v).then(p,l):f(i[0][2],u)}function p(u){s("next",u)}function l(u){s("throw",u)}function f(u,h){u(h),i.shift(),i.length&&s(i[0][0],i[0][1])}}function so(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof ue=="function"?ue(e):e[Symbol.iterator](),r={},o("next"),o("throw"),o("return"),r[Symbol.asyncIterator]=function(){return this},r);function o(i){r[i]=e[i]&&function(a){return new Promise(function(s,c){a=e[i](a),n(s,c,a.done,a.value)})}}function n(i,a,s,c){Promise.resolve(c).then(function(p){i({value:p,done:s})},a)}}function k(e){return typeof e=="function"}function pt(e){var t=function(o){Error.call(o),o.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var Wt=pt(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(o,n){return n+1+") "+o.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function Ve(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Ie=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,o,n,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=ue(a),c=s.next();!c.done;c=s.next()){var p=c.value;p.remove(this)}}catch(A){t={error:A}}finally{try{c&&!c.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var l=this.initialTeardown;if(k(l))try{l()}catch(A){i=A instanceof Wt?A.errors:[A]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=ue(f),h=u.next();!h.done;h=u.next()){var w=h.value;try{co(w)}catch(A){i=i!=null?i:[],A instanceof Wt?i=z(z([],V(i)),V(A.errors)):i.push(A)}}}catch(A){o={error:A}}finally{try{h&&!h.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}}if(i)throw new Wt(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)co(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Ve(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Ve(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Er=Ie.EMPTY;function Dt(e){return e instanceof Ie||e&&"closed"in e&&k(e.remove)&&k(e.add)&&k(e.unsubscribe)}function co(e){k(e)?e():e.unsubscribe()}var ke={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var lt={setTimeout:function(e,t){for(var r=[],o=2;o0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var o=this,n=this,i=n.hasError,a=n.isStopped,s=n.observers;return i||a?Er:(this.currentObservers=null,s.push(r),new Ie(function(){o.currentObservers=null,Ve(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,a=o.isStopped;n?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new j;return r.source=this,r},t.create=function(r,o){return new vo(r,o)},t}(j);var vo=function(e){se(t,e);function t(r,o){var n=e.call(this)||this;return n.destination=r,n.source=o,n}return t.prototype.next=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.next)===null||n===void 0||n.call(o,r)},t.prototype.error=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.error)===null||n===void 0||n.call(o,r)},t.prototype.complete=function(){var r,o;(o=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||o===void 0||o.call(r)},t.prototype._subscribe=function(r){var o,n;return(n=(o=this.source)===null||o===void 0?void 0:o.subscribe(r))!==null&&n!==void 0?n:Er},t}(g);var St={now:function(){return(St.delegate||Date).now()},delegate:void 0};var Ot=function(e){se(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=St);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,o),i}return t.prototype.next=function(r){var o=this,n=o.isStopped,i=o._buffer,a=o._infiniteTimeWindow,s=o._timestampProvider,c=o._windowTime;n||(i.push(r),!a&&i.push(s.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var o=this._innerSubscribe(r),n=this,i=n._infiniteTimeWindow,a=n._buffer,s=a.slice(),c=0;c0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=ut.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){var i;if(n===void 0&&(n=0),n!=null?n>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);var a=r.actions;o!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==o&&(ut.cancelAnimationFrame(o),r._scheduled=void 0)},t}(zt);var yo=function(e){se(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o=this._scheduled;this._scheduled=void 0;var n=this.actions,i;r=r||n.shift();do if(i=r.execute(r.state,r.delay))break;while((r=n[0])&&r.id===o&&n.shift());if(this._active=!1,i){for(;(r=n[0])&&r.id===o&&n.shift();)r.unsubscribe();throw i}},t}(qt);var de=new yo(xo);var L=new j(function(e){return e.complete()});function Kt(e){return e&&k(e.schedule)}function _r(e){return e[e.length-1]}function Je(e){return k(_r(e))?e.pop():void 0}function Ae(e){return Kt(_r(e))?e.pop():void 0}function Qt(e,t){return typeof _r(e)=="number"?e.pop():t}var dt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Yt(e){return k(e==null?void 0:e.then)}function Bt(e){return k(e[ft])}function Gt(e){return Symbol.asyncIterator&&k(e==null?void 0:e[Symbol.asyncIterator])}function Jt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Di(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Xt=Di();function Zt(e){return k(e==null?void 0:e[Xt])}function er(e){return ao(this,arguments,function(){var r,o,n,i;return Ut(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,ot(r.read())];case 3:return o=a.sent(),n=o.value,i=o.done,i?[4,ot(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,ot(n)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function tr(e){return k(e==null?void 0:e.getReader)}function N(e){if(e instanceof j)return e;if(e!=null){if(Bt(e))return Ni(e);if(dt(e))return Vi(e);if(Yt(e))return zi(e);if(Gt(e))return Eo(e);if(Zt(e))return qi(e);if(tr(e))return Ki(e)}throw Jt(e)}function Ni(e){return new j(function(t){var r=e[ft]();if(k(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Vi(e){return new j(function(t){for(var r=0;r=2;return function(o){return o.pipe(e?b(function(n,i){return e(n,i,o)}):ce,ye(1),r?Qe(t):jo(function(){return new or}))}}function $r(e){return e<=0?function(){return L}:x(function(t,r){var o=[];t.subscribe(S(r,function(n){o.push(n),e=2,!0))}function le(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new g}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,c=s===void 0?!0:s;return function(p){var l,f,u,h=0,w=!1,A=!1,Z=function(){f==null||f.unsubscribe(),f=void 0},te=function(){Z(),l=u=void 0,w=A=!1},J=function(){var C=l;te(),C==null||C.unsubscribe()};return x(function(C,ct){h++,!A&&!w&&Z();var Ne=u=u!=null?u:r();ct.add(function(){h--,h===0&&!A&&!w&&(f=Pr(J,c))}),Ne.subscribe(ct),!l&&h>0&&(l=new it({next:function(Pe){return Ne.next(Pe)},error:function(Pe){A=!0,Z(),f=Pr(te,n,Pe),Ne.error(Pe)},complete:function(){w=!0,Z(),f=Pr(te,a),Ne.complete()}}),N(C).subscribe(l))})(p)}}function Pr(e,t){for(var r=[],o=2;oe.next(document)),e}function R(e,t=document){return Array.from(t.querySelectorAll(e))}function P(e,t=document){let r=me(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function me(e,t=document){return t.querySelector(e)||void 0}function Re(){var e,t,r,o;return(o=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?o:void 0}var la=T(d(document.body,"focusin"),d(document.body,"focusout")).pipe(be(1),q(void 0),m(()=>Re()||document.body),B(1));function vt(e){return la.pipe(m(t=>e.contains(t)),Y())}function Vo(e,t){return T(d(e,"mouseenter").pipe(m(()=>!0)),d(e,"mouseleave").pipe(m(()=>!1))).pipe(t?be(t):ce,q(!1))}function Ue(e){return{x:e.offsetLeft,y:e.offsetTop}}function zo(e){return T(d(window,"load"),d(window,"resize")).pipe(Me(0,de),m(()=>Ue(e)),q(Ue(e)))}function ir(e){return{x:e.scrollLeft,y:e.scrollTop}}function et(e){return T(d(e,"scroll"),d(window,"resize")).pipe(Me(0,de),m(()=>ir(e)),q(ir(e)))}function qo(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)qo(e,r)}function E(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="undefined"&&(typeof t[n]!="boolean"?o.setAttribute(n,t[n]):o.setAttribute(n,""));for(let n of r)qo(o,n);return o}function ar(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function gt(e){let t=E("script",{src:e});return H(()=>(document.head.appendChild(t),T(d(t,"load"),d(t,"error").pipe(v(()=>Ar(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(m(()=>{}),_(()=>document.head.removeChild(t)),ye(1))))}var Ko=new g,ma=H(()=>typeof ResizeObserver=="undefined"?gt("https://unpkg.com/resize-observer-polyfill"):$(void 0)).pipe(m(()=>new ResizeObserver(e=>{for(let t of e)Ko.next(t)})),v(e=>T(qe,$(e)).pipe(_(()=>e.disconnect()))),B(1));function pe(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Ee(e){return ma.pipe(y(t=>t.observe(e)),v(t=>Ko.pipe(b(({target:r})=>r===e),_(()=>t.unobserve(e)),m(()=>pe(e)))),q(pe(e)))}function xt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function sr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}var Qo=new g,fa=H(()=>$(new IntersectionObserver(e=>{for(let t of e)Qo.next(t)},{threshold:0}))).pipe(v(e=>T(qe,$(e)).pipe(_(()=>e.disconnect()))),B(1));function yt(e){return fa.pipe(y(t=>t.observe(e)),v(t=>Qo.pipe(b(({target:r})=>r===e),_(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function Yo(e,t=16){return et(e).pipe(m(({y:r})=>{let o=pe(e),n=xt(e);return r>=n.height-o.height-t}),Y())}var cr={drawer:P("[data-md-toggle=drawer]"),search:P("[data-md-toggle=search]")};function Bo(e){return cr[e].checked}function Be(e,t){cr[e].checked!==t&&cr[e].click()}function We(e){let t=cr[e];return d(t,"change").pipe(m(()=>t.checked),q(t.checked))}function ua(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function da(){return T(d(window,"compositionstart").pipe(m(()=>!0)),d(window,"compositionend").pipe(m(()=>!1))).pipe(q(!1))}function Go(){let e=d(window,"keydown").pipe(b(t=>!(t.metaKey||t.ctrlKey)),m(t=>({mode:Bo("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),b(({mode:t,type:r})=>{if(t==="global"){let o=Re();if(typeof o!="undefined")return!ua(o,r)}return!0}),le());return da().pipe(v(t=>t?L:e))}function ve(){return new URL(location.href)}function st(e,t=!1){if(G("navigation.instant")&&!t){let r=E("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function Jo(){return new g}function Xo(){return location.hash.slice(1)}function Zo(e){let t=E("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function ha(e){return T(d(window,"hashchange"),e).pipe(m(Xo),q(Xo()),b(t=>t.length>0),B(1))}function en(e){return ha(e).pipe(m(t=>me(`[id="${t}"]`)),b(t=>typeof t!="undefined"))}function At(e){let t=matchMedia(e);return nr(r=>t.addListener(()=>r(t.matches))).pipe(q(t.matches))}function tn(){let e=matchMedia("print");return T(d(window,"beforeprint").pipe(m(()=>!0)),d(window,"afterprint").pipe(m(()=>!1))).pipe(q(e.matches))}function Ur(e,t){return e.pipe(v(r=>r?t():L))}function Wr(e,t){return new j(r=>{let o=new XMLHttpRequest;return o.open("GET",`${e}`),o.responseType="blob",o.addEventListener("load",()=>{o.status>=200&&o.status<300?(r.next(o.response),r.complete()):r.error(new Error(o.statusText))}),o.addEventListener("error",()=>{r.error(new Error("Network error"))}),o.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(o.addEventListener("progress",n=>{var i;if(n.lengthComputable)t.progress$.next(n.loaded/n.total*100);else{let a=(i=o.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(n.loaded/+a*100)}}),t.progress$.next(5)),o.send(),()=>o.abort()})}function De(e,t){return Wr(e,t).pipe(v(r=>r.text()),m(r=>JSON.parse(r)),B(1))}function rn(e,t){let r=new DOMParser;return Wr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/html")),B(1))}function on(e,t){let r=new DOMParser;return Wr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/xml")),B(1))}function nn(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function an(){return T(d(window,"scroll",{passive:!0}),d(window,"resize",{passive:!0})).pipe(m(nn),q(nn()))}function sn(){return{width:innerWidth,height:innerHeight}}function cn(){return d(window,"resize",{passive:!0}).pipe(m(sn),q(sn()))}function pn(){return Q([an(),cn()]).pipe(m(([e,t])=>({offset:e,size:t})),B(1))}function pr(e,{viewport$:t,header$:r}){let o=t.pipe(X("size")),n=Q([o,r]).pipe(m(()=>Ue(e)));return Q([r,t,n]).pipe(m(([{height:i},{offset:a,size:s},{x:c,y:p}])=>({offset:{x:a.x-c,y:a.y-p+i},size:s})))}function ba(e){return d(e,"message",t=>t.data)}function va(e){let t=new g;return t.subscribe(r=>e.postMessage(r)),t}function ln(e,t=new Worker(e)){let r=ba(t),o=va(t),n=new g;n.subscribe(o);let i=o.pipe(ee(),oe(!0));return n.pipe(ee(),$e(r.pipe(U(i))),le())}var ga=P("#__config"),Et=JSON.parse(ga.textContent);Et.base=`${new URL(Et.base,ve())}`;function we(){return Et}function G(e){return Et.features.includes(e)}function ge(e,t){return typeof t!="undefined"?Et.translations[e].replace("#",t.toString()):Et.translations[e]}function Te(e,t=document){return P(`[data-md-component=${e}]`,t)}function ie(e,t=document){return R(`[data-md-component=${e}]`,t)}function xa(e){let t=P(".md-typeset > :first-child",e);return d(t,"click",{once:!0}).pipe(m(()=>P(".md-typeset",e)),m(r=>({hash:__md_hash(r.innerHTML)})))}function mn(e){if(!G("announce.dismiss")||!e.childElementCount)return L;if(!e.hidden){let t=P(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return H(()=>{let t=new g;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),xa(e).pipe(y(r=>t.next(r)),_(()=>t.complete()),m(r=>F({ref:e},r)))})}function ya(e,{target$:t}){return t.pipe(m(r=>({hidden:r!==e})))}function fn(e,t){let r=new g;return r.subscribe(({hidden:o})=>{e.hidden=o}),ya(e,t).pipe(y(o=>r.next(o)),_(()=>r.complete()),m(o=>F({ref:e},o)))}function Ct(e,t){return t==="inline"?E("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},E("div",{class:"md-tooltip__inner md-typeset"})):E("div",{class:"md-tooltip",id:e,role:"tooltip"},E("div",{class:"md-tooltip__inner md-typeset"}))}function un(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return E("aside",{class:"md-annotation",tabIndex:0},Ct(t),E("a",{href:r,class:"md-annotation__index",tabIndex:-1},E("span",{"data-md-annotation-id":e})))}else return E("aside",{class:"md-annotation",tabIndex:0},Ct(t),E("span",{class:"md-annotation__index",tabIndex:-1},E("span",{"data-md-annotation-id":e})))}function dn(e){return E("button",{class:"md-clipboard md-icon",title:ge("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}function Dr(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(c=>!e.terms[c]).reduce((c,p)=>[...c,E("del",null,p)," "],[]).slice(0,-1),i=we(),a=new URL(e.location,i.base);G("search.highlight")&&a.searchParams.set("h",Object.entries(e.terms).filter(([,c])=>c).reduce((c,[p])=>`${c} ${p}`.trim(),""));let{tags:s}=we();return E("a",{href:`${a}`,class:"md-search-result__link",tabIndex:-1},E("article",{class:"md-search-result__article md-typeset","data-md-score":e.score.toFixed(2)},r>0&&E("div",{class:"md-search-result__icon md-icon"}),r>0&&E("h1",null,e.title),r<=0&&E("h2",null,e.title),o>0&&e.text.length>0&&e.text,e.tags&&e.tags.map(c=>{let p=s?c in s?`md-tag-icon md-tag--${s[c]}`:"md-tag-icon":"";return E("span",{class:`md-tag ${p}`},c)}),o>0&&n.length>0&&E("p",{class:"md-search-result__terms"},ge("search.result.term.missing"),": ",...n)))}function hn(e){let t=e[0].score,r=[...e],o=we(),n=r.findIndex(l=>!`${new URL(l.location,o.base)}`.includes("#")),[i]=r.splice(n,1),a=r.findIndex(l=>l.scoreDr(l,1)),...c.length?[E("details",{class:"md-search-result__more"},E("summary",{tabIndex:-1},E("div",null,c.length>0&&c.length===1?ge("search.result.more.one"):ge("search.result.more.other",c.length))),...c.map(l=>Dr(l,1)))]:[]];return E("li",{class:"md-search-result__item"},p)}function bn(e){return E("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>E("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?ar(r):r)))}function Nr(e){let t=`tabbed-control tabbed-control--${e}`;return E("div",{class:t,hidden:!0},E("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function vn(e){return E("div",{class:"md-typeset__scrollwrap"},E("div",{class:"md-typeset__table"},e))}function Ea(e){let t=we(),r=new URL(`../${e.version}/`,t.base);return E("li",{class:"md-version__item"},E("a",{href:`${r}`,class:"md-version__link"},e.title))}function gn(e,t){return e=e.filter(r=>{var o;return!((o=r.properties)!=null&&o.hidden)}),E("div",{class:"md-version"},E("button",{class:"md-version__current","aria-label":ge("select.version")},t.title),E("ul",{class:"md-version__list"},e.map(Ea)))}var wa=0;function Ta(e,t){document.body.append(e);let{width:r}=pe(e);e.style.setProperty("--md-tooltip-width",`${r}px`),e.remove();let o=sr(t),n=typeof o!="undefined"?et(o):$({x:0,y:0}),i=T(vt(t),Vo(t)).pipe(Y());return Q([i,n]).pipe(m(([a,s])=>{let{x:c,y:p}=Ue(t),l=pe(t),f=t.closest("table");return f&&t.parentElement&&(c+=f.offsetLeft+t.parentElement.offsetLeft,p+=f.offsetTop+t.parentElement.offsetTop),{active:a,offset:{x:c-s.x+l.width/2-r/2,y:p-s.y+l.height+8}}}))}function Ge(e){let t=e.title;if(!t.length)return L;let r=`__tooltip_${wa++}`,o=Ct(r,"inline"),n=P(".md-typeset",o);return n.innerHTML=t,H(()=>{let i=new g;return i.subscribe({next({offset:a}){o.style.setProperty("--md-tooltip-x",`${a.x}px`),o.style.setProperty("--md-tooltip-y",`${a.y}px`)},complete(){o.style.removeProperty("--md-tooltip-x"),o.style.removeProperty("--md-tooltip-y")}}),T(i.pipe(b(({active:a})=>a)),i.pipe(be(250),b(({active:a})=>!a))).subscribe({next({active:a}){a?(e.insertAdjacentElement("afterend",o),e.setAttribute("aria-describedby",r),e.removeAttribute("title")):(o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t))},complete(){o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t)}}),i.pipe(Me(16,de)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(_t(125,de),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:a})=>a)).subscribe({next(a){a?o.style.setProperty("--md-tooltip-0",`${-a}px`):o.style.removeProperty("--md-tooltip-0")},complete(){o.style.removeProperty("--md-tooltip-0")}}),Ta(o,e).pipe(y(a=>i.next(a)),_(()=>i.complete()),m(a=>F({ref:e},a)))}).pipe(ze(ae))}function Sa(e,t){let r=H(()=>Q([zo(e),et(t)])).pipe(m(([{x:o,y:n},i])=>{let{width:a,height:s}=pe(e);return{x:o-i.x+a/2,y:n-i.y+s/2}}));return vt(e).pipe(v(o=>r.pipe(m(n=>({active:o,offset:n})),ye(+!o||1/0))))}function xn(e,t,{target$:r}){let[o,n]=Array.from(e.children);return H(()=>{let i=new g,a=i.pipe(ee(),oe(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),yt(e).pipe(U(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),T(i.pipe(b(({active:s})=>s)),i.pipe(be(250),b(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(o):o.remove()},complete(){e.prepend(o)}}),i.pipe(Me(16,de)).subscribe(({active:s})=>{o.classList.toggle("md-tooltip--active",s)}),i.pipe(_t(125,de),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),d(n,"click").pipe(U(a),b(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),d(n,"mousedown").pipe(U(a),ne(i)).subscribe(([s,{active:c}])=>{var p;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(c){s.preventDefault();let l=e.parentElement.closest(".md-annotation");l instanceof HTMLElement?l.focus():(p=Re())==null||p.blur()}}),r.pipe(U(a),b(s=>s===o),Ye(125)).subscribe(()=>e.focus()),Sa(e,t).pipe(y(s=>i.next(s)),_(()=>i.complete()),m(s=>F({ref:e},s)))})}function Oa(e){return e.tagName==="CODE"?R(".c, .c1, .cm",e):[e]}function Ma(e){let t=[];for(let r of Oa(e)){let o=[],n=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=n.nextNode();i;i=n.nextNode())o.push(i);for(let i of o){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,c]=a;if(typeof c=="undefined"){let p=i.splitText(a.index);i=p.splitText(s.length),t.push(p)}else{i.textContent=s,t.push(i);break}}}}return t}function yn(e,t){t.append(...Array.from(e.childNodes))}function lr(e,t,{target$:r,print$:o}){let n=t.closest("[id]"),i=n==null?void 0:n.id,a=new Map;for(let s of Ma(t)){let[,c]=s.textContent.match(/\((\d+)\)/);me(`:scope > li:nth-child(${c})`,e)&&(a.set(c,un(c,i)),s.replaceWith(a.get(c)))}return a.size===0?L:H(()=>{let s=new g,c=s.pipe(ee(),oe(!0)),p=[];for(let[l,f]of a)p.push([P(".md-typeset",f),P(`:scope > li:nth-child(${l})`,e)]);return o.pipe(U(c)).subscribe(l=>{e.hidden=!l,e.classList.toggle("md-annotation-list",l);for(let[f,u]of p)l?yn(f,u):yn(u,f)}),T(...[...a].map(([,l])=>xn(l,t,{target$:r}))).pipe(_(()=>s.complete()),le())})}function En(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return En(t)}}function wn(e,t){return H(()=>{let r=En(e);return typeof r!="undefined"?lr(r,e,t):L})}var Tn=jt(zr());var La=0;function Sn(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Sn(t)}}function _a(e){return Ee(e).pipe(m(({width:t})=>({scrollable:xt(e).width>t})),X("scrollable"))}function On(e,t){let{matches:r}=matchMedia("(hover)"),o=H(()=>{let n=new g,i=n.pipe($r(1));n.subscribe(({scrollable:c})=>{c&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[];if(Tn.default.isSupported()&&(e.closest(".copy")||G("content.code.copy")&&!e.closest(".no-copy"))){let c=e.closest("pre");c.id=`__code_${La++}`;let p=dn(c.id);c.insertBefore(p,e),G("content.tooltips")&&a.push(Ge(p))}let s=e.closest(".highlight");if(s instanceof HTMLElement){let c=Sn(s);if(typeof c!="undefined"&&(s.classList.contains("annotate")||G("content.code.annotate"))){let p=lr(c,e,t);a.push(Ee(s).pipe(U(i),m(({width:l,height:f})=>l&&f),Y(),v(l=>l?p:L)))}}return _a(e).pipe(y(c=>n.next(c)),_(()=>n.complete()),m(c=>F({ref:e},c)),$e(...a))});return G("content.lazy")?yt(e).pipe(b(n=>n),ye(1),v(()=>o)):o}function Aa(e,{target$:t,print$:r}){let o=!0;return T(t.pipe(m(n=>n.closest("details:not([open])")),b(n=>e===n),m(()=>({action:"open",reveal:!0}))),r.pipe(b(n=>n||!o),y(()=>o=e.open),m(n=>({action:n?"open":"close"}))))}function Mn(e,t){return H(()=>{let r=new g;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),Aa(e,t).pipe(y(o=>r.next(o)),_(()=>r.complete()),m(o=>F({ref:e},o)))})}var Ln=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel rect,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel rect{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color);stroke-width:.05rem}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs #classDiagram-compositionEnd,defs #classDiagram-compositionStart,defs #classDiagram-dependencyEnd,defs #classDiagram-dependencyStart,defs #classDiagram-extensionEnd,defs #classDiagram-extensionStart{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs #classDiagram-aggregationEnd,defs #classDiagram-aggregationStart{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}.attributeBoxEven,.attributeBoxOdd{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityBox{fill:var(--md-mermaid-label-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityLabel{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.relationshipLabelBox{fill:var(--md-mermaid-label-bg-color);fill-opacity:1;background-color:var(--md-mermaid-label-bg-color);opacity:1}.relationshipLabel{fill:var(--md-mermaid-label-fg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs #ONE_OR_MORE_END *,defs #ONE_OR_MORE_START *,defs #ONLY_ONE_END *,defs #ONLY_ONE_START *,defs #ZERO_OR_MORE_END *,defs #ZERO_OR_MORE_START *,defs #ZERO_OR_ONE_END *,defs #ZERO_OR_ONE_START *{stroke:var(--md-mermaid-edge-color)!important}defs #ZERO_OR_MORE_END circle,defs #ZERO_OR_MORE_START circle{fill:var(--md-mermaid-label-bg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var qr,ka=0;function Ha(){return typeof mermaid=="undefined"||mermaid instanceof Element?gt("https://unpkg.com/mermaid@10.7.0/dist/mermaid.min.js"):$(void 0)}function _n(e){return e.classList.remove("mermaid"),qr||(qr=Ha().pipe(y(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Ln,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),m(()=>{}),B(1))),qr.subscribe(()=>ro(this,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${ka++}`,r=E("div",{class:"mermaid"}),o=e.textContent,{svg:n,fn:i}=yield mermaid.render(t,o),a=r.attachShadow({mode:"closed"});a.innerHTML=n,e.replaceWith(r),i==null||i(a)})),qr.pipe(m(()=>({ref:e})))}var An=E("table");function Cn(e){return e.replaceWith(An),An.replaceWith(vn(e)),$({ref:e})}function $a(e){let t=e.find(r=>r.checked)||e[0];return T(...e.map(r=>d(r,"change").pipe(m(()=>P(`label[for="${r.id}"]`))))).pipe(q(P(`label[for="${t.id}"]`)),m(r=>({active:r})))}function kn(e,{viewport$:t,target$:r}){let o=P(".tabbed-labels",e),n=R(":scope > input",e),i=Nr("prev");e.append(i);let a=Nr("next");return e.append(a),H(()=>{let s=new g,c=s.pipe(ee(),oe(!0));Q([s,Ee(e)]).pipe(U(c),Me(1,de)).subscribe({next([{active:p},l]){let f=Ue(p),{width:u}=pe(p);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let h=ir(o);(f.xh.x+l.width)&&o.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),Q([et(o),Ee(o)]).pipe(U(c)).subscribe(([p,l])=>{let f=xt(o);i.hidden=p.x<16,a.hidden=p.x>f.width-l.width-16}),T(d(i,"click").pipe(m(()=>-1)),d(a,"click").pipe(m(()=>1))).pipe(U(c)).subscribe(p=>{let{width:l}=pe(o);o.scrollBy({left:l*p,behavior:"smooth"})}),r.pipe(U(c),b(p=>n.includes(p))).subscribe(p=>p.click()),o.classList.add("tabbed-labels--linked");for(let p of n){let l=P(`label[for="${p.id}"]`);l.replaceChildren(E("a",{href:`#${l.htmlFor}`,tabIndex:-1},...Array.from(l.childNodes))),d(l.firstElementChild,"click").pipe(U(c),b(f=>!(f.metaKey||f.ctrlKey)),y(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${l.htmlFor}`),l.click()})}return G("content.tabs.link")&&s.pipe(Le(1),ne(t)).subscribe(([{active:p},{offset:l}])=>{let f=p.innerText.trim();if(p.hasAttribute("data-md-switching"))p.removeAttribute("data-md-switching");else{let u=e.offsetTop-l.y;for(let w of R("[data-tabs]"))for(let A of R(":scope > input",w)){let Z=P(`label[for="${A.id}"]`);if(Z!==p&&Z.innerText.trim()===f){Z.setAttribute("data-md-switching",""),A.click();break}}window.scrollTo({top:e.offsetTop-u});let h=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...h])])}}),s.pipe(U(c)).subscribe(()=>{for(let p of R("audio, video",e))p.pause()}),$a(n).pipe(y(p=>s.next(p)),_(()=>s.complete()),m(p=>F({ref:e},p)))}).pipe(ze(ae))}function Hn(e,{viewport$:t,target$:r,print$:o}){return T(...R(".annotate:not(.highlight)",e).map(n=>wn(n,{target$:r,print$:o})),...R("pre:not(.mermaid) > code",e).map(n=>On(n,{target$:r,print$:o})),...R("pre.mermaid",e).map(n=>_n(n)),...R("table:not([class])",e).map(n=>Cn(n)),...R("details",e).map(n=>Mn(n,{target$:r,print$:o})),...R("[data-tabs]",e).map(n=>kn(n,{viewport$:t,target$:r})),...R("[title]",e).filter(()=>G("content.tooltips")).map(n=>Ge(n)))}function Ra(e,{alert$:t}){return t.pipe(v(r=>T($(!0),$(!1).pipe(Ye(2e3))).pipe(m(o=>({message:r,active:o})))))}function $n(e,t){let r=P(".md-typeset",e);return H(()=>{let o=new g;return o.subscribe(({message:n,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=n}),Ra(e,t).pipe(y(n=>o.next(n)),_(()=>o.complete()),m(n=>F({ref:e},n)))})}function Pa({viewport$:e}){if(!G("header.autohide"))return $(!1);let t=e.pipe(m(({offset:{y:n}})=>n),Ke(2,1),m(([n,i])=>[nMath.abs(i-n.y)>100),m(([,[n]])=>n),Y()),o=We("search");return Q([e,o]).pipe(m(([{offset:n},i])=>n.y>400&&!i),Y(),v(n=>n?r:$(!1)),q(!1))}function Rn(e,t){return H(()=>Q([Ee(e),Pa(t)])).pipe(m(([{height:r},o])=>({height:r,hidden:o})),Y((r,o)=>r.height===o.height&&r.hidden===o.hidden),B(1))}function Pn(e,{header$:t,main$:r}){return H(()=>{let o=new g,n=o.pipe(ee(),oe(!0));o.pipe(X("active"),je(t)).subscribe(([{active:a},{hidden:s}])=>{e.classList.toggle("md-header--shadow",a&&!s),e.hidden=s});let i=fe(R("[title]",e)).pipe(b(()=>G("content.tooltips")),re(a=>Ge(a)));return r.subscribe(o),t.pipe(U(n),m(a=>F({ref:e},a)),$e(i.pipe(U(n))))})}function Ia(e,{viewport$:t,header$:r}){return pr(e,{viewport$:t,header$:r}).pipe(m(({offset:{y:o}})=>{let{height:n}=pe(e);return{active:o>=n}}),X("active"))}function In(e,t){return H(()=>{let r=new g;r.subscribe({next({active:n}){e.classList.toggle("md-header__title--active",n)},complete(){e.classList.remove("md-header__title--active")}});let o=me(".md-content h1");return typeof o=="undefined"?L:Ia(o,t).pipe(y(n=>r.next(n)),_(()=>r.complete()),m(n=>F({ref:e},n)))})}function Fn(e,{viewport$:t,header$:r}){let o=r.pipe(m(({height:i})=>i),Y()),n=o.pipe(v(()=>Ee(e).pipe(m(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),X("bottom"))));return Q([o,n,t]).pipe(m(([i,{top:a,bottom:s},{offset:{y:c},size:{height:p}}])=>(p=Math.max(0,p-Math.max(0,a-c,i)-Math.max(0,p+c-s)),{offset:a-i,height:p,active:a-i<=c})),Y((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function Fa(e){let t=__md_get("__palette")||{index:e.findIndex(o=>matchMedia(o.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return $(...e).pipe(re(o=>d(o,"change").pipe(m(()=>o))),q(e[r]),m(o=>({index:e.indexOf(o),color:{media:o.getAttribute("data-md-color-media"),scheme:o.getAttribute("data-md-color-scheme"),primary:o.getAttribute("data-md-color-primary"),accent:o.getAttribute("data-md-color-accent")}})),B(1))}function jn(e){let t=R("input",e),r=E("meta",{name:"theme-color"});document.head.appendChild(r);let o=E("meta",{name:"color-scheme"});document.head.appendChild(o);let n=At("(prefers-color-scheme: light)");return H(()=>{let i=new g;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),c=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=c.getAttribute("data-md-color-scheme"),a.color.primary=c.getAttribute("data-md-color-primary"),a.color.accent=c.getAttribute("data-md-color-accent")}for(let[s,c]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,c);for(let s=0;sa.key==="Enter"),ne(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(m(()=>{let a=Te("header"),s=window.getComputedStyle(a);return o.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(c=>(+c).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(Oe(ae)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),Fa(t).pipe(U(n.pipe(Le(1))),at(),y(a=>i.next(a)),_(()=>i.complete()),m(a=>F({ref:e},a)))})}function Un(e,{progress$:t}){return H(()=>{let r=new g;return r.subscribe(({value:o})=>{e.style.setProperty("--md-progress-value",`${o}`)}),t.pipe(y(o=>r.next({value:o})),_(()=>r.complete()),m(o=>({ref:e,value:o})))})}var Kr=jt(zr());function ja(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function Wn({alert$:e}){Kr.default.isSupported()&&new j(t=>{new Kr.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||ja(P(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(y(t=>{t.trigger.focus()}),m(()=>ge("clipboard.copied"))).subscribe(e)}function Dn(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,e}function Ua(e,t){let r=new Map;for(let o of R("url",e)){let n=P("loc",o),i=[Dn(new URL(n.textContent),t)];r.set(`${i[0]}`,i);for(let a of R("[rel=alternate]",o)){let s=a.getAttribute("href");s!=null&&i.push(Dn(new URL(s),t))}}return r}function mr(e){return on(new URL("sitemap.xml",e)).pipe(m(t=>Ua(t,new URL(e))),he(()=>$(new Map)))}function Wa(e,t){if(!(e.target instanceof Element))return L;let r=e.target.closest("a");if(r===null)return L;if(r.target||e.metaKey||e.ctrlKey)return L;let o=new URL(r.href);return o.search=o.hash="",t.has(`${o}`)?(e.preventDefault(),$(new URL(r.href))):L}function Nn(e){let t=new Map;for(let r of R(":scope > *",e.head))t.set(r.outerHTML,r);return t}function Vn(e){for(let t of R("[href], [src]",e))for(let r of["href","src"]){let o=t.getAttribute(r);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){t[r]=t[r];break}}return $(e)}function Da(e){for(let o of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...G("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let n=me(o),i=me(o,e);typeof n!="undefined"&&typeof i!="undefined"&&n.replaceWith(i)}let t=Nn(document);for(let[o,n]of Nn(e))t.has(o)?t.delete(o):document.head.appendChild(n);for(let o of t.values()){let n=o.getAttribute("name");n!=="theme-color"&&n!=="color-scheme"&&o.remove()}let r=Te("container");return Fe(R("script",r)).pipe(v(o=>{let n=e.createElement("script");if(o.src){for(let i of o.getAttributeNames())n.setAttribute(i,o.getAttribute(i));return o.replaceWith(n),new j(i=>{n.onload=()=>i.complete()})}else return n.textContent=o.textContent,o.replaceWith(n),L}),ee(),oe(document))}function zn({location$:e,viewport$:t,progress$:r}){let o=we();if(location.protocol==="file:")return L;let n=mr(o.base);$(document).subscribe(Vn);let i=d(document.body,"click").pipe(je(n),v(([c,p])=>Wa(c,p)),le()),a=d(window,"popstate").pipe(m(ve),le());i.pipe(ne(t)).subscribe(([c,{offset:p}])=>{history.replaceState(p,""),history.pushState(null,"",c)}),T(i,a).subscribe(e);let s=e.pipe(X("pathname"),v(c=>rn(c,{progress$:r}).pipe(he(()=>(st(c,!0),L)))),v(Vn),v(Da),le());return T(s.pipe(ne(e,(c,p)=>p)),e.pipe(X("pathname"),v(()=>e),X("hash")),e.pipe(Y((c,p)=>c.pathname===p.pathname&&c.hash===p.hash),v(()=>i),y(()=>history.back()))).subscribe(c=>{var p,l;history.state!==null||!c.hash?window.scrollTo(0,(l=(p=history.state)==null?void 0:p.y)!=null?l:0):(history.scrollRestoration="auto",Zo(c.hash),history.scrollRestoration="manual")}),e.subscribe(()=>{history.scrollRestoration="manual"}),d(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),t.pipe(X("offset"),be(100)).subscribe(({offset:c})=>{history.replaceState(c,"")}),s}var Qn=jt(Kn());function Yn(e){let t=e.separator.split("|").map(n=>n.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":n).join("|"),r=new RegExp(t,"img"),o=(n,i,a)=>`${i}${a}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").trim();let i=new RegExp(`(^|${e.separator}|)(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return a=>(0,Qn.default)(a).replace(i,o).replace(/<\/mark>(\s+)]*>/img,"$1")}}function Ht(e){return e.type===1}function fr(e){return e.type===3}function Bn(e,t){let r=ln(e);return T($(location.protocol!=="file:"),We("search")).pipe(He(o=>o),v(()=>t)).subscribe(({config:o,docs:n})=>r.next({type:0,data:{config:o,docs:n,options:{suggest:G("search.suggest")}}})),r}function Gn({document$:e}){let t=we(),r=De(new URL("../versions.json",t.base)).pipe(he(()=>L)),o=r.pipe(m(n=>{let[,i]=t.base.match(/([^/]+)\/?$/);return n.find(({version:a,aliases:s})=>a===i||s.includes(i))||n[0]}));r.pipe(m(n=>new Map(n.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),v(n=>d(document.body,"click").pipe(b(i=>!i.metaKey&&!i.ctrlKey),ne(o),v(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&n.has(s.href)){let c=s.href;return!i.target.closest(".md-version")&&n.get(c)===a?L:(i.preventDefault(),$(c))}}return L}),v(i=>{let{version:a}=n.get(i);return mr(new URL(i)).pipe(m(s=>{let p=ve().href.replace(t.base,"");return s.has(p.split("#")[0])?new URL(`../${a}/${p}`,t.base):new URL(i)}))})))).subscribe(n=>st(n,!0)),Q([r,o]).subscribe(([n,i])=>{P(".md-header__topic").appendChild(gn(n,i))}),e.pipe(v(()=>o)).subscribe(n=>{var a;let i=__md_get("__outdated",sessionStorage);if(i===null){i=!0;let s=((a=t.version)==null?void 0:a.default)||"latest";Array.isArray(s)||(s=[s]);e:for(let c of s)for(let p of n.aliases.concat(n.version))if(new RegExp(c,"i").test(p)){i=!1;break e}__md_set("__outdated",i,sessionStorage)}if(i)for(let s of ie("outdated"))s.hidden=!1})}function Ka(e,{worker$:t}){let{searchParams:r}=ve();r.has("q")&&(Be("search",!0),e.value=r.get("q"),e.focus(),We("search").pipe(He(i=>!i)).subscribe(()=>{let i=ve();i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=vt(e),n=T(t.pipe(He(Ht)),d(e,"keyup"),o).pipe(m(()=>e.value),Y());return Q([n,o]).pipe(m(([i,a])=>({value:i,focus:a})),B(1))}function Jn(e,{worker$:t}){let r=new g,o=r.pipe(ee(),oe(!0));Q([t.pipe(He(Ht)),r],(i,a)=>a).pipe(X("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(X("focus")).subscribe(({focus:i})=>{i&&Be("search",i)}),d(e.form,"reset").pipe(U(o)).subscribe(()=>e.focus());let n=P("header [for=__search]");return d(n,"click").subscribe(()=>e.focus()),Ka(e,{worker$:t}).pipe(y(i=>r.next(i)),_(()=>r.complete()),m(i=>F({ref:e},i)),B(1))}function Xn(e,{worker$:t,query$:r}){let o=new g,n=Yo(e.parentElement).pipe(b(Boolean)),i=e.parentElement,a=P(":scope > :first-child",e),s=P(":scope > :last-child",e);We("search").subscribe(l=>s.setAttribute("role",l?"list":"presentation")),o.pipe(ne(r),Ir(t.pipe(He(Ht)))).subscribe(([{items:l},{value:f}])=>{switch(l.length){case 0:a.textContent=f.length?ge("search.result.none"):ge("search.result.placeholder");break;case 1:a.textContent=ge("search.result.one");break;default:let u=ar(l.length);a.textContent=ge("search.result.other",u)}});let c=o.pipe(y(()=>s.innerHTML=""),v(({items:l})=>T($(...l.slice(0,10)),$(...l.slice(10)).pipe(Ke(4),jr(n),v(([f])=>f)))),m(hn),le());return c.subscribe(l=>s.appendChild(l)),c.pipe(re(l=>{let f=me("details",l);return typeof f=="undefined"?L:d(f,"toggle").pipe(U(o),m(()=>f))})).subscribe(l=>{l.open===!1&&l.offsetTop<=i.scrollTop&&i.scrollTo({top:l.offsetTop})}),t.pipe(b(fr),m(({data:l})=>l)).pipe(y(l=>o.next(l)),_(()=>o.complete()),m(l=>F({ref:e},l)))}function Qa(e,{query$:t}){return t.pipe(m(({value:r})=>{let o=ve();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function Zn(e,t){let r=new g,o=r.pipe(ee(),oe(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),d(e,"click").pipe(U(o)).subscribe(n=>n.preventDefault()),Qa(e,t).pipe(y(n=>r.next(n)),_(()=>r.complete()),m(n=>F({ref:e},n)))}function ei(e,{worker$:t,keyboard$:r}){let o=new g,n=Te("search-query"),i=T(d(n,"keydown"),d(n,"focus")).pipe(Oe(ae),m(()=>n.value),Y());return o.pipe(je(i),m(([{suggest:s},c])=>{let p=c.split(/([\s-]+)/);if(s!=null&&s.length&&p[p.length-1]){let l=s[s.length-1];l.startsWith(p[p.length-1])&&(p[p.length-1]=l)}else p.length=0;return p})).subscribe(s=>e.innerHTML=s.join("").replace(/\s/g," ")),r.pipe(b(({mode:s})=>s==="search")).subscribe(s=>{switch(s.type){case"ArrowRight":e.innerText.length&&n.selectionStart===n.value.length&&(n.value=e.innerText);break}}),t.pipe(b(fr),m(({data:s})=>s)).pipe(y(s=>o.next(s)),_(()=>o.complete()),m(()=>({ref:e})))}function ti(e,{index$:t,keyboard$:r}){let o=we();try{let n=Bn(o.search,t),i=Te("search-query",e),a=Te("search-result",e);d(e,"click").pipe(b(({target:c})=>c instanceof Element&&!!c.closest("a"))).subscribe(()=>Be("search",!1)),r.pipe(b(({mode:c})=>c==="search")).subscribe(c=>{let p=Re();switch(c.type){case"Enter":if(p===i){let l=new Map;for(let f of R(":first-child [href]",a)){let u=f.firstElementChild;l.set(f,parseFloat(u.getAttribute("data-md-score")))}if(l.size){let[[f]]=[...l].sort(([,u],[,h])=>h-u);f.click()}c.claim()}break;case"Escape":case"Tab":Be("search",!1),i.blur();break;case"ArrowUp":case"ArrowDown":if(typeof p=="undefined")i.focus();else{let l=[i,...R(":not(details) > [href], summary, details[open] [href]",a)],f=Math.max(0,(Math.max(0,l.indexOf(p))+l.length+(c.type==="ArrowUp"?-1:1))%l.length);l[f].focus()}c.claim();break;default:i!==Re()&&i.focus()}}),r.pipe(b(({mode:c})=>c==="global")).subscribe(c=>{switch(c.type){case"f":case"s":case"/":i.focus(),i.select(),c.claim();break}});let s=Jn(i,{worker$:n});return T(s,Xn(a,{worker$:n,query$:s})).pipe($e(...ie("search-share",e).map(c=>Zn(c,{query$:s})),...ie("search-suggest",e).map(c=>ei(c,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,qe}}function ri(e,{index$:t,location$:r}){return Q([t,r.pipe(q(ve()),b(o=>!!o.searchParams.get("h")))]).pipe(m(([o,n])=>Yn(o.config)(n.searchParams.get("h"))),m(o=>{var a;let n=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let c=s.textContent,p=o(c);p.length>c.length&&n.set(s,p)}for(let[s,c]of n){let{childNodes:p}=E("span",null,c);s.replaceWith(...Array.from(p))}return{ref:e,nodes:n}}))}function Ya(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return Q([r,t]).pipe(m(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(n,Math.max(0,s-i))-n,{height:a,locked:s>=i+n})),Y((i,a)=>i.height===a.height&&i.locked===a.locked))}function Qr(e,o){var n=o,{header$:t}=n,r=to(n,["header$"]);let i=P(".md-sidebar__scrollwrap",e),{y:a}=Ue(i);return H(()=>{let s=new g,c=s.pipe(ee(),oe(!0)),p=s.pipe(Me(0,de));return p.pipe(ne(t)).subscribe({next([{height:l},{height:f}]){i.style.height=`${l-2*a}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),p.pipe(He()).subscribe(()=>{for(let l of R(".md-nav__link--active[href]",e)){if(!l.clientHeight)continue;let f=l.closest(".md-sidebar__scrollwrap");if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:h}=pe(f);f.scrollTo({top:u-h/2})}}}),fe(R("label[tabindex]",e)).pipe(re(l=>d(l,"click").pipe(Oe(ae),m(()=>l),U(c)))).subscribe(l=>{let f=P(`[id="${l.htmlFor}"]`);P(`[aria-labelledby="${l.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),Ya(e,r).pipe(y(l=>s.next(l)),_(()=>s.complete()),m(l=>F({ref:e},l)))})}function oi(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return Lt(De(`${r}/releases/latest`).pipe(he(()=>L),m(o=>({version:o.tag_name})),Qe({})),De(r).pipe(he(()=>L),m(o=>({stars:o.stargazers_count,forks:o.forks_count})),Qe({}))).pipe(m(([o,n])=>F(F({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return De(r).pipe(m(o=>({repositories:o.public_repos})),Qe({}))}}function ni(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return De(r).pipe(he(()=>L),m(({star_count:o,forks_count:n})=>({stars:o,forks:n})),Qe({}))}function ii(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return oi(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return ni(r,o)}return L}var Ba;function Ga(e){return Ba||(Ba=H(()=>{let t=__md_get("__source",sessionStorage);if(t)return $(t);if(ie("consent").length){let o=__md_get("__consent");if(!(o&&o.github))return L}return ii(e.href).pipe(y(o=>__md_set("__source",o,sessionStorage)))}).pipe(he(()=>L),b(t=>Object.keys(t).length>0),m(t=>({facts:t})),B(1)))}function ai(e){let t=P(":scope > :last-child",e);return H(()=>{let r=new g;return r.subscribe(({facts:o})=>{t.appendChild(bn(o)),t.classList.add("md-source__repository--active")}),Ga(e).pipe(y(o=>r.next(o)),_(()=>r.complete()),m(o=>F({ref:e},o)))})}function Ja(e,{viewport$:t,header$:r}){return Ee(document.body).pipe(v(()=>pr(e,{header$:r,viewport$:t})),m(({offset:{y:o}})=>({hidden:o>=10})),X("hidden"))}function si(e,t){return H(()=>{let r=new g;return r.subscribe({next({hidden:o}){e.hidden=o},complete(){e.hidden=!1}}),(G("navigation.tabs.sticky")?$({hidden:!1}):Ja(e,t)).pipe(y(o=>r.next(o)),_(()=>r.complete()),m(o=>F({ref:e},o)))})}function Xa(e,{viewport$:t,header$:r}){let o=new Map,n=R(".md-nav__link",e);for(let s of n){let c=decodeURIComponent(s.hash.substring(1)),p=me(`[id="${c}"]`);typeof p!="undefined"&&o.set(s,p)}let i=r.pipe(X("height"),m(({height:s})=>{let c=Te("main"),p=P(":scope > :first-child",c);return s+.8*(p.offsetTop-c.offsetTop)}),le());return Ee(document.body).pipe(X("height"),v(s=>H(()=>{let c=[];return $([...o].reduce((p,[l,f])=>{for(;c.length&&o.get(c[c.length-1]).tagName>=f.tagName;)c.pop();let u=f.offsetTop;for(;!u&&f.parentElement;)f=f.parentElement,u=f.offsetTop;let h=f.offsetParent;for(;h;h=h.offsetParent)u+=h.offsetTop;return p.set([...c=[...c,l]].reverse(),u)},new Map))}).pipe(m(c=>new Map([...c].sort(([,p],[,l])=>p-l))),je(i),v(([c,p])=>t.pipe(Rr(([l,f],{offset:{y:u},size:h})=>{let w=u+h.height>=Math.floor(s.height);for(;f.length;){let[,A]=f[0];if(A-p=u&&!w)f=[l.pop(),...f];else break}return[l,f]},[[],[...c]]),Y((l,f)=>l[0]===f[0]&&l[1]===f[1])))))).pipe(m(([s,c])=>({prev:s.map(([p])=>p),next:c.map(([p])=>p)})),q({prev:[],next:[]}),Ke(2,1),m(([s,c])=>s.prev.length{let i=new g,a=i.pipe(ee(),oe(!0));if(i.subscribe(({prev:s,next:c})=>{for(let[p]of c)p.classList.remove("md-nav__link--passed"),p.classList.remove("md-nav__link--active");for(let[p,[l]]of s.entries())l.classList.add("md-nav__link--passed"),l.classList.toggle("md-nav__link--active",p===s.length-1)}),G("toc.follow")){let s=T(t.pipe(be(1),m(()=>{})),t.pipe(be(250),m(()=>"smooth")));i.pipe(b(({prev:c})=>c.length>0),je(o.pipe(Oe(ae))),ne(s)).subscribe(([[{prev:c}],p])=>{let[l]=c[c.length-1];if(l.offsetHeight){let f=sr(l);if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:h}=pe(f);f.scrollTo({top:u-h/2,behavior:p})}}})}return G("navigation.tracking")&&t.pipe(U(a),X("offset"),be(250),Le(1),U(n.pipe(Le(1))),at({delay:250}),ne(i)).subscribe(([,{prev:s}])=>{let c=ve(),p=s[s.length-1];if(p&&p.length){let[l]=p,{hash:f}=new URL(l.href);c.hash!==f&&(c.hash=f,history.replaceState({},"",`${c}`))}else c.hash="",history.replaceState({},"",`${c}`)}),Xa(e,{viewport$:t,header$:r}).pipe(y(s=>i.next(s)),_(()=>i.complete()),m(s=>F({ref:e},s)))})}function Za(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(m(({offset:{y:a}})=>a),Ke(2,1),m(([a,s])=>a>s&&s>0),Y()),i=r.pipe(m(({active:a})=>a));return Q([i,n]).pipe(m(([a,s])=>!(a&&s)),Y(),U(o.pipe(Le(1))),oe(!0),at({delay:250}),m(a=>({hidden:a})))}function pi(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new g,a=i.pipe(ee(),oe(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(U(a),X("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),d(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),Za(e,{viewport$:t,main$:o,target$:n}).pipe(y(s=>i.next(s)),_(()=>i.complete()),m(s=>F({ref:e},s)))}function li({document$:e}){e.pipe(v(()=>R(".md-ellipsis")),re(t=>yt(t).pipe(U(e.pipe(Le(1))),b(r=>r),m(()=>t),ye(1))),b(t=>t.offsetWidth{let r=t.innerText,o=t.closest("a")||t;return o.title=r,Ge(o).pipe(U(e.pipe(Le(1))),_(()=>o.removeAttribute("title")))})).subscribe(),e.pipe(v(()=>R(".md-status")),re(t=>Ge(t))).subscribe()}function mi({document$:e,tablet$:t}){e.pipe(v(()=>R(".md-toggle--indeterminate")),y(r=>{r.indeterminate=!0,r.checked=!1}),re(r=>d(r,"change").pipe(Fr(()=>r.classList.contains("md-toggle--indeterminate")),m(()=>r))),ne(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function es(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function fi({document$:e}){e.pipe(v(()=>R("[data-md-scrollfix]")),y(t=>t.removeAttribute("data-md-scrollfix")),b(es),re(t=>d(t,"touchstart").pipe(m(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function ui({viewport$:e,tablet$:t}){Q([We("search"),t]).pipe(m(([r,o])=>r&&!o),v(r=>$(r).pipe(Ye(r?400:100))),ne(e)).subscribe(([r,{offset:{y:o}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${o}px`;else{let n=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",n&&window.scrollTo(0,n)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let o=e[r];typeof o=="string"?o=document.createTextNode(o):o.parentNode&&o.parentNode.removeChild(o),r?t.insertBefore(this.previousSibling,o):t.replaceChild(o,this)}}}));function ts(){return location.protocol==="file:"?gt(`${new URL("search/search_index.js",Yr.base)}`).pipe(m(()=>__index),B(1)):De(new URL("search/search_index.json",Yr.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var rt=No(),Rt=Jo(),wt=en(Rt),Br=Go(),_e=pn(),ur=At("(min-width: 960px)"),hi=At("(min-width: 1220px)"),bi=tn(),Yr=we(),vi=document.forms.namedItem("search")?ts():qe,Gr=new g;Wn({alert$:Gr});var Jr=new g;G("navigation.instant")&&zn({location$:Rt,viewport$:_e,progress$:Jr}).subscribe(rt);var di;((di=Yr.version)==null?void 0:di.provider)==="mike"&&Gn({document$:rt});T(Rt,wt).pipe(Ye(125)).subscribe(()=>{Be("drawer",!1),Be("search",!1)});Br.pipe(b(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=me("link[rel=prev]");typeof t!="undefined"&&st(t);break;case"n":case".":let r=me("link[rel=next]");typeof r!="undefined"&&st(r);break;case"Enter":let o=Re();o instanceof HTMLLabelElement&&o.click()}});li({document$:rt});mi({document$:rt,tablet$:ur});fi({document$:rt});ui({viewport$:_e,tablet$:ur});var tt=Rn(Te("header"),{viewport$:_e}),$t=rt.pipe(m(()=>Te("main")),v(e=>Fn(e,{viewport$:_e,header$:tt})),B(1)),rs=T(...ie("consent").map(e=>fn(e,{target$:wt})),...ie("dialog").map(e=>$n(e,{alert$:Gr})),...ie("header").map(e=>Pn(e,{viewport$:_e,header$:tt,main$:$t})),...ie("palette").map(e=>jn(e)),...ie("progress").map(e=>Un(e,{progress$:Jr})),...ie("search").map(e=>ti(e,{index$:vi,keyboard$:Br})),...ie("source").map(e=>ai(e))),os=H(()=>T(...ie("announce").map(e=>mn(e)),...ie("content").map(e=>Hn(e,{viewport$:_e,target$:wt,print$:bi})),...ie("content").map(e=>G("search.highlight")?ri(e,{index$:vi,location$:Rt}):L),...ie("header-title").map(e=>In(e,{viewport$:_e,header$:tt})),...ie("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?Ur(hi,()=>Qr(e,{viewport$:_e,header$:tt,main$:$t})):Ur(ur,()=>Qr(e,{viewport$:_e,header$:tt,main$:$t}))),...ie("tabs").map(e=>si(e,{viewport$:_e,header$:tt})),...ie("toc").map(e=>ci(e,{viewport$:_e,header$:tt,main$:$t,target$:wt})),...ie("top").map(e=>pi(e,{viewport$:_e,header$:tt,main$:$t,target$:wt})))),gi=rt.pipe(v(()=>os),$e(rs),B(1));gi.subscribe();window.document$=rt;window.location$=Rt;window.target$=wt;window.keyboard$=Br;window.viewport$=_e;window.tablet$=ur;window.screen$=hi;window.print$=bi;window.alert$=Gr;window.progress$=Jr;window.component$=gi;})(); +//# sourceMappingURL=bundle.1e8ae164.min.js.map + diff --git a/assets/javascripts/bundle.1e8ae164.min.js.map b/assets/javascripts/bundle.1e8ae164.min.js.map new file mode 100644 index 0000000000..6c33b8e8e6 --- /dev/null +++ b/assets/javascripts/bundle.1e8ae164.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/clipboard/dist/clipboard.js", "node_modules/escape-html/index.js", "src/templates/assets/javascripts/bundle.ts", "node_modules/rxjs/node_modules/tslib/tslib.es6.js", "node_modules/rxjs/src/internal/util/isFunction.ts", "node_modules/rxjs/src/internal/util/createErrorClass.ts", "node_modules/rxjs/src/internal/util/UnsubscriptionError.ts", "node_modules/rxjs/src/internal/util/arrRemove.ts", "node_modules/rxjs/src/internal/Subscription.ts", "node_modules/rxjs/src/internal/config.ts", "node_modules/rxjs/src/internal/scheduler/timeoutProvider.ts", "node_modules/rxjs/src/internal/util/reportUnhandledError.ts", "node_modules/rxjs/src/internal/util/noop.ts", "node_modules/rxjs/src/internal/NotificationFactories.ts", "node_modules/rxjs/src/internal/util/errorContext.ts", "node_modules/rxjs/src/internal/Subscriber.ts", "node_modules/rxjs/src/internal/symbol/observable.ts", "node_modules/rxjs/src/internal/util/identity.ts", "node_modules/rxjs/src/internal/util/pipe.ts", "node_modules/rxjs/src/internal/Observable.ts", "node_modules/rxjs/src/internal/util/lift.ts", "node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts", "node_modules/rxjs/src/internal/scheduler/animationFrameProvider.ts", "node_modules/rxjs/src/internal/util/ObjectUnsubscribedError.ts", "node_modules/rxjs/src/internal/Subject.ts", "node_modules/rxjs/src/internal/scheduler/dateTimestampProvider.ts", "node_modules/rxjs/src/internal/ReplaySubject.ts", "node_modules/rxjs/src/internal/scheduler/Action.ts", "node_modules/rxjs/src/internal/scheduler/intervalProvider.ts", "node_modules/rxjs/src/internal/scheduler/AsyncAction.ts", "node_modules/rxjs/src/internal/Scheduler.ts", "node_modules/rxjs/src/internal/scheduler/AsyncScheduler.ts", "node_modules/rxjs/src/internal/scheduler/async.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameAction.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameScheduler.ts", "node_modules/rxjs/src/internal/scheduler/animationFrame.ts", "node_modules/rxjs/src/internal/observable/empty.ts", "node_modules/rxjs/src/internal/util/isScheduler.ts", "node_modules/rxjs/src/internal/util/args.ts", "node_modules/rxjs/src/internal/util/isArrayLike.ts", "node_modules/rxjs/src/internal/util/isPromise.ts", "node_modules/rxjs/src/internal/util/isInteropObservable.ts", "node_modules/rxjs/src/internal/util/isAsyncIterable.ts", "node_modules/rxjs/src/internal/util/throwUnobservableError.ts", "node_modules/rxjs/src/internal/symbol/iterator.ts", "node_modules/rxjs/src/internal/util/isIterable.ts", "node_modules/rxjs/src/internal/util/isReadableStreamLike.ts", "node_modules/rxjs/src/internal/observable/innerFrom.ts", "node_modules/rxjs/src/internal/util/executeSchedule.ts", "node_modules/rxjs/src/internal/operators/observeOn.ts", "node_modules/rxjs/src/internal/operators/subscribeOn.ts", "node_modules/rxjs/src/internal/scheduled/scheduleObservable.ts", "node_modules/rxjs/src/internal/scheduled/schedulePromise.ts", "node_modules/rxjs/src/internal/scheduled/scheduleArray.ts", "node_modules/rxjs/src/internal/scheduled/scheduleIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleAsyncIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleReadableStreamLike.ts", "node_modules/rxjs/src/internal/scheduled/scheduled.ts", "node_modules/rxjs/src/internal/observable/from.ts", "node_modules/rxjs/src/internal/observable/of.ts", "node_modules/rxjs/src/internal/observable/throwError.ts", "node_modules/rxjs/src/internal/util/EmptyError.ts", "node_modules/rxjs/src/internal/util/isDate.ts", "node_modules/rxjs/src/internal/operators/map.ts", "node_modules/rxjs/src/internal/util/mapOneOrManyArgs.ts", "node_modules/rxjs/src/internal/util/argsArgArrayOrObject.ts", "node_modules/rxjs/src/internal/util/createObject.ts", "node_modules/rxjs/src/internal/observable/combineLatest.ts", "node_modules/rxjs/src/internal/operators/mergeInternals.ts", "node_modules/rxjs/src/internal/operators/mergeMap.ts", "node_modules/rxjs/src/internal/operators/mergeAll.ts", "node_modules/rxjs/src/internal/operators/concatAll.ts", "node_modules/rxjs/src/internal/observable/concat.ts", "node_modules/rxjs/src/internal/observable/defer.ts", "node_modules/rxjs/src/internal/observable/fromEvent.ts", "node_modules/rxjs/src/internal/observable/fromEventPattern.ts", "node_modules/rxjs/src/internal/observable/timer.ts", "node_modules/rxjs/src/internal/observable/merge.ts", "node_modules/rxjs/src/internal/observable/never.ts", "node_modules/rxjs/src/internal/util/argsOrArgArray.ts", "node_modules/rxjs/src/internal/operators/filter.ts", "node_modules/rxjs/src/internal/observable/zip.ts", "node_modules/rxjs/src/internal/operators/audit.ts", "node_modules/rxjs/src/internal/operators/auditTime.ts", "node_modules/rxjs/src/internal/operators/bufferCount.ts", "node_modules/rxjs/src/internal/operators/catchError.ts", "node_modules/rxjs/src/internal/operators/scanInternals.ts", "node_modules/rxjs/src/internal/operators/combineLatest.ts", "node_modules/rxjs/src/internal/operators/combineLatestWith.ts", "node_modules/rxjs/src/internal/operators/debounceTime.ts", "node_modules/rxjs/src/internal/operators/defaultIfEmpty.ts", "node_modules/rxjs/src/internal/operators/take.ts", "node_modules/rxjs/src/internal/operators/ignoreElements.ts", "node_modules/rxjs/src/internal/operators/mapTo.ts", "node_modules/rxjs/src/internal/operators/delayWhen.ts", "node_modules/rxjs/src/internal/operators/delay.ts", "node_modules/rxjs/src/internal/operators/distinctUntilChanged.ts", "node_modules/rxjs/src/internal/operators/distinctUntilKeyChanged.ts", "node_modules/rxjs/src/internal/operators/throwIfEmpty.ts", "node_modules/rxjs/src/internal/operators/endWith.ts", "node_modules/rxjs/src/internal/operators/finalize.ts", "node_modules/rxjs/src/internal/operators/first.ts", "node_modules/rxjs/src/internal/operators/takeLast.ts", "node_modules/rxjs/src/internal/operators/merge.ts", "node_modules/rxjs/src/internal/operators/mergeWith.ts", "node_modules/rxjs/src/internal/operators/repeat.ts", "node_modules/rxjs/src/internal/operators/scan.ts", "node_modules/rxjs/src/internal/operators/share.ts", "node_modules/rxjs/src/internal/operators/shareReplay.ts", "node_modules/rxjs/src/internal/operators/skip.ts", "node_modules/rxjs/src/internal/operators/skipUntil.ts", "node_modules/rxjs/src/internal/operators/startWith.ts", "node_modules/rxjs/src/internal/operators/switchMap.ts", "node_modules/rxjs/src/internal/operators/takeUntil.ts", "node_modules/rxjs/src/internal/operators/takeWhile.ts", "node_modules/rxjs/src/internal/operators/tap.ts", "node_modules/rxjs/src/internal/operators/throttle.ts", "node_modules/rxjs/src/internal/operators/throttleTime.ts", "node_modules/rxjs/src/internal/operators/withLatestFrom.ts", "node_modules/rxjs/src/internal/operators/zip.ts", "node_modules/rxjs/src/internal/operators/zipWith.ts", "src/templates/assets/javascripts/browser/document/index.ts", "src/templates/assets/javascripts/browser/element/_/index.ts", "src/templates/assets/javascripts/browser/element/focus/index.ts", "src/templates/assets/javascripts/browser/element/hover/index.ts", "src/templates/assets/javascripts/browser/element/offset/_/index.ts", "src/templates/assets/javascripts/browser/element/offset/content/index.ts", "src/templates/assets/javascripts/utilities/h/index.ts", "src/templates/assets/javascripts/utilities/round/index.ts", "src/templates/assets/javascripts/browser/script/index.ts", "src/templates/assets/javascripts/browser/element/size/_/index.ts", "src/templates/assets/javascripts/browser/element/size/content/index.ts", "src/templates/assets/javascripts/browser/element/visibility/index.ts", "src/templates/assets/javascripts/browser/toggle/index.ts", "src/templates/assets/javascripts/browser/keyboard/index.ts", "src/templates/assets/javascripts/browser/location/_/index.ts", "src/templates/assets/javascripts/browser/location/hash/index.ts", "src/templates/assets/javascripts/browser/media/index.ts", "src/templates/assets/javascripts/browser/request/index.ts", "src/templates/assets/javascripts/browser/viewport/offset/index.ts", "src/templates/assets/javascripts/browser/viewport/size/index.ts", "src/templates/assets/javascripts/browser/viewport/_/index.ts", "src/templates/assets/javascripts/browser/viewport/at/index.ts", "src/templates/assets/javascripts/browser/worker/index.ts", "src/templates/assets/javascripts/_/index.ts", "src/templates/assets/javascripts/components/_/index.ts", "src/templates/assets/javascripts/components/announce/index.ts", "src/templates/assets/javascripts/components/consent/index.ts", "src/templates/assets/javascripts/templates/tooltip/index.tsx", "src/templates/assets/javascripts/templates/annotation/index.tsx", "src/templates/assets/javascripts/templates/clipboard/index.tsx", "src/templates/assets/javascripts/templates/search/index.tsx", "src/templates/assets/javascripts/templates/source/index.tsx", "src/templates/assets/javascripts/templates/tabbed/index.tsx", "src/templates/assets/javascripts/templates/table/index.tsx", "src/templates/assets/javascripts/templates/version/index.tsx", "src/templates/assets/javascripts/components/tooltip/index.ts", "src/templates/assets/javascripts/components/content/annotation/_/index.ts", "src/templates/assets/javascripts/components/content/annotation/list/index.ts", "src/templates/assets/javascripts/components/content/annotation/block/index.ts", "src/templates/assets/javascripts/components/content/code/_/index.ts", "src/templates/assets/javascripts/components/content/details/index.ts", "src/templates/assets/javascripts/components/content/mermaid/index.css", "src/templates/assets/javascripts/components/content/mermaid/index.ts", "src/templates/assets/javascripts/components/content/table/index.ts", "src/templates/assets/javascripts/components/content/tabs/index.ts", "src/templates/assets/javascripts/components/content/_/index.ts", "src/templates/assets/javascripts/components/dialog/index.ts", "src/templates/assets/javascripts/components/header/_/index.ts", "src/templates/assets/javascripts/components/header/title/index.ts", "src/templates/assets/javascripts/components/main/index.ts", "src/templates/assets/javascripts/components/palette/index.ts", "src/templates/assets/javascripts/components/progress/index.ts", "src/templates/assets/javascripts/integrations/clipboard/index.ts", "src/templates/assets/javascripts/integrations/sitemap/index.ts", "src/templates/assets/javascripts/integrations/instant/index.ts", "src/templates/assets/javascripts/integrations/search/highlighter/index.ts", "src/templates/assets/javascripts/integrations/search/worker/message/index.ts", "src/templates/assets/javascripts/integrations/search/worker/_/index.ts", "src/templates/assets/javascripts/integrations/version/index.ts", "src/templates/assets/javascripts/components/search/query/index.ts", "src/templates/assets/javascripts/components/search/result/index.ts", "src/templates/assets/javascripts/components/search/share/index.ts", "src/templates/assets/javascripts/components/search/suggest/index.ts", "src/templates/assets/javascripts/components/search/_/index.ts", "src/templates/assets/javascripts/components/search/highlight/index.ts", "src/templates/assets/javascripts/components/sidebar/index.ts", "src/templates/assets/javascripts/components/source/facts/github/index.ts", "src/templates/assets/javascripts/components/source/facts/gitlab/index.ts", "src/templates/assets/javascripts/components/source/facts/_/index.ts", "src/templates/assets/javascripts/components/source/_/index.ts", "src/templates/assets/javascripts/components/tabs/index.ts", "src/templates/assets/javascripts/components/toc/index.ts", "src/templates/assets/javascripts/components/top/index.ts", "src/templates/assets/javascripts/patches/ellipsis/index.ts", "src/templates/assets/javascripts/patches/indeterminate/index.ts", "src/templates/assets/javascripts/patches/scrollfix/index.ts", "src/templates/assets/javascripts/patches/scrolllock/index.ts", "src/templates/assets/javascripts/polyfills/index.ts"], + "sourcesContent": ["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesAllowlist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. \u00AF\\_(\u30C4)_/\u00AF\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n", "/*!\n * clipboard.js v2.0.11\n * https://clipboardjs.com/\n *\n * Licensed MIT \u00A9 Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function() { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 686:\n/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n \"default\": function() { return /* binding */ clipboard; }\n});\n\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(279);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(370);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(817);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n;// CONCATENATED MODULE: ./src/common/command.js\n/**\n * Executes a given operation type.\n * @param {String} type\n * @return {Boolean}\n */\nfunction command(type) {\n try {\n return document.execCommand(type);\n } catch (err) {\n return false;\n }\n}\n;// CONCATENATED MODULE: ./src/actions/cut.js\n\n\n/**\n * Cut action wrapper.\n * @param {String|HTMLElement} target\n * @return {String}\n */\n\nvar ClipboardActionCut = function ClipboardActionCut(target) {\n var selectedText = select_default()(target);\n command('cut');\n return selectedText;\n};\n\n/* harmony default export */ var actions_cut = (ClipboardActionCut);\n;// CONCATENATED MODULE: ./src/common/create-fake-element.js\n/**\n * Creates a fake textarea element with a value.\n * @param {String} value\n * @return {HTMLElement}\n */\nfunction createFakeElement(value) {\n var isRTL = document.documentElement.getAttribute('dir') === 'rtl';\n var fakeElement = document.createElement('textarea'); // Prevent zooming on iOS\n\n fakeElement.style.fontSize = '12pt'; // Reset box model\n\n fakeElement.style.border = '0';\n fakeElement.style.padding = '0';\n fakeElement.style.margin = '0'; // Move element out of screen horizontally\n\n fakeElement.style.position = 'absolute';\n fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; // Move element to the same position vertically\n\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n fakeElement.style.top = \"\".concat(yPosition, \"px\");\n fakeElement.setAttribute('readonly', '');\n fakeElement.value = value;\n return fakeElement;\n}\n;// CONCATENATED MODULE: ./src/actions/copy.js\n\n\n\n/**\n * Create fake copy action wrapper using a fake element.\n * @param {String} target\n * @param {Object} options\n * @return {String}\n */\n\nvar fakeCopyAction = function fakeCopyAction(value, options) {\n var fakeElement = createFakeElement(value);\n options.container.appendChild(fakeElement);\n var selectedText = select_default()(fakeElement);\n command('copy');\n fakeElement.remove();\n return selectedText;\n};\n/**\n * Copy action wrapper.\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @return {String}\n */\n\n\nvar ClipboardActionCopy = function ClipboardActionCopy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n var selectedText = '';\n\n if (typeof target === 'string') {\n selectedText = fakeCopyAction(target, options);\n } else if (target instanceof HTMLInputElement && !['text', 'search', 'url', 'tel', 'password'].includes(target === null || target === void 0 ? void 0 : target.type)) {\n // If input type doesn't support `setSelectionRange`. Simulate it. https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange\n selectedText = fakeCopyAction(target.value, options);\n } else {\n selectedText = select_default()(target);\n command('copy');\n }\n\n return selectedText;\n};\n\n/* harmony default export */ var actions_copy = (ClipboardActionCopy);\n;// CONCATENATED MODULE: ./src/actions/default.js\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\n\n\n/**\n * Inner function which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n * @param {Object} options\n */\n\nvar ClipboardActionDefault = function ClipboardActionDefault() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n // Defines base properties passed from constructor.\n var _options$action = options.action,\n action = _options$action === void 0 ? 'copy' : _options$action,\n container = options.container,\n target = options.target,\n text = options.text; // Sets the `action` to be performed which can be either 'copy' or 'cut'.\n\n if (action !== 'copy' && action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n } // Sets the `target` property using an element that will be have its content copied.\n\n\n if (target !== undefined) {\n if (target && _typeof(target) === 'object' && target.nodeType === 1) {\n if (action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n } // Define selection strategy based on `text` property.\n\n\n if (text) {\n return actions_copy(text, {\n container: container\n });\n } // Defines which selection strategy based on `target` property.\n\n\n if (target) {\n return action === 'cut' ? actions_cut(target) : actions_copy(target, {\n container: container\n });\n }\n};\n\n/* harmony default export */ var actions_default = (ClipboardActionDefault);\n;// CONCATENATED MODULE: ./src/clipboard.js\nfunction clipboard_typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { clipboard_typeof = function _typeof(obj) { return typeof obj; }; } else { clipboard_typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return clipboard_typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (clipboard_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\n\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\nfunction getAttributeValue(suffix, element) {\n var attribute = \"data-clipboard-\".concat(suffix);\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\n\nvar Clipboard = /*#__PURE__*/function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n var _super = _createSuper(Clipboard);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n var _this;\n\n _classCallCheck(this, Clipboard);\n\n _this = _super.call(this);\n\n _this.resolveOptions(options);\n\n _this.listenClick(trigger);\n\n return _this;\n }\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n _createClass(Clipboard, [{\n key: \"resolveOptions\",\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: \"listenClick\",\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: \"onClick\",\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n var action = this.action(trigger) || 'copy';\n var text = actions_default({\n action: action,\n container: this.container,\n target: this.target(trigger),\n text: this.text(trigger)\n }); // Fires an event based on the copy operation result.\n\n this.emit(text ? 'success' : 'error', {\n action: action,\n text: text,\n trigger: trigger,\n clearSelection: function clearSelection() {\n if (trigger) {\n trigger.focus();\n }\n\n window.getSelection().removeAllRanges();\n }\n });\n }\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultAction\",\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultTarget\",\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n /**\n * Allow fire programmatically a copy action\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @returns Text copied.\n */\n\n }, {\n key: \"defaultText\",\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: \"destroy\",\n value: function destroy() {\n this.listener.destroy();\n }\n }], [{\n key: \"copy\",\n value: function copy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n return actions_copy(target, options);\n }\n /**\n * Allow fire programmatically a cut action\n * @param {String|HTMLElement} target\n * @returns Text cutted.\n */\n\n }, {\n key: \"cut\",\n value: function cut(target) {\n return actions_cut(target);\n }\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: \"isSupported\",\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n return support;\n }\n }]);\n\n return Clipboard;\n}((tiny_emitter_default()));\n\n/* harmony default export */ var clipboard = (Clipboard);\n\n/***/ }),\n\n/***/ 828:\n/***/ (function(module) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n\n/***/ 438:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar closest = __webpack_require__(828);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n\n/***/ 879:\n/***/ (function(__unused_webpack_module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n\n/***/ 370:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar is = __webpack_require__(879);\nvar delegate = __webpack_require__(438);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n\n/***/ 817:\n/***/ (function(module) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n\n/***/ 279:\n/***/ (function(module) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(__webpack_module_cache__[moduleId]) {\n/******/ \t\t\treturn __webpack_module_cache__[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t!function() {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = function(module) {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\tfunction() { return module['default']; } :\n/******/ \t\t\t\tfunction() { return module; };\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t!function() {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = function(exports, definition) {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t!function() {\n/******/ \t\t__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }\n/******/ \t}();\n/******/ \t\n/************************************************************************/\n/******/ \t// module exports must be returned from runtime so entry inlining is disabled\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(686);\n/******/ })()\n.default;\n});", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"focus-visible\"\n\nimport {\n EMPTY,\n NEVER,\n Observable,\n Subject,\n defer,\n delay,\n filter,\n map,\n merge,\n mergeWith,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"./_\"\nimport {\n at,\n getActiveElement,\n getOptionalElement,\n requestJSON,\n setLocation,\n setToggle,\n watchDocument,\n watchKeyboard,\n watchLocation,\n watchLocationTarget,\n watchMedia,\n watchPrint,\n watchScript,\n watchViewport\n} from \"./browser\"\nimport {\n getComponentElement,\n getComponentElements,\n mountAnnounce,\n mountBackToTop,\n mountConsent,\n mountContent,\n mountDialog,\n mountHeader,\n mountHeaderTitle,\n mountPalette,\n mountProgress,\n mountSearch,\n mountSearchHiglight,\n mountSidebar,\n mountSource,\n mountTableOfContents,\n mountTabs,\n watchHeader,\n watchMain\n} from \"./components\"\nimport {\n SearchIndex,\n setupClipboardJS,\n setupInstantNavigation,\n setupVersionSelector\n} from \"./integrations\"\nimport {\n patchEllipsis,\n patchIndeterminate,\n patchScrollfix,\n patchScrolllock\n} from \"./patches\"\nimport \"./polyfills\"\n\n/* ----------------------------------------------------------------------------\n * Functions - @todo refactor\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch search index\n *\n * @returns Search index observable\n */\nfunction fetchSearchIndex(): Observable {\n if (location.protocol === \"file:\") {\n return watchScript(\n `${new URL(\"search/search_index.js\", config.base)}`\n )\n .pipe(\n // @ts-ignore - @todo fix typings\n map(() => __index),\n shareReplay(1)\n )\n } else {\n return requestJSON(\n new URL(\"search/search_index.json\", config.base)\n )\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Application\n * ------------------------------------------------------------------------- */\n\n/* Yay, JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Set up navigation observables and subjects */\nconst document$ = watchDocument()\nconst location$ = watchLocation()\nconst target$ = watchLocationTarget(location$)\nconst keyboard$ = watchKeyboard()\n\n/* Set up media observables */\nconst viewport$ = watchViewport()\nconst tablet$ = watchMedia(\"(min-width: 960px)\")\nconst screen$ = watchMedia(\"(min-width: 1220px)\")\nconst print$ = watchPrint()\n\n/* Retrieve search index, if search is enabled */\nconst config = configuration()\nconst index$ = document.forms.namedItem(\"search\")\n ? fetchSearchIndex()\n : NEVER\n\n/* Set up Clipboard.js integration */\nconst alert$ = new Subject()\nsetupClipboardJS({ alert$ })\n\n/* Set up progress indicator */\nconst progress$ = new Subject()\n\n/* Set up instant navigation, if enabled */\nif (feature(\"navigation.instant\"))\n setupInstantNavigation({ location$, viewport$, progress$ })\n .subscribe(document$)\n\n/* Set up version selector */\nif (config.version?.provider === \"mike\")\n setupVersionSelector({ document$ })\n\n/* Always close drawer and search on navigation */\nmerge(location$, target$)\n .pipe(\n delay(125)\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n setToggle(\"search\", false)\n })\n\n/* Set up global keyboard handlers */\nkeyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getOptionalElement(\"link[rel=prev]\")\n if (typeof prev !== \"undefined\")\n setLocation(prev)\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getOptionalElement(\"link[rel=next]\")\n if (typeof next !== \"undefined\")\n setLocation(next)\n break\n\n /* Expand navigation, see https://bit.ly/3ZjG5io */\n case \"Enter\":\n const active = getActiveElement()\n if (active instanceof HTMLLabelElement)\n active.click()\n }\n })\n\n/* Set up patches */\npatchEllipsis({ document$ })\npatchIndeterminate({ document$, tablet$ })\npatchScrollfix({ document$ })\npatchScrolllock({ viewport$, tablet$ })\n\n/* Set up header and main area observable */\nconst header$ = watchHeader(getComponentElement(\"header\"), { viewport$ })\nconst main$ = document$\n .pipe(\n map(() => getComponentElement(\"main\")),\n switchMap(el => watchMain(el, { viewport$, header$ })),\n shareReplay(1)\n )\n\n/* Set up control component observables */\nconst control$ = merge(\n\n /* Consent */\n ...getComponentElements(\"consent\")\n .map(el => mountConsent(el, { target$ })),\n\n /* Dialog */\n ...getComponentElements(\"dialog\")\n .map(el => mountDialog(el, { alert$ })),\n\n /* Header */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\n\n /* Color palette */\n ...getComponentElements(\"palette\")\n .map(el => mountPalette(el)),\n\n /* Progress bar */\n ...getComponentElements(\"progress\")\n .map(el => mountProgress(el, { progress$ })),\n\n /* Search */\n ...getComponentElements(\"search\")\n .map(el => mountSearch(el, { index$, keyboard$ })),\n\n /* Repository information */\n ...getComponentElements(\"source\")\n .map(el => mountSource(el))\n)\n\n/* Set up content component observables */\nconst content$ = defer(() => merge(\n\n /* Announcement bar */\n ...getComponentElements(\"announce\")\n .map(el => mountAnnounce(el)),\n\n /* Content */\n ...getComponentElements(\"content\")\n .map(el => mountContent(el, { viewport$, target$, print$ })),\n\n /* Search highlighting */\n ...getComponentElements(\"content\")\n .map(el => feature(\"search.highlight\")\n ? mountSearchHiglight(el, { index$, location$ })\n : EMPTY\n ),\n\n /* Header title */\n ...getComponentElements(\"header-title\")\n .map(el => mountHeaderTitle(el, { viewport$, header$ })),\n\n /* Sidebar */\n ...getComponentElements(\"sidebar\")\n .map(el => el.getAttribute(\"data-md-type\") === \"navigation\"\n ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))\n : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))\n ),\n\n /* Navigation tabs */\n ...getComponentElements(\"tabs\")\n .map(el => mountTabs(el, { viewport$, header$ })),\n\n /* Table of contents */\n ...getComponentElements(\"toc\")\n .map(el => mountTableOfContents(el, {\n viewport$, header$, main$, target$\n })),\n\n /* Back-to-top button */\n ...getComponentElements(\"top\")\n .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))\n))\n\n/* Set up component observables */\nconst component$ = document$\n .pipe(\n switchMap(() => content$),\n mergeWith(control$),\n shareReplay(1)\n )\n\n/* Subscribe to all components */\ncomponent$.subscribe()\n\n/* ----------------------------------------------------------------------------\n * Exports\n * ------------------------------------------------------------------------- */\n\nwindow.document$ = document$ /* Document observable */\nwindow.location$ = location$ /* Location subject */\nwindow.target$ = target$ /* Location target observable */\nwindow.keyboard$ = keyboard$ /* Keyboard observable */\nwindow.viewport$ = viewport$ /* Viewport observable */\nwindow.tablet$ = tablet$ /* Media tablet observable */\nwindow.screen$ = screen$ /* Media screen observable */\nwindow.print$ = print$ /* Media print observable */\nwindow.alert$ = alert$ /* Alert subject */\nwindow.progress$ = progress$ /* Progress indicator subject */\nwindow.component$ = component$ /* Component observable */\n", "/*! *****************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n if (typeof b !== \"function\" && b !== null)\r\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (_) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });\r\n}) : (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n});\r\n\r\nexport function __exportStar(m, o) {\r\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n}\r\n\r\nexport function __spreadArray(to, from, pack) {\r\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\r\n if (ar || !(i in from)) {\r\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\r\n ar[i] = from[i];\r\n }\r\n }\r\n return to.concat(ar || Array.prototype.slice.call(from));\r\n}\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === \"return\" } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nvar __setModuleDefault = Object.create ? (function(o, v) {\r\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\r\n}) : function(o, v) {\r\n o[\"default\"] = v;\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\r\n __setModuleDefault(result, mod);\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\r\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\r\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\r\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\r\n}\r\n", "/**\n * Returns true if the object is a function.\n * @param value The value to check\n */\nexport function isFunction(value: any): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "/**\n * Used to create Error subclasses until the community moves away from ES5.\n *\n * This is because compiling from TypeScript down to ES5 has issues with subclassing Errors\n * as well as other built-in types: https://github.com/Microsoft/TypeScript/issues/12123\n *\n * @param createImpl A factory function to create the actual constructor implementation. The returned\n * function should be a named function that calls `_super` internally.\n */\nexport function createErrorClass(createImpl: (_super: any) => any): T {\n const _super = (instance: any) => {\n Error.call(instance);\n instance.stack = new Error().stack;\n };\n\n const ctorFunc = createImpl(_super);\n ctorFunc.prototype = Object.create(Error.prototype);\n ctorFunc.prototype.constructor = ctorFunc;\n return ctorFunc;\n}\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface UnsubscriptionError extends Error {\n readonly errors: any[];\n}\n\nexport interface UnsubscriptionErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (errors: any[]): UnsubscriptionError;\n}\n\n/**\n * An error thrown when one or more errors have occurred during the\n * `unsubscribe` of a {@link Subscription}.\n */\nexport const UnsubscriptionError: UnsubscriptionErrorCtor = createErrorClass(\n (_super) =>\n function UnsubscriptionErrorImpl(this: any, errors: (Error | string)[]) {\n _super(this);\n this.message = errors\n ? `${errors.length} errors occurred during unsubscription:\n${errors.map((err, i) => `${i + 1}) ${err.toString()}`).join('\\n ')}`\n : '';\n this.name = 'UnsubscriptionError';\n this.errors = errors;\n }\n);\n", "/**\n * Removes an item from an array, mutating it.\n * @param arr The array to remove the item from\n * @param item The item to remove\n */\nexport function arrRemove(arr: T[] | undefined | null, item: T) {\n if (arr) {\n const index = arr.indexOf(item);\n 0 <= index && arr.splice(index, 1);\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { UnsubscriptionError } from './util/UnsubscriptionError';\nimport { SubscriptionLike, TeardownLogic, Unsubscribable } from './types';\nimport { arrRemove } from './util/arrRemove';\n\n/**\n * Represents a disposable resource, such as the execution of an Observable. A\n * Subscription has one important method, `unsubscribe`, that takes no argument\n * and just disposes the resource held by the subscription.\n *\n * Additionally, subscriptions may be grouped together through the `add()`\n * method, which will attach a child Subscription to the current Subscription.\n * When a Subscription is unsubscribed, all its children (and its grandchildren)\n * will be unsubscribed as well.\n *\n * @class Subscription\n */\nexport class Subscription implements SubscriptionLike {\n /** @nocollapse */\n public static EMPTY = (() => {\n const empty = new Subscription();\n empty.closed = true;\n return empty;\n })();\n\n /**\n * A flag to indicate whether this Subscription has already been unsubscribed.\n */\n public closed = false;\n\n private _parentage: Subscription[] | Subscription | null = null;\n\n /**\n * The list of registered finalizers to execute upon unsubscription. Adding and removing from this\n * list occurs in the {@link #add} and {@link #remove} methods.\n */\n private _finalizers: Exclude[] | null = null;\n\n /**\n * @param initialTeardown A function executed first as part of the finalization\n * process that is kicked off when {@link #unsubscribe} is called.\n */\n constructor(private initialTeardown?: () => void) {}\n\n /**\n * Disposes the resources held by the subscription. May, for instance, cancel\n * an ongoing Observable execution or cancel any other type of work that\n * started when the Subscription was created.\n * @return {void}\n */\n unsubscribe(): void {\n let errors: any[] | undefined;\n\n if (!this.closed) {\n this.closed = true;\n\n // Remove this from it's parents.\n const { _parentage } = this;\n if (_parentage) {\n this._parentage = null;\n if (Array.isArray(_parentage)) {\n for (const parent of _parentage) {\n parent.remove(this);\n }\n } else {\n _parentage.remove(this);\n }\n }\n\n const { initialTeardown: initialFinalizer } = this;\n if (isFunction(initialFinalizer)) {\n try {\n initialFinalizer();\n } catch (e) {\n errors = e instanceof UnsubscriptionError ? e.errors : [e];\n }\n }\n\n const { _finalizers } = this;\n if (_finalizers) {\n this._finalizers = null;\n for (const finalizer of _finalizers) {\n try {\n execFinalizer(finalizer);\n } catch (err) {\n errors = errors ?? [];\n if (err instanceof UnsubscriptionError) {\n errors = [...errors, ...err.errors];\n } else {\n errors.push(err);\n }\n }\n }\n }\n\n if (errors) {\n throw new UnsubscriptionError(errors);\n }\n }\n }\n\n /**\n * Adds a finalizer to this subscription, so that finalization will be unsubscribed/called\n * when this subscription is unsubscribed. If this subscription is already {@link #closed},\n * because it has already been unsubscribed, then whatever finalizer is passed to it\n * will automatically be executed (unless the finalizer itself is also a closed subscription).\n *\n * Closed Subscriptions cannot be added as finalizers to any subscription. Adding a closed\n * subscription to a any subscription will result in no operation. (A noop).\n *\n * Adding a subscription to itself, or adding `null` or `undefined` will not perform any\n * operation at all. (A noop).\n *\n * `Subscription` instances that are added to this instance will automatically remove themselves\n * if they are unsubscribed. Functions and {@link Unsubscribable} objects that you wish to remove\n * will need to be removed manually with {@link #remove}\n *\n * @param teardown The finalization logic to add to this subscription.\n */\n add(teardown: TeardownLogic): void {\n // Only add the finalizer if it's not undefined\n // and don't add a subscription to itself.\n if (teardown && teardown !== this) {\n if (this.closed) {\n // If this subscription is already closed,\n // execute whatever finalizer is handed to it automatically.\n execFinalizer(teardown);\n } else {\n if (teardown instanceof Subscription) {\n // We don't add closed subscriptions, and we don't add the same subscription\n // twice. Subscription unsubscribe is idempotent.\n if (teardown.closed || teardown._hasParent(this)) {\n return;\n }\n teardown._addParent(this);\n }\n (this._finalizers = this._finalizers ?? []).push(teardown);\n }\n }\n }\n\n /**\n * Checks to see if a this subscription already has a particular parent.\n * This will signal that this subscription has already been added to the parent in question.\n * @param parent the parent to check for\n */\n private _hasParent(parent: Subscription) {\n const { _parentage } = this;\n return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));\n }\n\n /**\n * Adds a parent to this subscription so it can be removed from the parent if it\n * unsubscribes on it's own.\n *\n * NOTE: THIS ASSUMES THAT {@link _hasParent} HAS ALREADY BEEN CHECKED.\n * @param parent The parent subscription to add\n */\n private _addParent(parent: Subscription) {\n const { _parentage } = this;\n this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;\n }\n\n /**\n * Called on a child when it is removed via {@link #remove}.\n * @param parent The parent to remove\n */\n private _removeParent(parent: Subscription) {\n const { _parentage } = this;\n if (_parentage === parent) {\n this._parentage = null;\n } else if (Array.isArray(_parentage)) {\n arrRemove(_parentage, parent);\n }\n }\n\n /**\n * Removes a finalizer from this subscription that was previously added with the {@link #add} method.\n *\n * Note that `Subscription` instances, when unsubscribed, will automatically remove themselves\n * from every other `Subscription` they have been added to. This means that using the `remove` method\n * is not a common thing and should be used thoughtfully.\n *\n * If you add the same finalizer instance of a function or an unsubscribable object to a `Subscription` instance\n * more than once, you will need to call `remove` the same number of times to remove all instances.\n *\n * All finalizer instances are removed to free up memory upon unsubscription.\n *\n * @param teardown The finalizer to remove from this subscription\n */\n remove(teardown: Exclude): void {\n const { _finalizers } = this;\n _finalizers && arrRemove(_finalizers, teardown);\n\n if (teardown instanceof Subscription) {\n teardown._removeParent(this);\n }\n }\n}\n\nexport const EMPTY_SUBSCRIPTION = Subscription.EMPTY;\n\nexport function isSubscription(value: any): value is Subscription {\n return (\n value instanceof Subscription ||\n (value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe))\n );\n}\n\nfunction execFinalizer(finalizer: Unsubscribable | (() => void)) {\n if (isFunction(finalizer)) {\n finalizer();\n } else {\n finalizer.unsubscribe();\n }\n}\n", "import { Subscriber } from './Subscriber';\nimport { ObservableNotification } from './types';\n\n/**\n * The {@link GlobalConfig} object for RxJS. It is used to configure things\n * like how to react on unhandled errors.\n */\nexport const config: GlobalConfig = {\n onUnhandledError: null,\n onStoppedNotification: null,\n Promise: undefined,\n useDeprecatedSynchronousErrorHandling: false,\n useDeprecatedNextContext: false,\n};\n\n/**\n * The global configuration object for RxJS, used to configure things\n * like how to react on unhandled errors. Accessible via {@link config}\n * object.\n */\nexport interface GlobalConfig {\n /**\n * A registration point for unhandled errors from RxJS. These are errors that\n * cannot were not handled by consuming code in the usual subscription path. For\n * example, if you have this configured, and you subscribe to an observable without\n * providing an error handler, errors from that subscription will end up here. This\n * will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onUnhandledError: ((err: any) => void) | null;\n\n /**\n * A registration point for notifications that cannot be sent to subscribers because they\n * have completed, errored or have been explicitly unsubscribed. By default, next, complete\n * and error notifications sent to stopped subscribers are noops. However, sometimes callers\n * might want a different behavior. For example, with sources that attempt to report errors\n * to stopped subscribers, a caller can configure RxJS to throw an unhandled error instead.\n * This will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onStoppedNotification: ((notification: ObservableNotification, subscriber: Subscriber) => void) | null;\n\n /**\n * The promise constructor used by default for {@link Observable#toPromise toPromise} and {@link Observable#forEach forEach}\n * methods.\n *\n * @deprecated As of version 8, RxJS will no longer support this sort of injection of a\n * Promise constructor. If you need a Promise implementation other than native promises,\n * please polyfill/patch Promise as you see appropriate. Will be removed in v8.\n */\n Promise?: PromiseConstructorLike;\n\n /**\n * If true, turns on synchronous error rethrowing, which is a deprecated behavior\n * in v6 and higher. This behavior enables bad patterns like wrapping a subscribe\n * call in a try/catch block. It also enables producer interference, a nasty bug\n * where a multicast can be broken for all observers by a downstream consumer with\n * an unhandled error. DO NOT USE THIS FLAG UNLESS IT'S NEEDED TO BUY TIME\n * FOR MIGRATION REASONS.\n *\n * @deprecated As of version 8, RxJS will no longer support synchronous throwing\n * of unhandled errors. All errors will be thrown on a separate call stack to prevent bad\n * behaviors described above. Will be removed in v8.\n */\n useDeprecatedSynchronousErrorHandling: boolean;\n\n /**\n * If true, enables an as-of-yet undocumented feature from v5: The ability to access\n * `unsubscribe()` via `this` context in `next` functions created in observers passed\n * to `subscribe`.\n *\n * This is being removed because the performance was severely problematic, and it could also cause\n * issues when types other than POJOs are passed to subscribe as subscribers, as they will likely have\n * their `this` context overwritten.\n *\n * @deprecated As of version 8, RxJS will no longer support altering the\n * context of next functions provided as part of an observer to Subscribe. Instead,\n * you will have access to a subscription or a signal or token that will allow you to do things like\n * unsubscribe and test closed status. Will be removed in v8.\n */\n useDeprecatedNextContext: boolean;\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetTimeoutFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearTimeoutFunction = (handle: TimerHandle) => void;\n\ninterface TimeoutProvider {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n delegate:\n | {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n }\n | undefined;\n}\n\nexport const timeoutProvider: TimeoutProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setTimeout(handler: () => void, timeout?: number, ...args) {\n const { delegate } = timeoutProvider;\n if (delegate?.setTimeout) {\n return delegate.setTimeout(handler, timeout, ...args);\n }\n return setTimeout(handler, timeout, ...args);\n },\n clearTimeout(handle) {\n const { delegate } = timeoutProvider;\n return (delegate?.clearTimeout || clearTimeout)(handle as any);\n },\n delegate: undefined,\n};\n", "import { config } from '../config';\nimport { timeoutProvider } from '../scheduler/timeoutProvider';\n\n/**\n * Handles an error on another job either with the user-configured {@link onUnhandledError},\n * or by throwing it on that new job so it can be picked up by `window.onerror`, `process.on('error')`, etc.\n *\n * This should be called whenever there is an error that is out-of-band with the subscription\n * or when an error hits a terminal boundary of the subscription and no error handler was provided.\n *\n * @param err the error to report\n */\nexport function reportUnhandledError(err: any) {\n timeoutProvider.setTimeout(() => {\n const { onUnhandledError } = config;\n if (onUnhandledError) {\n // Execute the user-configured error handler.\n onUnhandledError(err);\n } else {\n // Throw so it is picked up by the runtime's uncaught error mechanism.\n throw err;\n }\n });\n}\n", "/* tslint:disable:no-empty */\nexport function noop() { }\n", "import { CompleteNotification, NextNotification, ErrorNotification } from './types';\n\n/**\n * A completion object optimized for memory use and created to be the\n * same \"shape\" as other notifications in v8.\n * @internal\n */\nexport const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)();\n\n/**\n * Internal use only. Creates an optimized error notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function errorNotification(error: any): ErrorNotification {\n return createNotification('E', undefined, error) as any;\n}\n\n/**\n * Internal use only. Creates an optimized next notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function nextNotification(value: T) {\n return createNotification('N', value, undefined) as NextNotification;\n}\n\n/**\n * Ensures that all notifications created internally have the same \"shape\" in v8.\n *\n * TODO: This is only exported to support a crazy legacy test in `groupBy`.\n * @internal\n */\nexport function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) {\n return {\n kind,\n value,\n error,\n };\n}\n", "import { config } from '../config';\n\nlet context: { errorThrown: boolean; error: any } | null = null;\n\n/**\n * Handles dealing with errors for super-gross mode. Creates a context, in which\n * any synchronously thrown errors will be passed to {@link captureError}. Which\n * will record the error such that it will be rethrown after the call back is complete.\n * TODO: Remove in v8\n * @param cb An immediately executed function.\n */\nexport function errorContext(cb: () => void) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n const isRoot = !context;\n if (isRoot) {\n context = { errorThrown: false, error: null };\n }\n cb();\n if (isRoot) {\n const { errorThrown, error } = context!;\n context = null;\n if (errorThrown) {\n throw error;\n }\n }\n } else {\n // This is the general non-deprecated path for everyone that\n // isn't crazy enough to use super-gross mode (useDeprecatedSynchronousErrorHandling)\n cb();\n }\n}\n\n/**\n * Captures errors only in super-gross mode.\n * @param err the error to capture\n */\nexport function captureError(err: any) {\n if (config.useDeprecatedSynchronousErrorHandling && context) {\n context.errorThrown = true;\n context.error = err;\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { Observer, ObservableNotification } from './types';\nimport { isSubscription, Subscription } from './Subscription';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nimport { noop } from './util/noop';\nimport { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories';\nimport { timeoutProvider } from './scheduler/timeoutProvider';\nimport { captureError } from './util/errorContext';\n\n/**\n * Implements the {@link Observer} interface and extends the\n * {@link Subscription} class. While the {@link Observer} is the public API for\n * consuming the values of an {@link Observable}, all Observers get converted to\n * a Subscriber, in order to provide Subscription-like capabilities such as\n * `unsubscribe`. Subscriber is a common type in RxJS, and crucial for\n * implementing operators, but it is rarely used as a public API.\n *\n * @class Subscriber\n */\nexport class Subscriber extends Subscription implements Observer {\n /**\n * A static factory for a Subscriber, given a (potentially partial) definition\n * of an Observer.\n * @param next The `next` callback of an Observer.\n * @param error The `error` callback of an\n * Observer.\n * @param complete The `complete` callback of an\n * Observer.\n * @return A Subscriber wrapping the (partially defined)\n * Observer represented by the given arguments.\n * @nocollapse\n * @deprecated Do not use. Will be removed in v8. There is no replacement for this\n * method, and there is no reason to be creating instances of `Subscriber` directly.\n * If you have a specific use case, please file an issue.\n */\n static create(next?: (x?: T) => void, error?: (e?: any) => void, complete?: () => void): Subscriber {\n return new SafeSubscriber(next, error, complete);\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected isStopped: boolean = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected destination: Subscriber | Observer; // this `any` is the escape hatch to erase extra type param (e.g. R)\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * There is no reason to directly create an instance of Subscriber. This type is exported for typings reasons.\n */\n constructor(destination?: Subscriber | Observer) {\n super();\n if (destination) {\n this.destination = destination;\n // Automatically chain subscriptions together here.\n // if destination is a Subscription, then it is a Subscriber.\n if (isSubscription(destination)) {\n destination.add(this);\n }\n } else {\n this.destination = EMPTY_OBSERVER;\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `next` from\n * the Observable, with a value. The Observable may call this method 0 or more\n * times.\n * @param {T} [value] The `next` value.\n * @return {void}\n */\n next(value?: T): void {\n if (this.isStopped) {\n handleStoppedNotification(nextNotification(value), this);\n } else {\n this._next(value!);\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `error` from\n * the Observable, with an attached `Error`. Notifies the Observer that\n * the Observable has experienced an error condition.\n * @param {any} [err] The `error` exception.\n * @return {void}\n */\n error(err?: any): void {\n if (this.isStopped) {\n handleStoppedNotification(errorNotification(err), this);\n } else {\n this.isStopped = true;\n this._error(err);\n }\n }\n\n /**\n * The {@link Observer} callback to receive a valueless notification of type\n * `complete` from the Observable. Notifies the Observer that the Observable\n * has finished sending push-based notifications.\n * @return {void}\n */\n complete(): void {\n if (this.isStopped) {\n handleStoppedNotification(COMPLETE_NOTIFICATION, this);\n } else {\n this.isStopped = true;\n this._complete();\n }\n }\n\n unsubscribe(): void {\n if (!this.closed) {\n this.isStopped = true;\n super.unsubscribe();\n this.destination = null!;\n }\n }\n\n protected _next(value: T): void {\n this.destination.next(value);\n }\n\n protected _error(err: any): void {\n try {\n this.destination.error(err);\n } finally {\n this.unsubscribe();\n }\n }\n\n protected _complete(): void {\n try {\n this.destination.complete();\n } finally {\n this.unsubscribe();\n }\n }\n}\n\n/**\n * This bind is captured here because we want to be able to have\n * compatibility with monoid libraries that tend to use a method named\n * `bind`. In particular, a library called Monio requires this.\n */\nconst _bind = Function.prototype.bind;\n\nfunction bind any>(fn: Fn, thisArg: any): Fn {\n return _bind.call(fn, thisArg);\n}\n\n/**\n * Internal optimization only, DO NOT EXPOSE.\n * @internal\n */\nclass ConsumerObserver implements Observer {\n constructor(private partialObserver: Partial>) {}\n\n next(value: T): void {\n const { partialObserver } = this;\n if (partialObserver.next) {\n try {\n partialObserver.next(value);\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n\n error(err: any): void {\n const { partialObserver } = this;\n if (partialObserver.error) {\n try {\n partialObserver.error(err);\n } catch (error) {\n handleUnhandledError(error);\n }\n } else {\n handleUnhandledError(err);\n }\n }\n\n complete(): void {\n const { partialObserver } = this;\n if (partialObserver.complete) {\n try {\n partialObserver.complete();\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n}\n\nexport class SafeSubscriber extends Subscriber {\n constructor(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((e?: any) => void) | null,\n complete?: (() => void) | null\n ) {\n super();\n\n let partialObserver: Partial>;\n if (isFunction(observerOrNext) || !observerOrNext) {\n // The first argument is a function, not an observer. The next\n // two arguments *could* be observers, or they could be empty.\n partialObserver = {\n next: (observerOrNext ?? undefined) as (((value: T) => void) | undefined),\n error: error ?? undefined,\n complete: complete ?? undefined,\n };\n } else {\n // The first argument is a partial observer.\n let context: any;\n if (this && config.useDeprecatedNextContext) {\n // This is a deprecated path that made `this.unsubscribe()` available in\n // next handler functions passed to subscribe. This only exists behind a flag\n // now, as it is *very* slow.\n context = Object.create(observerOrNext);\n context.unsubscribe = () => this.unsubscribe();\n partialObserver = {\n next: observerOrNext.next && bind(observerOrNext.next, context),\n error: observerOrNext.error && bind(observerOrNext.error, context),\n complete: observerOrNext.complete && bind(observerOrNext.complete, context),\n };\n } else {\n // The \"normal\" path. Just use the partial observer directly.\n partialObserver = observerOrNext;\n }\n }\n\n // Wrap the partial observer to ensure it's a full observer, and\n // make sure proper error handling is accounted for.\n this.destination = new ConsumerObserver(partialObserver);\n }\n}\n\nfunction handleUnhandledError(error: any) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n captureError(error);\n } else {\n // Ideal path, we report this as an unhandled error,\n // which is thrown on a new call stack.\n reportUnhandledError(error);\n }\n}\n\n/**\n * An error handler used when no error handler was supplied\n * to the SafeSubscriber -- meaning no error handler was supplied\n * do the `subscribe` call on our observable.\n * @param err The error to handle\n */\nfunction defaultErrorHandler(err: any) {\n throw err;\n}\n\n/**\n * A handler for notifications that cannot be sent to a stopped subscriber.\n * @param notification The notification being sent\n * @param subscriber The stopped subscriber\n */\nfunction handleStoppedNotification(notification: ObservableNotification, subscriber: Subscriber) {\n const { onStoppedNotification } = config;\n onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber));\n}\n\n/**\n * The observer used as a stub for subscriptions where the user did not\n * pass any arguments to `subscribe`. Comes with the default error handling\n * behavior.\n */\nexport const EMPTY_OBSERVER: Readonly> & { closed: true } = {\n closed: true,\n next: noop,\n error: defaultErrorHandler,\n complete: noop,\n};\n", "/**\n * Symbol.observable or a string \"@@observable\". Used for interop\n *\n * @deprecated We will no longer be exporting this symbol in upcoming versions of RxJS.\n * Instead polyfill and use Symbol.observable directly *or* use https://www.npmjs.com/package/symbol-observable\n */\nexport const observable: string | symbol = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();\n", "/**\n * This function takes one parameter and just returns it. Simply put,\n * this is like `(x: T): T => x`.\n *\n * ## Examples\n *\n * This is useful in some cases when using things like `mergeMap`\n *\n * ```ts\n * import { interval, take, map, range, mergeMap, identity } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(5));\n *\n * const result$ = source$.pipe(\n * map(i => range(i)),\n * mergeMap(identity) // same as mergeMap(x => x)\n * );\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * Or when you want to selectively apply an operator\n *\n * ```ts\n * import { interval, take, identity } from 'rxjs';\n *\n * const shouldLimit = () => Math.random() < 0.5;\n *\n * const source$ = interval(1000);\n *\n * const result$ = source$.pipe(shouldLimit() ? take(5) : identity);\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * @param x Any value that is returned by this function\n * @returns The value passed as the first parameter to this function\n */\nexport function identity(x: T): T {\n return x;\n}\n", "import { identity } from './identity';\nimport { UnaryFunction } from '../types';\n\nexport function pipe(): typeof identity;\nexport function pipe(fn1: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction, fn3: UnaryFunction): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction,\n ...fns: UnaryFunction[]\n): UnaryFunction;\n\n/**\n * pipe() can be called on one or more functions, each of which can take one argument (\"UnaryFunction\")\n * and uses it to return a value.\n * It returns a function that takes one argument, passes it to the first UnaryFunction, and then\n * passes the result to the next one, passes that result to the next one, and so on. \n */\nexport function pipe(...fns: Array>): UnaryFunction {\n return pipeFromArray(fns);\n}\n\n/** @internal */\nexport function pipeFromArray(fns: Array>): UnaryFunction {\n if (fns.length === 0) {\n return identity as UnaryFunction;\n }\n\n if (fns.length === 1) {\n return fns[0];\n }\n\n return function piped(input: T): R {\n return fns.reduce((prev: any, fn: UnaryFunction) => fn(prev), input as any);\n };\n}\n", "import { Operator } from './Operator';\nimport { SafeSubscriber, Subscriber } from './Subscriber';\nimport { isSubscription, Subscription } from './Subscription';\nimport { TeardownLogic, OperatorFunction, Subscribable, Observer } from './types';\nimport { observable as Symbol_observable } from './symbol/observable';\nimport { pipeFromArray } from './util/pipe';\nimport { config } from './config';\nimport { isFunction } from './util/isFunction';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A representation of any set of values over any amount of time. This is the most basic building block\n * of RxJS.\n *\n * @class Observable\n */\nexport class Observable implements Subscribable {\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n source: Observable | undefined;\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n operator: Operator | undefined;\n\n /**\n * @constructor\n * @param {Function} subscribe the function that is called when the Observable is\n * initially subscribed to. This function is given a Subscriber, to which new values\n * can be `next`ed, or an `error` method can be called to raise an error, or\n * `complete` can be called to notify of a successful completion.\n */\n constructor(subscribe?: (this: Observable, subscriber: Subscriber) => TeardownLogic) {\n if (subscribe) {\n this._subscribe = subscribe;\n }\n }\n\n // HACK: Since TypeScript inherits static properties too, we have to\n // fight against TypeScript here so Subject can have a different static create signature\n /**\n * Creates a new Observable by calling the Observable constructor\n * @owner Observable\n * @method create\n * @param {Function} subscribe? the subscriber function to be passed to the Observable constructor\n * @return {Observable} a new observable\n * @nocollapse\n * @deprecated Use `new Observable()` instead. Will be removed in v8.\n */\n static create: (...args: any[]) => any = (subscribe?: (subscriber: Subscriber) => TeardownLogic) => {\n return new Observable(subscribe);\n };\n\n /**\n * Creates a new Observable, with this Observable instance as the source, and the passed\n * operator defined as the new observable's operator.\n * @method lift\n * @param operator the operator defining the operation to take on the observable\n * @return a new observable with the Operator applied\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * If you have implemented an operator using `lift`, it is recommended that you create an\n * operator by simply returning `new Observable()` directly. See \"Creating new operators from\n * scratch\" section here: https://rxjs.dev/guide/operators\n */\n lift(operator?: Operator): Observable {\n const observable = new Observable();\n observable.source = this;\n observable.operator = operator;\n return observable;\n }\n\n subscribe(observerOrNext?: Partial> | ((value: T) => void)): Subscription;\n /** @deprecated Instead of passing separate callback arguments, use an observer argument. Signatures taking separate callback arguments will be removed in v8. Details: https://rxjs.dev/deprecations/subscribe-arguments */\n subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;\n /**\n * Invokes an execution of an Observable and registers Observer handlers for notifications it will emit.\n *\n * Use it when you have all these Observables, but still nothing is happening.\n *\n * `subscribe` is not a regular operator, but a method that calls Observable's internal `subscribe` function. It\n * might be for example a function that you passed to Observable's constructor, but most of the time it is\n * a library implementation, which defines what will be emitted by an Observable, and when it be will emitted. This means\n * that calling `subscribe` is actually the moment when Observable starts its work, not when it is created, as it is often\n * the thought.\n *\n * Apart from starting the execution of an Observable, this method allows you to listen for values\n * that an Observable emits, as well as for when it completes or errors. You can achieve this in two\n * of the following ways.\n *\n * The first way is creating an object that implements {@link Observer} interface. It should have methods\n * defined by that interface, but note that it should be just a regular JavaScript object, which you can create\n * yourself in any way you want (ES6 class, classic function constructor, object literal etc.). In particular, do\n * not attempt to use any RxJS implementation details to create Observers - you don't need them. Remember also\n * that your object does not have to implement all methods. If you find yourself creating a method that doesn't\n * do anything, you can simply omit it. Note however, if the `error` method is not provided and an error happens,\n * it will be thrown asynchronously. Errors thrown asynchronously cannot be caught using `try`/`catch`. Instead,\n * use the {@link onUnhandledError} configuration option or use a runtime handler (like `window.onerror` or\n * `process.on('error)`) to be notified of unhandled errors. Because of this, it's recommended that you provide\n * an `error` method to avoid missing thrown errors.\n *\n * The second way is to give up on Observer object altogether and simply provide callback functions in place of its methods.\n * This means you can provide three functions as arguments to `subscribe`, where the first function is equivalent\n * of a `next` method, the second of an `error` method and the third of a `complete` method. Just as in case of an Observer,\n * if you do not need to listen for something, you can omit a function by passing `undefined` or `null`,\n * since `subscribe` recognizes these functions by where they were placed in function call. When it comes\n * to the `error` function, as with an Observer, if not provided, errors emitted by an Observable will be thrown asynchronously.\n *\n * You can, however, subscribe with no parameters at all. This may be the case where you're not interested in terminal events\n * and you also handled emissions internally by using operators (e.g. using `tap`).\n *\n * Whichever style of calling `subscribe` you use, in both cases it returns a Subscription object.\n * This object allows you to call `unsubscribe` on it, which in turn will stop the work that an Observable does and will clean\n * up all resources that an Observable used. Note that cancelling a subscription will not call `complete` callback\n * provided to `subscribe` function, which is reserved for a regular completion signal that comes from an Observable.\n *\n * Remember that callbacks provided to `subscribe` are not guaranteed to be called asynchronously.\n * It is an Observable itself that decides when these functions will be called. For example {@link of}\n * by default emits all its values synchronously. Always check documentation for how given Observable\n * will behave when subscribed and if its default behavior can be modified with a `scheduler`.\n *\n * #### Examples\n *\n * Subscribe with an {@link guide/observer Observer}\n *\n * ```ts\n * import { of } from 'rxjs';\n *\n * const sumObserver = {\n * sum: 0,\n * next(value) {\n * console.log('Adding: ' + value);\n * this.sum = this.sum + value;\n * },\n * error() {\n * // We actually could just remove this method,\n * // since we do not really care about errors right now.\n * },\n * complete() {\n * console.log('Sum equals: ' + this.sum);\n * }\n * };\n *\n * of(1, 2, 3) // Synchronously emits 1, 2, 3 and then completes.\n * .subscribe(sumObserver);\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Subscribe with functions ({@link deprecations/subscribe-arguments deprecated})\n *\n * ```ts\n * import { of } from 'rxjs'\n *\n * let sum = 0;\n *\n * of(1, 2, 3).subscribe(\n * value => {\n * console.log('Adding: ' + value);\n * sum = sum + value;\n * },\n * undefined,\n * () => console.log('Sum equals: ' + sum)\n * );\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Cancel a subscription\n *\n * ```ts\n * import { interval } from 'rxjs';\n *\n * const subscription = interval(1000).subscribe({\n * next(num) {\n * console.log(num)\n * },\n * complete() {\n * // Will not be called, even when cancelling subscription.\n * console.log('completed!');\n * }\n * });\n *\n * setTimeout(() => {\n * subscription.unsubscribe();\n * console.log('unsubscribed!');\n * }, 2500);\n *\n * // Logs:\n * // 0 after 1s\n * // 1 after 2s\n * // 'unsubscribed!' after 2.5s\n * ```\n *\n * @param {Observer|Function} observerOrNext (optional) Either an observer with methods to be called,\n * or the first of three possible handlers, which is the handler for each value emitted from the subscribed\n * Observable.\n * @param {Function} error (optional) A handler for a terminal event resulting from an error. If no error handler is provided,\n * the error will be thrown asynchronously as unhandled.\n * @param {Function} complete (optional) A handler for a terminal event resulting from successful completion.\n * @return {Subscription} a subscription reference to the registered handlers\n * @method subscribe\n */\n subscribe(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((error: any) => void) | null,\n complete?: (() => void) | null\n ): Subscription {\n const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);\n\n errorContext(() => {\n const { operator, source } = this;\n subscriber.add(\n operator\n ? // We're dealing with a subscription in the\n // operator chain to one of our lifted operators.\n operator.call(subscriber, source)\n : source\n ? // If `source` has a value, but `operator` does not, something that\n // had intimate knowledge of our API, like our `Subject`, must have\n // set it. We're going to just call `_subscribe` directly.\n this._subscribe(subscriber)\n : // In all other cases, we're likely wrapping a user-provided initializer\n // function, so we need to catch errors and handle them appropriately.\n this._trySubscribe(subscriber)\n );\n });\n\n return subscriber;\n }\n\n /** @internal */\n protected _trySubscribe(sink: Subscriber): TeardownLogic {\n try {\n return this._subscribe(sink);\n } catch (err) {\n // We don't need to return anything in this case,\n // because it's just going to try to `add()` to a subscription\n // above.\n sink.error(err);\n }\n }\n\n /**\n * Used as a NON-CANCELLABLE means of subscribing to an observable, for use with\n * APIs that expect promises, like `async/await`. You cannot unsubscribe from this.\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * #### Example\n *\n * ```ts\n * import { interval, take } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(4));\n *\n * async function getTotal() {\n * let total = 0;\n *\n * await source$.forEach(value => {\n * total += value;\n * console.log('observable -> ' + value);\n * });\n *\n * return total;\n * }\n *\n * getTotal().then(\n * total => console.log('Total: ' + total)\n * );\n *\n * // Expected:\n * // 'observable -> 0'\n * // 'observable -> 1'\n * // 'observable -> 2'\n * // 'observable -> 3'\n * // 'Total: 6'\n * ```\n *\n * @param next a handler for each value emitted by the observable\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n */\n forEach(next: (value: T) => void): Promise;\n\n /**\n * @param next a handler for each value emitted by the observable\n * @param promiseCtor a constructor function used to instantiate the Promise\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n * @deprecated Passing a Promise constructor will no longer be available\n * in upcoming versions of RxJS. This is because it adds weight to the library, for very\n * little benefit. If you need this functionality, it is recommended that you either\n * polyfill Promise, or you create an adapter to convert the returned native promise\n * to whatever promise implementation you wanted. Will be removed in v8.\n */\n forEach(next: (value: T) => void, promiseCtor: PromiseConstructorLike): Promise;\n\n forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n const subscriber = new SafeSubscriber({\n next: (value) => {\n try {\n next(value);\n } catch (err) {\n reject(err);\n subscriber.unsubscribe();\n }\n },\n error: reject,\n complete: resolve,\n });\n this.subscribe(subscriber);\n }) as Promise;\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): TeardownLogic {\n return this.source?.subscribe(subscriber);\n }\n\n /**\n * An interop point defined by the es7-observable spec https://github.com/zenparsing/es-observable\n * @method Symbol.observable\n * @return {Observable} this instance of the observable\n */\n [Symbol_observable]() {\n return this;\n }\n\n /* tslint:disable:max-line-length */\n pipe(): Observable;\n pipe(op1: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction,\n ...operations: OperatorFunction[]\n ): Observable;\n /* tslint:enable:max-line-length */\n\n /**\n * Used to stitch together functional operators into a chain.\n * @method pipe\n * @return {Observable} the Observable result of all of the operators having\n * been called in the order they were passed in.\n *\n * ## Example\n *\n * ```ts\n * import { interval, filter, map, scan } from 'rxjs';\n *\n * interval(1000)\n * .pipe(\n * filter(x => x % 2 === 0),\n * map(x => x + x),\n * scan((acc, x) => acc + x)\n * )\n * .subscribe(x => console.log(x));\n * ```\n */\n pipe(...operations: OperatorFunction[]): Observable {\n return pipeFromArray(operations)(this);\n }\n\n /* tslint:disable:max-line-length */\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: typeof Promise): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: PromiseConstructorLike): Promise;\n /* tslint:enable:max-line-length */\n\n /**\n * Subscribe to this Observable and get a Promise resolving on\n * `complete` with the last emission (if any).\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * @method toPromise\n * @param [promiseCtor] a constructor function used to instantiate\n * the Promise\n * @return A Promise that resolves with the last value emit, or\n * rejects on an error. If there were no emissions, Promise\n * resolves with undefined.\n * @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise\n */\n toPromise(promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n let value: T | undefined;\n this.subscribe(\n (x: T) => (value = x),\n (err: any) => reject(err),\n () => resolve(value)\n );\n }) as Promise;\n }\n}\n\n/**\n * Decides between a passed promise constructor from consuming code,\n * A default configured promise constructor, and the native promise\n * constructor and returns it. If nothing can be found, it will throw\n * an error.\n * @param promiseCtor The optional promise constructor to passed by consuming code\n */\nfunction getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) {\n return promiseCtor ?? config.Promise ?? Promise;\n}\n\nfunction isObserver(value: any): value is Observer {\n return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);\n}\n\nfunction isSubscriber(value: any): value is Subscriber {\n return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));\n}\n", "import { Observable } from '../Observable';\nimport { Subscriber } from '../Subscriber';\nimport { OperatorFunction } from '../types';\nimport { isFunction } from './isFunction';\n\n/**\n * Used to determine if an object is an Observable with a lift function.\n */\nexport function hasLift(source: any): source is { lift: InstanceType['lift'] } {\n return isFunction(source?.lift);\n}\n\n/**\n * Creates an `OperatorFunction`. Used to define operators throughout the library in a concise way.\n * @param init The logic to connect the liftedSource to the subscriber at the moment of subscription.\n */\nexport function operate(\n init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void\n): OperatorFunction {\n return (source: Observable) => {\n if (hasLift(source)) {\n return source.lift(function (this: Subscriber, liftedSource: Observable) {\n try {\n return init(liftedSource, this);\n } catch (err) {\n this.error(err);\n }\n });\n }\n throw new TypeError('Unable to lift unknown Observable type');\n };\n}\n", "import { Subscriber } from '../Subscriber';\n\n/**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional teardown logic here. This will only be called on teardown if the\n * subscriber itself is not already closed. This is called after all other teardown logic is executed.\n */\nexport function createOperatorSubscriber(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n onFinalize?: () => void\n): Subscriber {\n return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize);\n}\n\n/**\n * A generic helper for allowing operators to be created with a Subscriber and\n * use closures to capture necessary state from the operator function itself.\n */\nexport class OperatorSubscriber extends Subscriber {\n /**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional finalization logic here. This will only be called on finalization if the\n * subscriber itself is not already closed. This is called after all other finalization logic is executed.\n * @param shouldUnsubscribe An optional check to see if an unsubscribe call should truly unsubscribe.\n * NOTE: This currently **ONLY** exists to support the strange behavior of {@link groupBy}, where unsubscription\n * to the resulting observable does not actually disconnect from the source if there are active subscriptions\n * to any grouped observable. (DO NOT EXPOSE OR USE EXTERNALLY!!!)\n */\n constructor(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n private onFinalize?: () => void,\n private shouldUnsubscribe?: () => boolean\n ) {\n // It's important - for performance reasons - that all of this class's\n // members are initialized and that they are always initialized in the same\n // order. This will ensure that all OperatorSubscriber instances have the\n // same hidden class in V8. This, in turn, will help keep the number of\n // hidden classes involved in property accesses within the base class as\n // low as possible. If the number of hidden classes involved exceeds four,\n // the property accesses will become megamorphic and performance penalties\n // will be incurred - i.e. inline caches won't be used.\n //\n // The reasons for ensuring all instances have the same hidden class are\n // further discussed in this blog post from Benedikt Meurer:\n // https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/\n super(destination);\n this._next = onNext\n ? function (this: OperatorSubscriber, value: T) {\n try {\n onNext(value);\n } catch (err) {\n destination.error(err);\n }\n }\n : super._next;\n this._error = onError\n ? function (this: OperatorSubscriber, err: any) {\n try {\n onError(err);\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._error;\n this._complete = onComplete\n ? function (this: OperatorSubscriber) {\n try {\n onComplete();\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._complete;\n }\n\n unsubscribe() {\n if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) {\n const { closed } = this;\n super.unsubscribe();\n // Execute additional teardown if we have any and we didn't already do so.\n !closed && this.onFinalize?.();\n }\n }\n}\n", "import { Subscription } from '../Subscription';\n\ninterface AnimationFrameProvider {\n schedule(callback: FrameRequestCallback): Subscription;\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n delegate:\n | {\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n }\n | undefined;\n}\n\nexport const animationFrameProvider: AnimationFrameProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n schedule(callback) {\n let request = requestAnimationFrame;\n let cancel: typeof cancelAnimationFrame | undefined = cancelAnimationFrame;\n const { delegate } = animationFrameProvider;\n if (delegate) {\n request = delegate.requestAnimationFrame;\n cancel = delegate.cancelAnimationFrame;\n }\n const handle = request((timestamp) => {\n // Clear the cancel function. The request has been fulfilled, so\n // attempting to cancel the request upon unsubscription would be\n // pointless.\n cancel = undefined;\n callback(timestamp);\n });\n return new Subscription(() => cancel?.(handle));\n },\n requestAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.requestAnimationFrame || requestAnimationFrame)(...args);\n },\n cancelAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.cancelAnimationFrame || cancelAnimationFrame)(...args);\n },\n delegate: undefined,\n};\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface ObjectUnsubscribedError extends Error {}\n\nexport interface ObjectUnsubscribedErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (): ObjectUnsubscribedError;\n}\n\n/**\n * An error thrown when an action is invalid because the object has been\n * unsubscribed.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n *\n * @class ObjectUnsubscribedError\n */\nexport const ObjectUnsubscribedError: ObjectUnsubscribedErrorCtor = createErrorClass(\n (_super) =>\n function ObjectUnsubscribedErrorImpl(this: any) {\n _super(this);\n this.name = 'ObjectUnsubscribedError';\n this.message = 'object unsubscribed';\n }\n);\n", "import { Operator } from './Operator';\nimport { Observable } from './Observable';\nimport { Subscriber } from './Subscriber';\nimport { Subscription, EMPTY_SUBSCRIPTION } from './Subscription';\nimport { Observer, SubscriptionLike, TeardownLogic } from './types';\nimport { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError';\nimport { arrRemove } from './util/arrRemove';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A Subject is a special type of Observable that allows values to be\n * multicasted to many Observers. Subjects are like EventEmitters.\n *\n * Every Subject is an Observable and an Observer. You can subscribe to a\n * Subject, and you can call next to feed values as well as error and complete.\n */\nexport class Subject extends Observable implements SubscriptionLike {\n closed = false;\n\n private currentObservers: Observer[] | null = null;\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n observers: Observer[] = [];\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n isStopped = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n hasError = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n thrownError: any = null;\n\n /**\n * Creates a \"subject\" by basically gluing an observer to an observable.\n *\n * @nocollapse\n * @deprecated Recommended you do not use. Will be removed at some point in the future. Plans for replacement still under discussion.\n */\n static create: (...args: any[]) => any = (destination: Observer, source: Observable): AnonymousSubject => {\n return new AnonymousSubject(destination, source);\n };\n\n constructor() {\n // NOTE: This must be here to obscure Observable's constructor.\n super();\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n lift(operator: Operator): Observable {\n const subject = new AnonymousSubject(this, this);\n subject.operator = operator as any;\n return subject as any;\n }\n\n /** @internal */\n protected _throwIfClosed() {\n if (this.closed) {\n throw new ObjectUnsubscribedError();\n }\n }\n\n next(value: T) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n if (!this.currentObservers) {\n this.currentObservers = Array.from(this.observers);\n }\n for (const observer of this.currentObservers) {\n observer.next(value);\n }\n }\n });\n }\n\n error(err: any) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.hasError = this.isStopped = true;\n this.thrownError = err;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.error(err);\n }\n }\n });\n }\n\n complete() {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.isStopped = true;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.complete();\n }\n }\n });\n }\n\n unsubscribe() {\n this.isStopped = this.closed = true;\n this.observers = this.currentObservers = null!;\n }\n\n get observed() {\n return this.observers?.length > 0;\n }\n\n /** @internal */\n protected _trySubscribe(subscriber: Subscriber): TeardownLogic {\n this._throwIfClosed();\n return super._trySubscribe(subscriber);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._checkFinalizedStatuses(subscriber);\n return this._innerSubscribe(subscriber);\n }\n\n /** @internal */\n protected _innerSubscribe(subscriber: Subscriber) {\n const { hasError, isStopped, observers } = this;\n if (hasError || isStopped) {\n return EMPTY_SUBSCRIPTION;\n }\n this.currentObservers = null;\n observers.push(subscriber);\n return new Subscription(() => {\n this.currentObservers = null;\n arrRemove(observers, subscriber);\n });\n }\n\n /** @internal */\n protected _checkFinalizedStatuses(subscriber: Subscriber) {\n const { hasError, thrownError, isStopped } = this;\n if (hasError) {\n subscriber.error(thrownError);\n } else if (isStopped) {\n subscriber.complete();\n }\n }\n\n /**\n * Creates a new Observable with this Subject as the source. You can do this\n * to create custom Observer-side logic of the Subject and conceal it from\n * code that uses the Observable.\n * @return {Observable} Observable that the Subject casts to\n */\n asObservable(): Observable {\n const observable: any = new Observable();\n observable.source = this;\n return observable;\n }\n}\n\n/**\n * @class AnonymousSubject\n */\nexport class AnonymousSubject extends Subject {\n constructor(\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n public destination?: Observer,\n source?: Observable\n ) {\n super();\n this.source = source;\n }\n\n next(value: T) {\n this.destination?.next?.(value);\n }\n\n error(err: any) {\n this.destination?.error?.(err);\n }\n\n complete() {\n this.destination?.complete?.();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n return this.source?.subscribe(subscriber) ?? EMPTY_SUBSCRIPTION;\n }\n}\n", "import { TimestampProvider } from '../types';\n\ninterface DateTimestampProvider extends TimestampProvider {\n delegate: TimestampProvider | undefined;\n}\n\nexport const dateTimestampProvider: DateTimestampProvider = {\n now() {\n // Use the variable rather than `this` so that the function can be called\n // without being bound to the provider.\n return (dateTimestampProvider.delegate || Date).now();\n },\n delegate: undefined,\n};\n", "import { Subject } from './Subject';\nimport { TimestampProvider } from './types';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * A variant of {@link Subject} that \"replays\" old values to new subscribers by emitting them when they first subscribe.\n *\n * `ReplaySubject` has an internal buffer that will store a specified number of values that it has observed. Like `Subject`,\n * `ReplaySubject` \"observes\" values by having them passed to its `next` method. When it observes a value, it will store that\n * value for a time determined by the configuration of the `ReplaySubject`, as passed to its constructor.\n *\n * When a new subscriber subscribes to the `ReplaySubject` instance, it will synchronously emit all values in its buffer in\n * a First-In-First-Out (FIFO) manner. The `ReplaySubject` will also complete, if it has observed completion; and it will\n * error if it has observed an error.\n *\n * There are two main configuration items to be concerned with:\n *\n * 1. `bufferSize` - This will determine how many items are stored in the buffer, defaults to infinite.\n * 2. `windowTime` - The amount of time to hold a value in the buffer before removing it from the buffer.\n *\n * Both configurations may exist simultaneously. So if you would like to buffer a maximum of 3 values, as long as the values\n * are less than 2 seconds old, you could do so with a `new ReplaySubject(3, 2000)`.\n *\n * ### Differences with BehaviorSubject\n *\n * `BehaviorSubject` is similar to `new ReplaySubject(1)`, with a couple of exceptions:\n *\n * 1. `BehaviorSubject` comes \"primed\" with a single value upon construction.\n * 2. `ReplaySubject` will replay values, even after observing an error, where `BehaviorSubject` will not.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n * @see {@link shareReplay}\n */\nexport class ReplaySubject extends Subject {\n private _buffer: (T | number)[] = [];\n private _infiniteTimeWindow = true;\n\n /**\n * @param bufferSize The size of the buffer to replay on subscription\n * @param windowTime The amount of time the buffered items will stay buffered\n * @param timestampProvider An object with a `now()` method that provides the current timestamp. This is used to\n * calculate the amount of time something has been buffered.\n */\n constructor(\n private _bufferSize = Infinity,\n private _windowTime = Infinity,\n private _timestampProvider: TimestampProvider = dateTimestampProvider\n ) {\n super();\n this._infiniteTimeWindow = _windowTime === Infinity;\n this._bufferSize = Math.max(1, _bufferSize);\n this._windowTime = Math.max(1, _windowTime);\n }\n\n next(value: T): void {\n const { isStopped, _buffer, _infiniteTimeWindow, _timestampProvider, _windowTime } = this;\n if (!isStopped) {\n _buffer.push(value);\n !_infiniteTimeWindow && _buffer.push(_timestampProvider.now() + _windowTime);\n }\n this._trimBuffer();\n super.next(value);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._trimBuffer();\n\n const subscription = this._innerSubscribe(subscriber);\n\n const { _infiniteTimeWindow, _buffer } = this;\n // We use a copy here, so reentrant code does not mutate our array while we're\n // emitting it to a new subscriber.\n const copy = _buffer.slice();\n for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) {\n subscriber.next(copy[i] as T);\n }\n\n this._checkFinalizedStatuses(subscriber);\n\n return subscription;\n }\n\n private _trimBuffer() {\n const { _bufferSize, _timestampProvider, _buffer, _infiniteTimeWindow } = this;\n // If we don't have an infinite buffer size, and we're over the length,\n // use splice to truncate the old buffer values off. Note that we have to\n // double the size for instances where we're not using an infinite time window\n // because we're storing the values and the timestamps in the same array.\n const adjustedBufferSize = (_infiniteTimeWindow ? 1 : 2) * _bufferSize;\n _bufferSize < Infinity && adjustedBufferSize < _buffer.length && _buffer.splice(0, _buffer.length - adjustedBufferSize);\n\n // Now, if we're not in an infinite time window, remove all values where the time is\n // older than what is allowed.\n if (!_infiniteTimeWindow) {\n const now = _timestampProvider.now();\n let last = 0;\n // Search the array for the first timestamp that isn't expired and\n // truncate the buffer up to that point.\n for (let i = 1; i < _buffer.length && (_buffer[i] as number) <= now; i += 2) {\n last = i;\n }\n last && _buffer.splice(0, last + 1);\n }\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Subscription } from '../Subscription';\nimport { SchedulerAction } from '../types';\n\n/**\n * A unit of work to be executed in a `scheduler`. An action is typically\n * created from within a {@link SchedulerLike} and an RxJS user does not need to concern\n * themselves about creating and manipulating an Action.\n *\n * ```ts\n * class Action extends Subscription {\n * new (scheduler: Scheduler, work: (state?: T) => void);\n * schedule(state?: T, delay: number = 0): Subscription;\n * }\n * ```\n *\n * @class Action\n */\nexport class Action extends Subscription {\n constructor(scheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void) {\n super();\n }\n /**\n * Schedules this action on its parent {@link SchedulerLike} for execution. May be passed\n * some context object, `state`. May happen at some point in the future,\n * according to the `delay` parameter, if specified.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler.\n * @return {void}\n */\n public schedule(state?: T, delay: number = 0): Subscription {\n return this;\n }\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetIntervalFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearIntervalFunction = (handle: TimerHandle) => void;\n\ninterface IntervalProvider {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n delegate:\n | {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n }\n | undefined;\n}\n\nexport const intervalProvider: IntervalProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setInterval(handler: () => void, timeout?: number, ...args) {\n const { delegate } = intervalProvider;\n if (delegate?.setInterval) {\n return delegate.setInterval(handler, timeout, ...args);\n }\n return setInterval(handler, timeout, ...args);\n },\n clearInterval(handle) {\n const { delegate } = intervalProvider;\n return (delegate?.clearInterval || clearInterval)(handle as any);\n },\n delegate: undefined,\n};\n", "import { Action } from './Action';\nimport { SchedulerAction } from '../types';\nimport { Subscription } from '../Subscription';\nimport { AsyncScheduler } from './AsyncScheduler';\nimport { intervalProvider } from './intervalProvider';\nimport { arrRemove } from '../util/arrRemove';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncAction extends Action {\n public id: TimerHandle | undefined;\n public state?: T;\n // @ts-ignore: Property has no initializer and is not definitely assigned\n public delay: number;\n protected pending: boolean = false;\n\n constructor(protected scheduler: AsyncScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (this.closed) {\n return this;\n }\n\n // Always replace the current state with the new state.\n this.state = state;\n\n const id = this.id;\n const scheduler = this.scheduler;\n\n //\n // Important implementation note:\n //\n // Actions only execute once by default, unless rescheduled from within the\n // scheduled callback. This allows us to implement single and repeat\n // actions via the same code path, without adding API surface area, as well\n // as mimic traditional recursion but across asynchronous boundaries.\n //\n // However, JS runtimes and timers distinguish between intervals achieved by\n // serial `setTimeout` calls vs. a single `setInterval` call. An interval of\n // serial `setTimeout` calls can be individually delayed, which delays\n // scheduling the next `setTimeout`, and so on. `setInterval` attempts to\n // guarantee the interval callback will be invoked more precisely to the\n // interval period, regardless of load.\n //\n // Therefore, we use `setInterval` to schedule single and repeat actions.\n // If the action reschedules itself with the same delay, the interval is not\n // canceled. If the action doesn't reschedule, or reschedules with a\n // different delay, the interval will be canceled after scheduled callback\n // execution.\n //\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, delay);\n }\n\n // Set the pending flag indicating that this action has been scheduled, or\n // has recursively rescheduled itself.\n this.pending = true;\n\n this.delay = delay;\n // If this action has already an async Id, don't request a new one.\n this.id = this.id ?? this.requestAsyncId(scheduler, this.id, delay);\n\n return this;\n }\n\n protected requestAsyncId(scheduler: AsyncScheduler, _id?: TimerHandle, delay: number = 0): TimerHandle {\n return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);\n }\n\n protected recycleAsyncId(_scheduler: AsyncScheduler, id?: TimerHandle, delay: number | null = 0): TimerHandle | undefined {\n // If this action is rescheduled with the same delay time, don't clear the interval id.\n if (delay != null && this.delay === delay && this.pending === false) {\n return id;\n }\n // Otherwise, if the action's delay time is different from the current delay,\n // or the action has been rescheduled before it's executed, clear the interval id\n if (id != null) {\n intervalProvider.clearInterval(id);\n }\n\n return undefined;\n }\n\n /**\n * Immediately executes this action and the `work` it contains.\n * @return {any}\n */\n public execute(state: T, delay: number): any {\n if (this.closed) {\n return new Error('executing a cancelled action');\n }\n\n this.pending = false;\n const error = this._execute(state, delay);\n if (error) {\n return error;\n } else if (this.pending === false && this.id != null) {\n // Dequeue if the action didn't reschedule itself. Don't call\n // unsubscribe(), because the action could reschedule later.\n // For example:\n // ```\n // scheduler.schedule(function doWork(counter) {\n // /* ... I'm a busy worker bee ... */\n // var originalAction = this;\n // /* wait 100ms before rescheduling the action */\n // setTimeout(function () {\n // originalAction.schedule(counter + 1);\n // }, 100);\n // }, 1000);\n // ```\n this.id = this.recycleAsyncId(this.scheduler, this.id, null);\n }\n }\n\n protected _execute(state: T, _delay: number): any {\n let errored: boolean = false;\n let errorValue: any;\n try {\n this.work(state);\n } catch (e) {\n errored = true;\n // HACK: Since code elsewhere is relying on the \"truthiness\" of the\n // return here, we can't have it return \"\" or 0 or false.\n // TODO: Clean this up when we refactor schedulers mid-version-8 or so.\n errorValue = e ? e : new Error('Scheduled action threw falsy error');\n }\n if (errored) {\n this.unsubscribe();\n return errorValue;\n }\n }\n\n unsubscribe() {\n if (!this.closed) {\n const { id, scheduler } = this;\n const { actions } = scheduler;\n\n this.work = this.state = this.scheduler = null!;\n this.pending = false;\n\n arrRemove(actions, this);\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, null);\n }\n\n this.delay = null!;\n super.unsubscribe();\n }\n }\n}\n", "import { Action } from './scheduler/Action';\nimport { Subscription } from './Subscription';\nimport { SchedulerLike, SchedulerAction } from './types';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * An execution context and a data structure to order tasks and schedule their\n * execution. Provides a notion of (potentially virtual) time, through the\n * `now()` getter method.\n *\n * Each unit of work in a Scheduler is called an `Action`.\n *\n * ```ts\n * class Scheduler {\n * now(): number;\n * schedule(work, delay?, state?): Subscription;\n * }\n * ```\n *\n * @class Scheduler\n * @deprecated Scheduler is an internal implementation detail of RxJS, and\n * should not be used directly. Rather, create your own class and implement\n * {@link SchedulerLike}. Will be made internal in v8.\n */\nexport class Scheduler implements SchedulerLike {\n public static now: () => number = dateTimestampProvider.now;\n\n constructor(private schedulerActionCtor: typeof Action, now: () => number = Scheduler.now) {\n this.now = now;\n }\n\n /**\n * A getter method that returns a number representing the current time\n * (at the time this function was called) according to the scheduler's own\n * internal clock.\n * @return {number} A number that represents the current time. May or may not\n * have a relation to wall-clock time. May or may not refer to a time unit\n * (e.g. milliseconds).\n */\n public now: () => number;\n\n /**\n * Schedules a function, `work`, for execution. May happen at some point in\n * the future, according to the `delay` parameter, if specified. May be passed\n * some context object, `state`, which will be passed to the `work` function.\n *\n * The given arguments will be processed an stored as an Action object in a\n * queue of actions.\n *\n * @param {function(state: ?T): ?Subscription} work A function representing a\n * task, or some unit of work to be executed by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler itself.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @return {Subscription} A subscription in order to be able to unsubscribe\n * the scheduled work.\n */\n public schedule(work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription {\n return new this.schedulerActionCtor(this, work).schedule(state, delay);\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Action } from './Action';\nimport { AsyncAction } from './AsyncAction';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncScheduler extends Scheduler {\n public actions: Array> = [];\n /**\n * A flag to indicate whether the Scheduler is currently executing a batch of\n * queued actions.\n * @type {boolean}\n * @internal\n */\n public _active: boolean = false;\n /**\n * An internal ID used to track the latest asynchronous task such as those\n * coming from `setTimeout`, `setInterval`, `requestAnimationFrame`, and\n * others.\n * @type {any}\n * @internal\n */\n public _scheduled: TimerHandle | undefined;\n\n constructor(SchedulerAction: typeof Action, now: () => number = Scheduler.now) {\n super(SchedulerAction, now);\n }\n\n public flush(action: AsyncAction): void {\n const { actions } = this;\n\n if (this._active) {\n actions.push(action);\n return;\n }\n\n let error: any;\n this._active = true;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions.shift()!)); // exhaust the scheduler queue\n\n this._active = false;\n\n if (error) {\n while ((action = actions.shift()!)) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\n/**\n *\n * Async Scheduler\n *\n * Schedule task as if you used setTimeout(task, duration)\n *\n * `async` scheduler schedules tasks asynchronously, by putting them on the JavaScript\n * event loop queue. It is best used to delay tasks in time or to schedule tasks repeating\n * in intervals.\n *\n * If you just want to \"defer\" task, that is to perform it right after currently\n * executing synchronous code ends (commonly achieved by `setTimeout(deferredTask, 0)`),\n * better choice will be the {@link asapScheduler} scheduler.\n *\n * ## Examples\n * Use async scheduler to delay task\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * const task = () => console.log('it works!');\n *\n * asyncScheduler.schedule(task, 2000);\n *\n * // After 2 seconds logs:\n * // \"it works!\"\n * ```\n *\n * Use async scheduler to repeat task in intervals\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * function task(state) {\n * console.log(state);\n * this.schedule(state + 1, 1000); // `this` references currently executing Action,\n * // which we reschedule with new state and delay\n * }\n *\n * asyncScheduler.schedule(task, 3000, 0);\n *\n * // Logs:\n * // 0 after 3s\n * // 1 after 4s\n * // 2 after 5s\n * // 3 after 6s\n * ```\n */\n\nexport const asyncScheduler = new AsyncScheduler(AsyncAction);\n\n/**\n * @deprecated Renamed to {@link asyncScheduler}. Will be removed in v8.\n */\nexport const async = asyncScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\nimport { SchedulerAction } from '../types';\nimport { animationFrameProvider } from './animationFrameProvider';\nimport { TimerHandle } from './timerHandle';\n\nexport class AnimationFrameAction extends AsyncAction {\n constructor(protected scheduler: AnimationFrameScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay is greater than 0, request as an async action.\n if (delay !== null && delay > 0) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n // Push the action to the end of the scheduler queue.\n scheduler.actions.push(this);\n // If an animation frame has already been requested, don't request another\n // one. If an animation frame hasn't been requested yet, request one. Return\n // the current animation frame request id.\n return scheduler._scheduled || (scheduler._scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));\n }\n\n protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n if (delay != null ? delay > 0 : this.delay > 0) {\n return super.recycleAsyncId(scheduler, id, delay);\n }\n // If the scheduler queue has no remaining actions with the same async id,\n // cancel the requested animation frame and set the scheduled flag to\n // undefined so the next AnimationFrameAction will request its own.\n const { actions } = scheduler;\n if (id != null && actions[actions.length - 1]?.id !== id) {\n animationFrameProvider.cancelAnimationFrame(id as number);\n scheduler._scheduled = undefined;\n }\n // Return undefined so the action knows to request a new async id if it's rescheduled.\n return undefined;\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\nexport class AnimationFrameScheduler extends AsyncScheduler {\n public flush(action?: AsyncAction): void {\n this._active = true;\n // The async id that effects a call to flush is stored in _scheduled.\n // Before executing an action, it's necessary to check the action's async\n // id to determine whether it's supposed to be executed in the current\n // flush.\n // Previous implementations of this method used a count to determine this,\n // but that was unsound, as actions that are unsubscribed - i.e. cancelled -\n // are removed from the actions array and that can shift actions that are\n // scheduled to be executed in a subsequent flush into positions at which\n // they are executed within the current flush.\n const flushId = this._scheduled;\n this._scheduled = undefined;\n\n const { actions } = this;\n let error: any;\n action = action || actions.shift()!;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions[0]) && action.id === flushId && actions.shift());\n\n this._active = false;\n\n if (error) {\n while ((action = actions[0]) && action.id === flushId && actions.shift()) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AnimationFrameAction } from './AnimationFrameAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\n\n/**\n *\n * Animation Frame Scheduler\n *\n * Perform task when `window.requestAnimationFrame` would fire\n *\n * When `animationFrame` scheduler is used with delay, it will fall back to {@link asyncScheduler} scheduler\n * behaviour.\n *\n * Without delay, `animationFrame` scheduler can be used to create smooth browser animations.\n * It makes sure scheduled task will happen just before next browser content repaint,\n * thus performing animations as efficiently as possible.\n *\n * ## Example\n * Schedule div height animation\n * ```ts\n * // html:
\n * import { animationFrameScheduler } from 'rxjs';\n *\n * const div = document.querySelector('div');\n *\n * animationFrameScheduler.schedule(function(height) {\n * div.style.height = height + \"px\";\n *\n * this.schedule(height + 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * }, 0, 0);\n *\n * // You will see a div element growing in height\n * ```\n */\n\nexport const animationFrameScheduler = new AnimationFrameScheduler(AnimationFrameAction);\n\n/**\n * @deprecated Renamed to {@link animationFrameScheduler}. Will be removed in v8.\n */\nexport const animationFrame = animationFrameScheduler;\n", "import { Observable } from '../Observable';\nimport { SchedulerLike } from '../types';\n\n/**\n * A simple Observable that emits no items to the Observer and immediately\n * emits a complete notification.\n *\n * Just emits 'complete', and nothing else.\n *\n * ![](empty.png)\n *\n * A simple Observable that only emits the complete notification. It can be used\n * for composing with other Observables, such as in a {@link mergeMap}.\n *\n * ## Examples\n *\n * Log complete notification\n *\n * ```ts\n * import { EMPTY } from 'rxjs';\n *\n * EMPTY.subscribe({\n * next: () => console.log('Next'),\n * complete: () => console.log('Complete!')\n * });\n *\n * // Outputs\n * // Complete!\n * ```\n *\n * Emit the number 7, then complete\n *\n * ```ts\n * import { EMPTY, startWith } from 'rxjs';\n *\n * const result = EMPTY.pipe(startWith(7));\n * result.subscribe(x => console.log(x));\n *\n * // Outputs\n * // 7\n * ```\n *\n * Map and flatten only odd numbers to the sequence `'a'`, `'b'`, `'c'`\n *\n * ```ts\n * import { interval, mergeMap, of, EMPTY } from 'rxjs';\n *\n * const interval$ = interval(1000);\n * const result = interval$.pipe(\n * mergeMap(x => x % 2 === 1 ? of('a', 'b', 'c') : EMPTY),\n * );\n * result.subscribe(x => console.log(x));\n *\n * // Results in the following to the console:\n * // x is equal to the count on the interval, e.g. (0, 1, 2, 3, ...)\n * // x will occur every 1000ms\n * // if x % 2 is equal to 1, print a, b, c (each on its own)\n * // if x % 2 is not equal to 1, nothing will be output\n * ```\n *\n * @see {@link Observable}\n * @see {@link NEVER}\n * @see {@link of}\n * @see {@link throwError}\n */\nexport const EMPTY = new Observable((subscriber) => subscriber.complete());\n\n/**\n * @param scheduler A {@link SchedulerLike} to use for scheduling\n * the emission of the complete notification.\n * @deprecated Replaced with the {@link EMPTY} constant or {@link scheduled} (e.g. `scheduled([], scheduler)`). Will be removed in v8.\n */\nexport function empty(scheduler?: SchedulerLike) {\n return scheduler ? emptyScheduled(scheduler) : EMPTY;\n}\n\nfunction emptyScheduled(scheduler: SchedulerLike) {\n return new Observable((subscriber) => scheduler.schedule(() => subscriber.complete()));\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport function isScheduler(value: any): value is SchedulerLike {\n return value && isFunction(value.schedule);\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\nimport { isScheduler } from './isScheduler';\n\nfunction last(arr: T[]): T | undefined {\n return arr[arr.length - 1];\n}\n\nexport function popResultSelector(args: any[]): ((...args: unknown[]) => unknown) | undefined {\n return isFunction(last(args)) ? args.pop() : undefined;\n}\n\nexport function popScheduler(args: any[]): SchedulerLike | undefined {\n return isScheduler(last(args)) ? args.pop() : undefined;\n}\n\nexport function popNumber(args: any[], defaultValue: number): number {\n return typeof last(args) === 'number' ? args.pop()! : defaultValue;\n}\n", "export const isArrayLike = ((x: any): x is ArrayLike => x && typeof x.length === 'number' && typeof x !== 'function');", "import { isFunction } from \"./isFunction\";\n\n/**\n * Tests to see if the object is \"thennable\".\n * @param value the object to test\n */\nexport function isPromise(value: any): value is PromiseLike {\n return isFunction(value?.then);\n}\n", "import { InteropObservable } from '../types';\nimport { observable as Symbol_observable } from '../symbol/observable';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being Observable (but not necessary an Rx Observable) */\nexport function isInteropObservable(input: any): input is InteropObservable {\n return isFunction(input[Symbol_observable]);\n}\n", "import { isFunction } from './isFunction';\n\nexport function isAsyncIterable(obj: any): obj is AsyncIterable {\n return Symbol.asyncIterator && isFunction(obj?.[Symbol.asyncIterator]);\n}\n", "/**\n * Creates the TypeError to throw if an invalid object is passed to `from` or `scheduled`.\n * @param input The object that was passed.\n */\nexport function createInvalidObservableTypeError(input: any) {\n // TODO: We should create error codes that can be looked up, so this can be less verbose.\n return new TypeError(\n `You provided ${\n input !== null && typeof input === 'object' ? 'an invalid object' : `'${input}'`\n } where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`\n );\n}\n", "export function getSymbolIterator(): symbol {\n if (typeof Symbol !== 'function' || !Symbol.iterator) {\n return '@@iterator' as any;\n }\n\n return Symbol.iterator;\n}\n\nexport const iterator = getSymbolIterator();\n", "import { iterator as Symbol_iterator } from '../symbol/iterator';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being an Iterable */\nexport function isIterable(input: any): input is Iterable {\n return isFunction(input?.[Symbol_iterator]);\n}\n", "import { ReadableStreamLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport async function* readableStreamLikeToAsyncGenerator(readableStream: ReadableStreamLike): AsyncGenerator {\n const reader = readableStream.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n return;\n }\n yield value!;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\nexport function isReadableStreamLike(obj: any): obj is ReadableStreamLike {\n // We don't want to use instanceof checks because they would return\n // false for instances from another Realm, like an
  1. Connect the tablet to WiFi (or connect direct to network using a SIM card)

Note: here you can also copy the file from your computer to the tablet using a cable

  1. Download the APK file to your tablet and tap it to install. You can also copy the APK file to the tablet with a cable

Note that here you may get a warning for enabling Unknown Sources. This setting allows you to install an app coming from outside the Play store. Please allow this in your settings.

The instructions below are generic and they may differ for your version of Android.

  1. If you receive the blocked install message like in the image below click Settings to enable Unknown Sources. Tap the Settings button

Make sure you select the Unknown sources

Click OK to Confirm

To continue with the installation note all different permission Tangerine needs. We are making user of the GPS location, and contents on device – this one is for data export.

SMS is used only if configured in your forms to send an sms

Taking pictures is also used only when configured in your forms

Click Next

Click Install

Wait for the installation to complete

Now you have the app installed. you can find it in the app drawer or your main screen.

To open the app click Open

The app is now installed. You can proceed and Register a user. In general we let the data assessor go through the registration process and create their username and password. Note that more than one users can share a tablet.

Fill in your information and click Submit

The next step is to create your assessor profile. All of the data on the user profile(Assessor/ Enumerator profile) is attached to each form collected on this device

Enter the information presented on the user profile page and tap Submit

You are now free to use the app

Using Device Setup Installation - 2 way sync setup

Installation

Tangerine offers two deployment /installation strategies, Android installation or web browser installation:

  • Android Installation. This is the standard deployment package where an actual apk file can be generated on the computer, downloaded, and then copied over to a mobile device via a USB cable and installed. This method of deployment is suitable in slow network environment or when the apk is large.

  • Web Browser Installation. This deployment strategy requires an Internet connection on the tablet for the Tangerine to be installed. Once installed, the app can work again offline. This method is suitable in places of good connectivity. This method of installation can also be used for installing app on your Chrome browser on a PC or laptop

For both of the installation models you will need a Registration Code (QR Code) or a device ID and Token. If you don't have this information you will not be able to install the application on your device.

We recommend that the initial device setup is done by a responsible person at the site. This can be someone who will handle queries regarding Tangerine or an IT staff. Each device registration requires that an admin user is create on that device. This admin user is the one that can authorize the registration of a user account. This is to make sure that your data can only be accessed by authorized personnel and no untheorized accounts exist on the tablet.

Installation on a tablet/phone

Copy the apk file to the tablet and open it. Follow the installation prompts until you receive a message that the app has been installed. Locate the Tangerine app in the application drawer and click it.

Warning

Note that you must be online on the tablet to do the initial installation.

The first step is to select the language for the user interface

Select the language and click Submit

Now enter the administration password for this tablet. You may wish to use the same admin password for all tables at your site. This same password will be required each time a user is registering to use the app on this device.

Select Yes if you have a device code or No if you are going to insert a device ID and Token for the registration.

Insert the ID and Token or click the Scan icon to scan the registration QR code.

Click Submit when done. The next screen will show you some information for this device. If it is correct select Yes, if the scanned device code and ID correspond to a different device select No and start over with the correct device code.

On the next screen you will see some synchronization information. The app at this moment is contacting the server and obtaining users assigned to your device location. If you have already collected data on another tablet for this location, this data will also be pulled.

Click Next and then go to the Registration tap. Ask your administrator to enter the admin password and enter your user information below. Click submit when ready.

If you are an administrator handing off the tablet to a user, enter your password and ask the user to enter their username and password. Here the Year of Birth can be used by the user to reset their password in case they forgot it.

On the next screen you will see a dropdown of all users for this location. Select the one that corresponds to you and click Submit.

You will now see a screen similar to the one below where you can start working

Installation in your Chrome browser

Tangerine can be installed and used offline in the Chrome browser. To do this we follow similar installation instructions as above.

Warning

You must be online on to do the initial installation.**

The first step is to follow the link for Browser installation that has been given to you or copied directly after it's generation in the backend. Copy the link and paste it in the Chrome's address bar. You will see a screen indicating that the app is being installed.

After a successful installation you will receive a confirmation screen like the one below. Do not click the link to proceed.

Click the + icon beside the address bar to install Tangerine in your browser. A popup will open to give you the option to install the app. Click Install

Depending on your browser setup, you may be asked to create a shortcut on your desktop or in your program folder or the browser may close automatically and offer you the link for Tangerine to open. If you see the link click it

If you cannot find it type this into the address bar of your browser: [chrome://apps/]{.underline}

Warning

Always start Tangerine from the application icon and not from the URL address. Only one Tangerine instllation per browser profile is allowed.

Click the Tangerine app to start the application.

Select the language and click Submit

Now enter the administration password for this tablet. You may wish to use the same admin password for all tables at your site. This same password will be required each time a user is registering to use the app on this device.

Select Yes if you have a device code or No if you are going to insert a device ID and Token for the registration.

Insert the ID and Token or click the Scan icon to scan the registration QR code. If your PC or laptop doesn't have a camera that can be used to scan the barcode, you'd have to type in the ID and Token

Click Submit when done. The next screen will show you some information for this device. If it is correct select Yes, if the scanned device code and ID correspond to a different device select No and start over with the correct device code.

On the next screen you will see some synchronization information. The app at this moment is contacting the server and obtaining users assigned to your device location. If you have already collected data on another tablet for this location, this data will also be pulled.

Click Next and then go to the Registration tap. Ask your administrator to enter the admin password and enter your user information below. Click submit when ready.

If you are an administrator handing off the tablet to a user, enter your password and ask the user to enter their username and password. Here the Year of Birth can be used by the user to reset their password in case they forgot it.

On the next screen you will see a dropdown of all users for this location. Select the one that corresponds to you and click Submit.

You will now see a screen similar to the one below where you can start working

Synchronization

User setup

Follow to below steps to prepare the Tangerine backend to allow installation of your app on the user device or Chrome browser. The menu items used during setup can be found under the Deploy link in the left side navigation menu.

Click the Device Users section to create a new tablet user profile. On this screen you will see a listing of all users already created. At the bottom left of the screen there is a'+' icon which allows you to add a new device user profile.

  • Click the + icon to create a user profile on the server

  • Fill in all information required in the profile and click Submit

  • You will see that next to the Submit button a blue check marks appears indicating that the profile was saved

  • Repeat the above steps for all users

Device setup

Now we have to create the 'virtual' devices that will be associated with a real device or browser upon installation. The devices that you create represent a real user device. Each virtual device can be associated with a real tablet only once. After being claimed the device cannot be reused (unless reset and the app re-installed). Each device requires that we assign it to a particular location. This assigned location is automatically attached to each form collected from the tablet. We can also control the synchronization level. This level will indicate to Tangerine what records should be kept in sync across tablets. If you select to synchronize on a top level, all tablets will contain all records collected over all of the facilities in under this top level. It may be better to choose to synchronize only at the bottom level.

The device listing go to Deploy->Devices

  • Here, if you have some devices already created you will see a full listing with some other information

  • The device listing gives you:

    • The ID of the device

    • The assigned location

    • Whether this device has already been used in an installation or not. A checkmark under Claimed indicates that it has been used.

    • Registered on gives you the date this device was first registered

    • The last synchronization date for this device

    • Updated on is the date this device was last updated

    • Version is the version of the application this device is running

    • Under options you will have access to a menu allowing you to Edit, Reset, Delete, get The QR registration code for a device.

Create a new device by going to Deploy->Devices

  • Click the + icon at the bottom of the page

  • Note that you have the ID and Token listed here (can also be access by clicking Edit for a particular device on the device listing screen) In cases where the QR code cannot be sent to the site for installation you can also use the ID and Token to install Tangerine.

  • Select the location this device is assigned to

  • Select the synchronization level

  • Select the actual site to be used for synchronization. Be careful here not to assign the device to one location and select a different one for synchronization.

  • Click Submit

  • The device now shows up on the of the list and you can get the QR code for it by clicking the Options menu and selecting Registration Code

  • Repeat the above steps for all devices that you need to use on your project.

NOTE: you may wish to store the device ID and Token in a file for safe keeping. Such a sample file can be found here. Put the device ID and the Token in the corresponding columns and the QR code will be generated for you. Keep in mind that his worksheet functions correctly only on Google Drive. You can also print this file and distribute the installation codes on paper.

Device modification

It may happen that you want to modify a device that is already in use. Although you will be able to do that in the interface, this modification is not pushed to the actual tablet. The way to go here is to apply the modification reset the device, and send the new Registration Code or ID and Token to the admin to reinstall the application. Make sure that before re-installing the app all data is synchronized.

NOTE: You can edit all devices that have not been claimed yet.

To Edit a device, click the Edit button in the Options menu. Apply any modifications and click Submit

To Delete a device , click the Delete button in the Options menu. Keep in mind that any device that you delete will disallow this device from synchronization or updates. Use this option if you have had one of your devices stolen or lost.

To Reset a device, click the Reset button in the options menu. This action will disallow the device from receiving updates or synchronizing data. Use this option, if one of your staff members leaves and will no longer use this device. Make sure data is synchronized before you reset the device.

\ No newline at end of file diff --git a/data-collector/index.html b/data-collector/index.html new file mode 100644 index 0000000000..74cb68d5f0 --- /dev/null +++ b/data-collector/index.html @@ -0,0 +1 @@ + Data Collector Guide - Tangerine Documentation
\ No newline at end of file diff --git a/data-collector/media/apk1.png b/data-collector/media/apk1.png new file mode 100644 index 0000000000..95d181761a Binary files /dev/null and b/data-collector/media/apk1.png differ diff --git a/data-collector/media/apk10.png b/data-collector/media/apk10.png new file mode 100644 index 0000000000..2ed8506a1b Binary files /dev/null and b/data-collector/media/apk10.png differ diff --git a/data-collector/media/apk2.png b/data-collector/media/apk2.png new file mode 100644 index 0000000000..418dba6a72 Binary files /dev/null and b/data-collector/media/apk2.png differ diff --git a/data-collector/media/apk3.png b/data-collector/media/apk3.png new file mode 100644 index 0000000000..218c4fa4d0 Binary files /dev/null and b/data-collector/media/apk3.png differ diff --git a/data-collector/media/apk4.png b/data-collector/media/apk4.png new file mode 100644 index 0000000000..fbf5b80cd2 Binary files /dev/null and b/data-collector/media/apk4.png differ diff --git a/data-collector/media/apk5.png b/data-collector/media/apk5.png new file mode 100644 index 0000000000..6bde94a7fa Binary files /dev/null and b/data-collector/media/apk5.png differ diff --git a/data-collector/media/apk6.png b/data-collector/media/apk6.png new file mode 100644 index 0000000000..197508a41f Binary files /dev/null and b/data-collector/media/apk6.png differ diff --git a/data-collector/media/apk7.png b/data-collector/media/apk7.png new file mode 100644 index 0000000000..a9394b457f Binary files /dev/null and b/data-collector/media/apk7.png differ diff --git a/data-collector/media/apk8.png b/data-collector/media/apk8.png new file mode 100644 index 0000000000..470490ca5a Binary files /dev/null and b/data-collector/media/apk8.png differ diff --git a/data-collector/media/apk9.png b/data-collector/media/apk9.png new file mode 100644 index 0000000000..275d31f8a5 Binary files /dev/null and b/data-collector/media/apk9.png differ diff --git a/data-collector/media/device10.png b/data-collector/media/device10.png new file mode 100644 index 0000000000..c59f73726c Binary files /dev/null and b/data-collector/media/device10.png differ diff --git a/data-collector/media/device11.png b/data-collector/media/device11.png new file mode 100644 index 0000000000..276f5e4cc6 Binary files /dev/null and b/data-collector/media/device11.png differ diff --git a/data-collector/media/device12.png b/data-collector/media/device12.png new file mode 100644 index 0000000000..82c87bc70f Binary files /dev/null and b/data-collector/media/device12.png differ diff --git a/data-collector/media/device13.png b/data-collector/media/device13.png new file mode 100644 index 0000000000..bb012714c4 Binary files /dev/null and b/data-collector/media/device13.png differ diff --git a/data-collector/media/device14.png b/data-collector/media/device14.png new file mode 100644 index 0000000000..34d9b38c22 Binary files /dev/null and b/data-collector/media/device14.png differ diff --git a/data-collector/media/device15.png b/data-collector/media/device15.png new file mode 100644 index 0000000000..3b4d2016d3 Binary files /dev/null and b/data-collector/media/device15.png differ diff --git a/data-collector/media/device16.png b/data-collector/media/device16.png new file mode 100644 index 0000000000..43094d1614 Binary files /dev/null and b/data-collector/media/device16.png differ diff --git a/data-collector/media/device17.png b/data-collector/media/device17.png new file mode 100644 index 0000000000..ec38ec3d24 Binary files /dev/null and b/data-collector/media/device17.png differ diff --git a/data-collector/media/device18.png b/data-collector/media/device18.png new file mode 100644 index 0000000000..7d2d737937 Binary files /dev/null and b/data-collector/media/device18.png differ diff --git a/data-collector/media/device19.png b/data-collector/media/device19.png new file mode 100644 index 0000000000..e0c75c85bf Binary files /dev/null and b/data-collector/media/device19.png differ diff --git a/data-collector/media/device2.png b/data-collector/media/device2.png new file mode 100644 index 0000000000..6a0630eee2 Binary files /dev/null and b/data-collector/media/device2.png differ diff --git a/data-collector/media/device20.png b/data-collector/media/device20.png new file mode 100644 index 0000000000..0994fc08c6 Binary files /dev/null and b/data-collector/media/device20.png differ diff --git a/data-collector/media/device21.png b/data-collector/media/device21.png new file mode 100644 index 0000000000..f934c03474 Binary files /dev/null and b/data-collector/media/device21.png differ diff --git a/data-collector/media/device22.png b/data-collector/media/device22.png new file mode 100644 index 0000000000..fefbf963ca Binary files /dev/null and b/data-collector/media/device22.png differ diff --git a/data-collector/media/device23.png b/data-collector/media/device23.png new file mode 100644 index 0000000000..8f9135daad Binary files /dev/null and b/data-collector/media/device23.png differ diff --git a/data-collector/media/device3.png b/data-collector/media/device3.png new file mode 100644 index 0000000000..f00e2b07af Binary files /dev/null and b/data-collector/media/device3.png differ diff --git a/data-collector/media/device4.png b/data-collector/media/device4.png new file mode 100644 index 0000000000..822d67f117 Binary files /dev/null and b/data-collector/media/device4.png differ diff --git a/data-collector/media/device5.png b/data-collector/media/device5.png new file mode 100644 index 0000000000..9e692f7e30 Binary files /dev/null and b/data-collector/media/device5.png differ diff --git a/data-collector/media/device6.png b/data-collector/media/device6.png new file mode 100644 index 0000000000..8f636d8951 Binary files /dev/null and b/data-collector/media/device6.png differ diff --git a/data-collector/media/device7.png b/data-collector/media/device7.png new file mode 100644 index 0000000000..618bdc91ed Binary files /dev/null and b/data-collector/media/device7.png differ diff --git a/data-collector/media/device8.png b/data-collector/media/device8.png new file mode 100644 index 0000000000..83735528be Binary files /dev/null and b/data-collector/media/device8.png differ diff --git a/data-collector/media/device9.png b/data-collector/media/device9.png new file mode 100644 index 0000000000..5d13db4b8e Binary files /dev/null and b/data-collector/media/device9.png differ diff --git a/data-collector/media/image74.png b/data-collector/media/image74.png new file mode 100644 index 0000000000..87ba9d20d6 Binary files /dev/null and b/data-collector/media/image74.png differ diff --git a/data-collector/media/image75.png b/data-collector/media/image75.png new file mode 100644 index 0000000000..ca381d3fcf Binary files /dev/null and b/data-collector/media/image75.png differ diff --git a/data-collector/media/image76.png b/data-collector/media/image76.png new file mode 100644 index 0000000000..15bd357e1b Binary files /dev/null and b/data-collector/media/image76.png differ diff --git a/data-collector/media/image77.png b/data-collector/media/image77.png new file mode 100644 index 0000000000..c7ee87c256 Binary files /dev/null and b/data-collector/media/image77.png differ diff --git a/data-collector/media/image78.png b/data-collector/media/image78.png new file mode 100644 index 0000000000..167fc8a0d4 Binary files /dev/null and b/data-collector/media/image78.png differ diff --git a/data-collector/media/image79.png b/data-collector/media/image79.png new file mode 100644 index 0000000000..00b715b38c Binary files /dev/null and b/data-collector/media/image79.png differ diff --git a/data-collector/media/image80.png b/data-collector/media/image80.png new file mode 100644 index 0000000000..3f81262262 Binary files /dev/null and b/data-collector/media/image80.png differ diff --git a/data-collector/media/image801.png b/data-collector/media/image801.png new file mode 100644 index 0000000000..993003a0f0 Binary files /dev/null and b/data-collector/media/image801.png differ diff --git a/data-collector/media/image802.png b/data-collector/media/image802.png new file mode 100644 index 0000000000..9ab94af990 Binary files /dev/null and b/data-collector/media/image802.png differ diff --git a/data-collector/media/image81.png b/data-collector/media/image81.png new file mode 100644 index 0000000000..5d1cb4ddf3 Binary files /dev/null and b/data-collector/media/image81.png differ diff --git a/data-collector/media/image82.jpg b/data-collector/media/image82.jpg new file mode 100644 index 0000000000..e485b145a3 Binary files /dev/null and b/data-collector/media/image82.jpg differ diff --git a/data-collector/media/image83.png b/data-collector/media/image83.png new file mode 100644 index 0000000000..b76f822fab Binary files /dev/null and b/data-collector/media/image83.png differ diff --git a/data-collector/media/image84.png b/data-collector/media/image84.png new file mode 100644 index 0000000000..e98ff25e12 Binary files /dev/null and b/data-collector/media/image84.png differ diff --git a/data-collector/media/image85.png b/data-collector/media/image85.png new file mode 100644 index 0000000000..6d556beb27 Binary files /dev/null and b/data-collector/media/image85.png differ diff --git a/data-collector/media/image92.png b/data-collector/media/image92.png new file mode 100644 index 0000000000..6ee9157588 Binary files /dev/null and b/data-collector/media/image92.png differ diff --git a/data-collector/media/image93.png b/data-collector/media/image93.png new file mode 100644 index 0000000000..2a28a83789 Binary files /dev/null and b/data-collector/media/image93.png differ diff --git a/data-collector/media/image94.png b/data-collector/media/image94.png new file mode 100644 index 0000000000..81f1af4018 Binary files /dev/null and b/data-collector/media/image94.png differ diff --git a/data-collector/media/image95.png b/data-collector/media/image95.png new file mode 100644 index 0000000000..1aff9bcb57 Binary files /dev/null and b/data-collector/media/image95.png differ diff --git a/data-collector/media/image96.png b/data-collector/media/image96.png new file mode 100644 index 0000000000..a05992eb2d Binary files /dev/null and b/data-collector/media/image96.png differ diff --git a/data-collector/media/image97.png b/data-collector/media/image97.png new file mode 100644 index 0000000000..18821d3d73 Binary files /dev/null and b/data-collector/media/image97.png differ diff --git a/data-collector/media/update1.png b/data-collector/media/update1.png new file mode 100644 index 0000000000..27096a966d Binary files /dev/null and b/data-collector/media/update1.png differ diff --git a/data-collector/media/update2.png b/data-collector/media/update2.png new file mode 100644 index 0000000000..28e9ad15cb Binary files /dev/null and b/data-collector/media/update2.png differ diff --git a/data-collector/media/update3.png b/data-collector/media/update3.png new file mode 100644 index 0000000000..13a917440d Binary files /dev/null and b/data-collector/media/update3.png differ diff --git a/data-collector/media/update4.png b/data-collector/media/update4.png new file mode 100644 index 0000000000..acfd5a8d62 Binary files /dev/null and b/data-collector/media/update4.png differ diff --git a/data-collector/media/update5.png b/data-collector/media/update5.png new file mode 100644 index 0000000000..6805fc7e11 Binary files /dev/null and b/data-collector/media/update5.png differ diff --git a/data-collector/media/visitsTab1.png b/data-collector/media/visitsTab1.png new file mode 100644 index 0000000000..9636e6e0bf Binary files /dev/null and b/data-collector/media/visitsTab1.png differ diff --git a/data-collector/media/visitsTab2.png b/data-collector/media/visitsTab2.png new file mode 100644 index 0000000000..9cb36ef834 Binary files /dev/null and b/data-collector/media/visitsTab2.png differ diff --git a/data-collector/p2p/images/sync-p2p-all-complete.jpg b/data-collector/p2p/images/sync-p2p-all-complete.jpg new file mode 100644 index 0000000000..0c385efb5c Binary files /dev/null and b/data-collector/p2p/images/sync-p2p-all-complete.jpg differ diff --git a/data-collector/p2p/images/sync-p2p-device-2-selected.jpg b/data-collector/p2p/images/sync-p2p-device-2-selected.jpg new file mode 100644 index 0000000000..f9609f967f Binary files /dev/null and b/data-collector/p2p/images/sync-p2p-device-2-selected.jpg differ diff --git a/data-collector/p2p/images/sync-p2p-discovery-button.jpg b/data-collector/p2p/images/sync-p2p-discovery-button.jpg new file mode 100644 index 0000000000..7c746669fa Binary files /dev/null and b/data-collector/p2p/images/sync-p2p-discovery-button.jpg differ diff --git a/data-collector/p2p/images/sync-p2p-discovery-endpoints-listed.jpg b/data-collector/p2p/images/sync-p2p-discovery-endpoints-listed.jpg new file mode 100644 index 0000000000..e42adca7fb Binary files /dev/null and b/data-collector/p2p/images/sync-p2p-discovery-endpoints-listed.jpg differ diff --git a/data-collector/p2p/images/sync-p2p-done-next.jpg b/data-collector/p2p/images/sync-p2p-done-next.jpg new file mode 100644 index 0000000000..41ddf54d7d Binary files /dev/null and b/data-collector/p2p/images/sync-p2p-done-next.jpg differ diff --git a/data-collector/p2p/images/sync-p2p-endpoint-chosen.jpg b/data-collector/p2p/images/sync-p2p-endpoint-chosen.jpg new file mode 100644 index 0000000000..b84084bc75 Binary files /dev/null and b/data-collector/p2p/images/sync-p2p-endpoint-chosen.jpg differ diff --git a/data-collector/p2p/images/sync-p2p-error-message.jpg b/data-collector/p2p/images/sync-p2p-error-message.jpg new file mode 100644 index 0000000000..495565349d Binary files /dev/null and b/data-collector/p2p/images/sync-p2p-error-message.jpg differ diff --git a/data-collector/p2p/images/sync-p2p-home-confirmation-fr.jpg b/data-collector/p2p/images/sync-p2p-home-confirmation-fr.jpg new file mode 100644 index 0000000000..44abcb8f18 Binary files /dev/null and b/data-collector/p2p/images/sync-p2p-home-confirmation-fr.jpg differ diff --git a/data-collector/p2p/images/sync-p2p-home-confirmation.jpg b/data-collector/p2p/images/sync-p2p-home-confirmation.jpg new file mode 100644 index 0000000000..9fb4568530 Binary files /dev/null and b/data-collector/p2p/images/sync-p2p-home-confirmation.jpg differ diff --git a/data-collector/p2p/images/sync-p2p-menu-fr.jpg b/data-collector/p2p/images/sync-p2p-menu-fr.jpg new file mode 100644 index 0000000000..af43913288 Binary files /dev/null and b/data-collector/p2p/images/sync-p2p-menu-fr.jpg differ diff --git a/data-collector/p2p/images/sync-p2p-p2p-sync-page.jpg b/data-collector/p2p/images/sync-p2p-p2p-sync-page.jpg new file mode 100644 index 0000000000..9d1a06f65c Binary files /dev/null and b/data-collector/p2p/images/sync-p2p-p2p-sync-page.jpg differ diff --git a/data-collector/p2p/images/sync-p2p-tangy-menu-sync.jpg b/data-collector/p2p/images/sync-p2p-tangy-menu-sync.jpg new file mode 100644 index 0000000000..34208fe956 Binary files /dev/null and b/data-collector/p2p/images/sync-p2p-tangy-menu-sync.jpg differ diff --git a/data-collector/p2p/images/sync-p2p-transfer-log-1.jpg b/data-collector/p2p/images/sync-p2p-transfer-log-1.jpg new file mode 100644 index 0000000000..34d6627786 Binary files /dev/null and b/data-collector/p2p/images/sync-p2p-transfer-log-1.jpg differ diff --git a/data-collector/p2p/p2p-sync/index.html b/data-collector/p2p/p2p-sync/index.html new file mode 100644 index 0000000000..b01c847e74 --- /dev/null +++ b/data-collector/p2p/p2p-sync/index.html @@ -0,0 +1 @@ + Using P2P Sync for Offline Data Transfer - Tangerine Documentation

Using P2P Sync for Offline Data Transfer

Use the P2P sync feature to transfer data between two or more tablets without an Internet connection.

Note: The tablets must be running Android 8 (Oreo) to use this function.

In the following example, your tablet will be syncing data from your tablet to two other peers' tablets running Tangerine. The goal is to have the same data on all tablets. At the end of the process, data will be transferred from your tablet to the Internet.

Accessing the P2P feature:

Each peer should select Sync from the menu.

sync-p2p-menu

Click the P2P Sync tab.

sync-p2p-menu

Discovery

Gather in a circle with your peers. You and your peers should click the Discovery button. It does not matter who clicks first.

sync-p2p-page-discovery

Endpoints

After a short time - a minute of two, the screen will show the device name, a list of available endpoints, and information about the data transfers in the Log.

In the screenshot, your device name is highlighted in green (89678). The list of available endpoints - your peers' tablet names - is highlighted in red. A log of diagnostic information is highlighted in blue.

(Please note that the endpoint names are randomly generated. The names you see when using this feature will be different.)

sync-p2p-discovery-endpoints-listed

Syncing to an Endpoint

At this point in the process, your peers don't need to push any buttons; they only need to monitor the sync process for errors. Your tablet will be called the "master" tablet because it is controlling the sync operations. Once the "master" tablet has collected all of the data from the tablets, it can be connected to the Internet and upload all of this data to the server.

Ask your peers which one has the tab at the top of the endpoints list marked "35747 - Pending". Upon identifying that peer, ask them to pay attention to the screen. Now you may click "35747 - Pending" to initiate the data transfer. Notice how the endpoint button you click turns a darker shade of grey to indicate that it has been pressed.

sync-p2p-endpoint-chosen

Your tablet will send its data to your peer's tablet and then your peer's tablet will send its data back to your tablet, as well as the data you just sent. It's a little redundant, but this is part of reaching "eventual consistency" for all of the tablets.

Notice that more data is added to the Log as the connection is made between the tablets and data transfer is initiated:

sync-p2p-transfer-log-1

When the data transfer is complete, the endpoint list updates to show that you are ready to sync the next device ("Done! Sync next device.").

sync-p2p-done-next

Ask your peer if they received any error messages; if not, it is safe to proceed to the next peer's tablet. Ask the peer who has the tablet marked "29726" to be ready. Click the endpoint marked "29726: Pending".

sync-p2p-done-next

When the data transfer is complete, the endpoint list updates to show that you are ready to sync the next device ("Done! Sync next device."). Ask your peer if they received any error messages. If none, great! Since you're at the end of the endpoints list, you are done with this first part.

sync-p2p-all-complete

Do it again!

When you synced data from your "master" tablet to the second tablet, it received data from the first tablet, which was transferred to the "master" when it was sync'd. But the first tablet still needs to receive data from the second tablet. So, you will need to repeat this whole process, starting from the first tablet (35747) and then to the second (29726). (You actually don't need to sync again the final device sync'd in the process (29726), but it is easier to explain this process as a simple round-robin.)

It may be useful to confirm that any records created on other tablets has indeed been transferred.

English: sync-p2p-home-confirmation

French: sync-p2p-home-confirmation

As mentioned earlier, once the sync process is complete (and you've done it twice), you may conect the "master" to the Internet and transfer data to the server.

Tips

  • Each time you visit the P2P page, your device name will change. It is a randomly generated number.
  • Errors are highlighted in pink. It is fine to ignore the error marked "State set to CONNECTED but already in that state".

sync-p2p-error-message

\ No newline at end of file diff --git a/data-collector/update-app/index.html b/data-collector/update-app/index.html new file mode 100644 index 0000000000..acbf1d6555 --- /dev/null +++ b/data-collector/update-app/index.html @@ -0,0 +1 @@ + Update app - Tangerine Documentation

Update app

Updating the App

The app update in Tangerine can bring up new content or updates. to your forms but it can also update the version of the app you are currently using.

To check which version of the app you are on go to the top right menu and select About. When you scroll down you will see the Device Information section. The most important information here is the Version Tag. In the example below the version tag is: 2022-06-16-14-21-15

If the default tag is used it will be in a similar format, where you see the date and time of the release. Ask your supervisor for the correct version tag to make sure you are running the right forms.

You can also see the build channel below. This indicates if you are running a Live or a Test release

To update the app you have to find the Check for Updates link in the top right menu. Make sure you are connected to the network and tap the Check for updates link.

After you tap the Check for updates link you will receive a pop up message. This message asks you to confirm that you want to perform the check for updates. Clicking OK will continue with the check and will update the app. Clicking Cancel will cancel the update.

Tap OK to continue:

After the update has been downloaded you will come back to a similar screen. Tap Continue to apply any pending actions.

You will now be logged out of the app and have to log back in. Every time you do an update you'd have to log in to the app again. After the update go to the About page and verify that the Version tag has changed.

You can see below that my version tag is now: 2022-06-24-14-10-00. If your version tag didn't change there was no update available to be installed. Updates can be pushed only by users with access to the backend.

\ No newline at end of file diff --git a/data-manager/data-structure/index.html b/data-manager/data-structure/index.html new file mode 100644 index 0000000000..bddbcf1cbd --- /dev/null +++ b/data-manager/data-structure/index.html @@ -0,0 +1,21 @@ + Data Structure - Tangerine Documentation

Data Structure

A Case is a Document in a CouchDB database. A Case consists of 4 other Entities: CaseEvent, EventForm, and Participant. A Case is described by its CaseDefinition. A CaseDefinition also contains CaseEvetnDefinition(s) that describe what CaseEvents can exist in a Case, EventFormDefinition(s) that define what EventForms can exist in what CaseEvents, and Roles that describe what kind of Participants can exist in a Case. Lastly, all EventForms relate to separate FormResponse Documents in the database.

FormResponse Schema

CaseDefinition Schema

{
+  // A unique string that will be used to refer to the CaseDefinition.
+  "id": "case-type-1",
+  // An ID of a Form listed in forms.json that will be used to store Case level variables. 
+  "formId": "case-type-1-manifest",
+  ...
+}
+

Case Schema

{
+  // A UUID generated by Tangerine to refer to the Case internal to Tangerine. Useful if Study IDs have collisions.
+  "id": "00673b47-8452-4b4c-9597-5a56fe8a059c",
+  // Because every Case is also a FormResponse, a Case includes top level form info you would find on a FormResponse.
+  "form": {
+    // The ID of the FormResponse this Case relates to. Shoudl be the same as the related CaseDefinition's formId.
+    "id": "case-type-1-manifest",
+    ...
+  },
+  // The related CaseDefinition's id property. 
+  "caseDefinitionId": "case-type-1",
+  ...
+}
+

CaseEventDefinition Schema

CaseEvent Schema

EventFormDefinition Schema

EventForm Schema

\ No newline at end of file diff --git a/data-manager/index.html b/data-manager/index.html new file mode 100644 index 0000000000..b4636e2878 --- /dev/null +++ b/data-manager/index.html @@ -0,0 +1 @@ + Data Manager - Tangerine Documentation

Data Manager

\ No newline at end of file diff --git a/data-manager/mysql/index.html b/data-manager/mysql/index.html new file mode 100644 index 0000000000..a389f2eb9c --- /dev/null +++ b/data-manager/mysql/index.html @@ -0,0 +1 @@ + MySQL - Tangerine Documentation
\ No newline at end of file diff --git a/data-manager/new-issue-on-device-button.png b/data-manager/new-issue-on-device-button.png new file mode 100644 index 0000000000..7e8c2fc274 Binary files /dev/null and b/data-manager/new-issue-on-device-button.png differ diff --git a/data-manager/on-device-data-corrections/index.html b/data-manager/on-device-data-corrections/index.html new file mode 100644 index 0000000000..ae96291a69 --- /dev/null +++ b/data-manager/on-device-data-corrections/index.html @@ -0,0 +1,20 @@ + On Device Data Corrections using Issues - Tangerine Documentation

On Device Data Corrections using Issues

Enable "Allow Creation of Issues" in your App Config for Devices and Data Collectors will be able to propose changes to data that has already been submitted. After the Data Collector syncs, Data Managers may view those proposals in the Issues UI in their corresponding group, then comment or choose to merge the issue.

New Issue on Device Button

Configuration

  • To enable Devices to create Issues, add "allowCreationOfIssues": true to the group's client/app-config.json and release to Devices.
  • To allow Devices to see Issues that have been created on the Device or been synced down to the Device, add "showIssues": trueto the group's client/app-config.json and release to Devices.
  • To template out the resulting Issue title and descriptions, add templateIssueTitle and templateIssueDescription to Case Definitions.
  • Example template for Issue Title: Issue for ${caseService.getVariable('study_id')} (${caseService.getVariable('firstname')} ${caseService.getVariable('surname')}, ${caseService.getVariable('village')}) by ${userId}

On Device Data Merge using Issues

Optionally, enable "Allow Merge of Issues" in your App Config for Data Collectors on Devices to see the "Commit" button on Issues. Clicking the "Commit" button will take the form and case changes in the proposal and make them the current version that appears in the case and form response.

Although the "Merge" setting can be available for all Data Collectors, it is best practice to require some oversite for data collectors to merge issues. The Data Manager can set User Roles to control.

To allow merge on tablet by user role, add user role (defined in user-profile.html) the "update" permissions section for event definitions or form definitions in the case definition file. For example, the case definition file excerpt below would allow all users except the "data_collector_role" to merge cases.

{
+    "id": "hosehold-case",
+    "formId": "household-case-manifest",
+    ...
+    "eventDefinitions": [
+      {
+        "id": "enrollment-event",
+        "name": "Household Baseline Visit",
+        "description": "",
+        "repeatable": false,
+        "estimatedTimeFromCaseOpening": 0,
+        "estimatedTimeWindow": 0,
+        "required": true,
+        "permissions": {
+          "create": ["admin"],
+          "read":   ["admin", "data_manager_role", "data_collector_role"],
+          "update": ["admin", "data_manager_role"],
+          "delete": ["admin"]
+        },
+

Configuration

  1. Consider which strategy best allows data collectors to merge corrections while providing oversight to Issues.
  2. To enable Devices to Merge Issues, add "allowMergeOfIssues": true to the group's client/app-config.json and release to Devices.
  3. Add User Role permissions to the case definitions file
\ No newline at end of file diff --git a/data-manager/tangerine-mysql-base-database-structure.png b/data-manager/tangerine-mysql-base-database-structure.png new file mode 100644 index 0000000000..5a3c6cc67d Binary files /dev/null and b/data-manager/tangerine-mysql-base-database-structure.png differ diff --git a/developer/assets/inspect-helper-functions.png b/developer/assets/inspect-helper-functions.png new file mode 100644 index 0000000000..d72cb3f957 Binary files /dev/null and b/developer/assets/inspect-helper-functions.png differ diff --git a/developer/assets/template-debugger.png b/developer/assets/template-debugger.png new file mode 100644 index 0000000000..0182d17eb8 Binary files /dev/null and b/developer/assets/template-debugger.png differ diff --git a/developer/bootstrapping-tangerine/index.html b/developer/bootstrapping-tangerine/index.html new file mode 100644 index 0000000000..d0540b806e --- /dev/null +++ b/developer/bootstrapping-tangerine/index.html @@ -0,0 +1 @@ + Bootstrapping Tangerine - Tangerine Documentation

Bootstrapping Tangerine

During a recent Angular 8 upgrade process, we had to change how Tangerine initializes. If Tangerine is running inside a Cordova app, it must wait until the 'deviceReady' event is emitted. Our earlier method of doing this in main.ts is no longer possible; therefore, we are intercepting the Service initialization process by using APP_INITIALIZER to pause the app while Cordova loads. Here is the relevant comit. For more information, view the Predefined tokens and multiple provider section in the Angular documentation.

\ No newline at end of file diff --git a/developer/class-deletions/index.html b/developer/class-deletions/index.html new file mode 100644 index 0000000000..50b9a1ea58 --- /dev/null +++ b/developer/class-deletions/index.html @@ -0,0 +1,33 @@ + Deletions in Tangerine - Tangerine Documentation

Deletions in Tangerine

We're archiving records instead of deleting them in Tangerine by setting archive:true on the root of the doc and filtering queries by && !archive.

Please note that vanilla Tangerine uses the archived flag.'

For most queries, you simply must simply append && !archive to the query in order to ensure your views filter for the archive flag.

Sample view that filters by archive:

    responsesByClassIdCurriculumId: {
+      map: function (doc) {
+        if (doc.hasOwnProperty('collection') && doc.collection === 'TangyFormResponse' && !doc.archive) {
+          if (doc.hasOwnProperty('metadata') && doc.metadata.studentRegistrationDoc.classId) {
+            // console.log("matching: " + doc.metadata.studentRegistrationDoc.classId)
+             emit([doc.metadata.studentRegistrationDoc.classId, doc.form.id], true);
+          }
+        }
+      }.toString()
+    },
+

Sample function to archive some records:

  async archiveStudent(column) {
+    let studentId = column.id
+    console.log("Archiving student:" + studentId)
+    let deleteConfirmed = confirm(_TRANSLATE("Delete this student?"));
+    if (deleteConfirmed) {
+      try {
+        let responses = await this.classViewService.getResponsesByStudentId(studentId)
+        for (const response of responses as any[] ) {
+          response.doc.archive = true;
+          let lastModified = Date.now();
+          response.doc.lastModified = lastModified
+          const archiveResult = await this.classViewService.saveResponse(response.doc)
+          console.log("archiveResult: " + archiveResult)
+        }
+        let result = await this.dashboardService.archiveStudentRegistration(studentId)
+        console.log("result: " + result)
+      } catch (e) {
+        console.log("Error deleting student: " + e)
+        return false;
+      }
+    }
+  }
+
\ No newline at end of file diff --git a/developer/class-docs/index.html b/developer/class-docs/index.html new file mode 100644 index 0000000000..249c3d7266 --- /dev/null +++ b/developer/class-docs/index.html @@ -0,0 +1,16 @@ + Getting Started - Tangerine Documentation

Getting Started

How to get data out of a TangyFormResponse

const studentRegistrationDoc = await dashboardService.getResponse(this.studentId);
+const srInputs = this.getInputValues(studentRegistrationDoc);
+
+  getInputValues(doc) {
+    let inputs = doc.items.reduce((acc, item) => [...acc, ...item.inputs], [])
+    let obj = {}
+    for (const el of inputs) {
+      var attrs = inputs.attributes;
+      for(let i = inputs.length - 1; i >= 0; i--) {
+        obj[inputs[i].name] = inputs[i].value;
+      }
+    }
+    console.log("obj: " + JSON.stringify(obj))
+    return obj;
+  }
+
\ No newline at end of file diff --git a/developer/cordova-plugin-development/index.html b/developer/cordova-plugin-development/index.html new file mode 100644 index 0000000000..9b744a15cf --- /dev/null +++ b/developer/cordova-plugin-development/index.html @@ -0,0 +1,26 @@ + Cordova plugin development - Tangerine Documentation

Cordova plugin development

Getting started

It is a lot easier to build a cordova plugin for Tangerine using a generic Cordova project instead of developing directly in Tangerine, because in Tangerine access to the actual client Cordova code is hidden away in /tangerine/client/builds/apk. So first use the cordova cli to generate a new project.

Refreshing your new plugin in your Cordova project

After making modifications to the plugin, rm and add the plugin and the cordova android platform before building.

cordova plugin rm cordova-plugin-nearby-connections
+cordova platform rm android
+cordova platform add android@8
+cordova plugin add ../../Tangerine-Community/cordova-plugin-nearby-connections
+cordova build android
+

Updating the cordova plugin inside Tangerine

After your done the bulk of your Cordova development, you will need to modify the docker-tangerine-base-image to include the new plugin. After updating the base image, don't forget to update the Dockerfile.

Sometimes you may need to view an update to the plugin but you don't want to go to the trouble of updating the base image. It is possible to work on the plugin code and then refresh the code in Tangerine. First you will need to share the source code with your docker instance Add the following to develop.sh:

  --volume $(pwd)/../cordova-plugin-nearby-connections:/tangerine/client/cordova-plugin-nearby-connections \
+

Once your container has started, docker exec into it, and run the following:

cd /tangerine/client/builds/apk
+cordova plugin rm cordova-plugin-nearby-connections
+cordova plugin add ../../cordova-plugin-nearby-connections --save
+
Sometimes cordova can have issues with cleaning the build; here's a way to make sure you have the updated code:
cd /tangerine/client/builds/apk
+cordova plugin rm cordova-plugin-nearby-connections
+cordova platform rm android
+cordova platform add android@8
+cordova plugin add ../../cordova-plugin-nearby-connections --save
+cordova build android
+

Updating Angular client code used in the APK

IF you're developing Cordova plugins for Tangerine and make changes to the Angular client code that is displayed in the apk, you will need to refresh the apk build. Run the following code:

cd /tangerine/client && \
+rm -rf builds/apk/www/shell && \
+rm -rf builds/pwa/release-uuid/app && \
+cp -r dev builds/apk/www/shell && \
+cp -r pwa-tools/updater-app/build/default builds/pwa && \
+cp -r dev builds/pwa/release-uuid/app
+

Then generate the apk.

To check if it worked, you can search for the new code in these files:

vi builds/apk/www/shell/main.js
+vi builds/pwa/release-uuid/app/main.js
+

T0 uninstall and re-install the apk:

adb uninstall org.rti.tangerine
+adb install qa/apks/group-long-uuisd/platforms/android/app/build/outputs/apk/debug/app-debug.apk
+
\ No newline at end of file diff --git a/developer/creating-clean-dev-content/index.html b/developer/creating-clean-dev-content/index.html new file mode 100644 index 0000000000..34d95950cd --- /dev/null +++ b/developer/creating-clean-dev-content/index.html @@ -0,0 +1,8 @@ + Creating Clean Development Content - Tangerine Documentation

Creating Clean Development Content

If you are trying to fix an issue, it is helpful to begin development using content that is known to support common Tangerine features. This can be more reliable than using a project's content because that content may have missing forms that create bugs that have nothing to do with the issue you are trying to resolve.

Client

cd client
+npm install
+rm -rf client/src/assets
+cp -r ../content-sets/<your pick>/client src/assets
+cp src/assets/app-config.defaults.json src/assets/app-config.json
+cp ../translations/translation* src/assets/
+npm start
+

Server

The create-group command to the rescue!

The following command downloads a content set known to support common Tangerine features and is used for load-testing. Notice that it is a github repo; therefore, you may clone it and modify at will.

docker exec tangerine create-group "New Group C" https://github.com/rjsteinert/tangerine-content-set-test.git

There is also support for creating a group using local content from the content-sets directory' in the Tangerine repository. Currently, there is support for creating a case-module:

docker exec tangerine create-group "New Group D" case-module

You may also configure how inputs are populated by custom functions; see the Case generation section in the Load testing doc.

If you add --help to the create-group command you may see other options as well.

docker exec tangerine create-group --help

To see more examples, check out the demo video from the v3.10.0 release.

\ No newline at end of file diff --git a/developer/data-structures/index.html b/developer/data-structures/index.html new file mode 100644 index 0000000000..7fb28d8032 --- /dev/null +++ b/developer/data-structures/index.html @@ -0,0 +1 @@ + Tangerine Data Structures - Document Collections and Types - Tangerine Documentation

Tangerine Data Structures - Document Collections and Types

If the goal is to have data output to CSV via the changes log, the collection property of the document should be "TangyFormResponse". This is the only collection that is currently supported by data processing on the server-side.

TangyFormResponse

A TangyFormResponse is a JSON object that contains all the data that a user has entered into a Tangy Form. It is the data that is stored in the database when a user submits a Tangy Form. It is also the data that is returned when you call the dashboardService.getResponse() method.

Tangerine modules - such as CSV and mysql - can be adapted to support different data structures in a TangyFormResponse. The default case is to output a CSV file with a header row and a row for each TangyFormResponse. The header row contains the names of the inputs in the TangyFormResponse. The data rows contain the values of the inputs in the TangyFormResponse.

A register - such as an Attendance register used in Teach - is a snapshot of a list of things being tracked, such as students attending a class. (Contrast this to a typical TangyFormResponse that is a snapshot of form inputs.) A register is for collecting data about multiple people or objects, whereas a formResponse is collecting data for a single unit or participant. An example of a register is a form of type 'attendance' or 'scores' which is used to collect data in the Attendance feature of the Teach module. There is a property called attendanceList that has an array of students. When processing the changes feed, the csv module detects the 'attendance' type and creates a new row for each student in the attendanceList.

In the case module context, a case definition manages participants and the forms submitted with data about each participant. A class project does not have a file similar to a case definition; it is more rigid and stores much of those relationships in the app logic.

One could say a case is similar to a Class in Teach in that Teach saves metadata about a school, classes and students, but it is a rigid structure. A case is more flexible and can be used to track any type of data.

\ No newline at end of file diff --git a/developer/debugging-reporting/index.html b/developer/debugging-reporting/index.html new file mode 100644 index 0000000000..dd7aaf338f --- /dev/null +++ b/developer/debugging-reporting/index.html @@ -0,0 +1,41 @@ + Debugging reporting - Tangerine Documentation

Debugging reporting

Debugging the Reporting Cache process

Summary of steps:

  1. Turn on reporting modules in config.sh.
  2. Run develop.sh.
  3. Create a group.
  4. Generate data.
  5. Stop the keep alive for reporting worker by commenting out this.keepAliveReportingWorker() in server/src/app.service.ts.
  6. Enter the container on command line with docker exec -it tangerine bash.
  7. Clear reporting cache with command reporting-cache-clear.
  8. Run a batch with debugger enabled by running command node --inspect-brk=0.0.0.0:9228 $(which reporting-worker-batch).
  9. Latch onto debugging session using Chrome Inspect. You may need to click "configure" and add localhost:9228 to "Target discovery settings".

Instructions

Configure your project to use the CSV and Logstash modules:

T_MODULES="['csv', 'logstash']"
+

Start the development environment...

./develop.sh
+

Create a group called foo in the GUI. Then open ./server/src/app.service.ts and comment out the call to this.keepAliveReportingWorker(). It's important to do these two things in this order otherwise the group could be disconnected from reporting.

"exec" into the container and note how foo has been added to the /reporting-worker-state.json file.

docker exec -it tangerine bash
+cat /reporting-worker-state.json
+

Seed the foo group with 100 form responses.

docker exec tangerine generate-uploads 100 foo
+

Clear the cache

```shell script docker exec tangerine reporting-cache-clear

If you get the error message 'Waiting for current reporting worker to stop...', you must exec into the container and remove the semaphore:
+
+```shell script
+rm /reporting-worker-running
+

and then run reporting-cache-clear again.

Start the reporting-worker-batch.js batch process manually and check for errors

```shell script node --inspect-brk=0.0.0.0:9228 $(which reporting-worker-batch)

In Chrome, go to `chrome://inspect`, click `Configure...`, and add `127.0.0.1:9228` as an entry in "Target discovery settings".
+
+## Debugging Demo 
+
+https://www.youtube.com/watch?v=AToUBoApw8E&feature=youtu.be
+
+Now manually trigger a batch. After the command finishes, verify the batch by checking `http://localhost:5984/_utils/#database/foo-reporting/_all_docs`.
+
node --inspect-brk=0.0.0.0:9228 $(which reporting-worker-batch)
There will be only 15 docs in your reporting db because that is the batch size.
+
+Although Tangerine in develop.sh mode runs node in a debugger process, you must launch a separate node process to debug the batch reporting worker.
+
+If no errors occurred, copy the temporary state to the current state.
+
cp /reporting-worker-state.json_tmp /reporting-worker-state.json
Keep repeating to continue processing...
+
cat /reporting-worker-state.json | /tangerine/serversrc/scripts/reporting-worker-batch.js | tee /reporting-worker-state.json_tmp cp /reporting-worker-state.json_tmp /reporting-worker-state.json
If you would like to debug, add the `--inspect-brk=0.0.0.0:9227` option to the `run-worker.js` command.
+
cat /reporting-worker-state.json | node --inspect-brk=0.0.0.0:9227 /tangerine/server/src/scripts/reporting-worker-batch.js | tee /reporting-worker-state.json_tmp
When you run that command, it will wait on the first line of the script for a debugger to connect to it. In Chrome, go to `chrome://inspect`, click `Configure...`, and add `127.0.0.1:9227` as an entry in "Target discovery settings". Now back to the `chrome://inspect` page and you will find under the `Remote Target #127.0.0.1` group, a new target has been discovered called `/tangerine/server/reporting/run-worker.js`. Click `inspect` and now you should be able to set breakpoints and walk through the code. You may not be able to set breakpoints in all files so use "step into" and the `debugger` keyword to get the debugger to the focus you want.
+
+
+If you want to keep the cache worker running, use watch.
+
watch -n 1 "cat /reporting-worker-state.json | node /tangerine/server/reporting/run-worker.js | tee /.reporting-worker-state.json | json_pp && cp /.reporting-worker-state.json /reporting-worker-state.json"
If you need to clear a reporting cache, don't simply delete the reporting db. Use
+
reporting-cache-clear

You typically need to remove the semaphore before running reporting-cache-clear, especially if there was a crash
+
rm /reporting-worker-running

## A typical report debugging workflow:
+
+Remember to setup config.sh properly! (Make sure  T_MODULES="['csv','logstash']")
+Comment out keepAliveReportingWorker in /server/src/app.service.ts.
+Remember to add `127.0.0.1:9228` as an entry in "Target discovery settings" in chrome://inspect/#devices
+
+You may need to add `debugger` before the line of code you wish to debug. 
+
+docker exec into your container
+
docker exec -it tangerine bash
Then you'll typically need to rm the reporting-worker-running - it keeps reporting-cache-clear from running if a previous debug session crashed.
+
rm /reporting-worker-running reporting-cache-clear node --inspect-brk=0.0.0.0:9228 $(which reporting-worker-batch)
Switch back to Chrome, open `chrome://inspect`. The debugger will be the session that looks like this:
+
Target /usr/local/bin/reporting-worker-batch file:///tangerine/server/src/scripts/reporting-worker-batch.js Inspect ```

When it launches, it will wait on the first line of the script for a debugger to connect to it. Click F8 to run. If all is right and good in this world, it will stop at your debugger statement. When the batch has completed, your debugger window will close.

\ No newline at end of file diff --git a/developer/debugging_node_apps/index.html b/developer/debugging_node_apps/index.html new file mode 100644 index 0000000000..09031f5c65 --- /dev/null +++ b/developer/debugging_node_apps/index.html @@ -0,0 +1,9 @@ + Debugging node apps - Tangerine Documentation

Debugging node apps

In develop.sh, the port 9229 should be opend.

docker run \
+  -d \
+  --name tangerine-container \
+  -p 80:80 -p 5984:5984 -p 9229:9229 \
+  --env "DEBUG=1" \
+  --env "NODE_ENV=development" \
+  etc...
+

Add the folloiwng to your node process:

--inspect=[::]:9229 index.js
+

for example, reporting/shart.sh:

nodemon --inspect=[::]:9229 index.js

using the leh* db for testing workflow csv generation

http://localhost/app/group-leh_wi_lan_pilot_2018/index.html#assessments

\ No newline at end of file diff --git a/developer/deletion-strategy/index.html b/developer/deletion-strategy/index.html new file mode 100644 index 0000000000..ffacf0b0fb --- /dev/null +++ b/developer/deletion-strategy/index.html @@ -0,0 +1,42 @@ + Deletion Strategy - Tangerine Documentation

Deletion Strategy

Introduction:

To delete a case in Editor, click the "Trashcan" button to the right of the case ID in the "breadcrumbs" area at the top of the case document. This adds archived:true to the case document as well as all eventForm responses for the case.

The client search index code filters out docs with the archived:true flag.

Once you start using this deletion feature, create an update using the following code:

await window['T'].search.createIndex()
+await userDb.query('search', { limit: 1 })
+

This code will rebuild the index on client in order to return the correct results when running a search on a tablet that already has data collected on it.

Difference between Delete and Archive feature in Editor

In Editor, the "Delete" and "Archive" buttons both add the archived:true flag to a document, but they do differ in the following ways: - Deleting a case creates "stub" documents with minimal properties. The case doc keeps its inputs so search works properly. - A case that has been archived can be un-archived. The only difference between an archived doc and a non-archived doc is the presence of the archived:true flag.

Deleting a record in Tangerine manually on the server

When using this manual method, deletions on the client do not follow deletions on the server.

Open the document in Fauxton. Click the "Delete" button on the right hand side of the header to delete. This will create a bare-bones document that includes the _deleted" flag. By default, deleted docs are not included in replication; however, there is a way to query them (see below).

Testing deletions

When you open a new case on client, don't enter any data, but click next. The app does create a case and it can be sync'd. Although the case does not display in the case home search results on client, this record does get output on the server via CSV export.

When you use the Delete button in Fauxton, it removes all data except for this tombstone:

{
+  "_id": "6c27f5c8-6e08-4245-ae57-cef7d63099de",
+  "_rev": "2-28819102bb2ec0e3390f28d94467212a",
+  "_deleted": true
+}
+

When the client syncs, it does not get this new rev.

Viewing deletions:

curl -X POST -H "content-Type: application/json" "http://admin:password@localhost:5984/group-2627a0a7-852a-4f51-9d5d-b7ae53130976/_changes?filter=_selector" -d '{"selector": {"_deleted": true}}'
+

response:

{
+  "results": [
+    {
+      "seq": "16-g1AAAAIBeJyVz0sOgjAQBuABjI-FZ9AjUJpaXMlNtC8CBNuFutab6E30JnqTWh4J0UQDm5nkz8yXmRIAplkgYa6NNlIl2mTmcCxd7DPgC2ttkQVsvHfBhIRrgST5Hv6xzpeu8k0reLWgMBacx32FpBK2reDXAltRRGLcV9hVwvlDEDiKKev7hR65ChfXHHLtFIIICzkdpNwa5d4pVDIs0mG3PBrlWSlQK5HCKUJIwuykpUpzreRf4dUItmBe8QYjK50u",
+      "id": "6c27f5c8-6e08-4245-ae57-cef7d63099de",
+      "changes": [
+        {
+          "rev": "2-28819102bb2ec0e3390f28d94467212a"
+        }
+      ],
+      "deleted": true
+    }
+  ],
+  "last_seq": "20-g1AAAAIfeJyV0F0OgjAMAOAp_j54Bj0CY5mDJ7mJbusIEtwe1Ge9id5Eb6I3wTFIiBoNvLRJ035pmyOEJqkHaKaNNqBibVKzP-S23OdIzIuiyFKPj3a2MKZ-JDHQz-Yf42Jho1jVQs8JihApRNhWiEthXQt9J_AlwzQkbYVNKZzeBEmCkPG2V-iBjehsk0UujUIx5b5gnZRrpdwahQEnMum2y71SHqWCnBIokmCMAU2PGlSy1Qr-Cs9KcD8ZOiEBSUX09dXsBf6mphA",
+  "pending": 0
+}
+

To get deleted doc, use the rev: http://localhost:5984/group-2627a0a7-852a-4f51-9d5d-b7ae53130976/6c27f5c8-6e08-4245-ae57-cef7d63099de?rev=2-28819102bb2ec0e3390f28d94467212a

How to create deletions on client

Potential steps: - Use the _changes example above to get a list of deleted docs on the server. - Process the results and use the pouchdb remove function on each doc/rev.

Restoring deleted documents

Using couchdb-wedge

Using the restore-deleted-doc command from couchdb-wedge, we can give it a URL, db, and docId to restore.

npm install -g couchdb-wedge
+wedge restore-deleted-doc --url http://username:password@source-server.com:5984 --db my-db --docId 1234
+

Using curl

Get the revs for the deleted doc:

curl -H 'Accept: application/json' 'http://server:5984/group-uuid-devices/6013f414-6401-4903-b0f9-fb862779cc3f?revs=true&open_revs=all'
+

Command returns:

{
+  "_id": "6013f414-6401-4903-b0f9-fb862779cc3f",
+  "_rev": "4-46fb2d064595bb6b2068b20450f2a3f9",
+  "_deleted": true,
+  "_revisions": {
+    "start": 4,
+    "ids": [
+      "46fb2d064595bb6b2068b20450f2a3f9",
+      "59e68396a5b632f62e3ab3930dda3d45",
+      "09463546fcf1177408821fccada40269",
+      "e3af953eab52ed5f3d56c9f57fdeb2f9"
+    ]
+  }
+}
+

Get the previous rev id by subtracting 1 from the _revisions.start property and appending the value of the second element in the _revisions.id array:

Result should be 3-59e68396a5b632f62e3ab3930dda3d45

Now query the server for that _rev:

http://server/group-uuid-devices/6013f414-6401-4903-b0f9-fb862779cc3f?rev=3-59e68396a5b632f62e3ab3930dda3d45

To overwrite the old deleted entry you have to post or put back the document with the correct id and the latest revision number (not the pre delete revision number, but the revision number the document has now it has been deleted).

An easier way to do this is to use the COPY command:

curl -X COPY "server:5984/group-uuid-devices/6013f414-6401-4903-b0f9-fb862779cc3f?rev=3-59e68396a5b632f62e3ab3930dda3d45" -H "Destination: 6013f414-6401-4903-b0f9-fb862779cc3f"

\ No newline at end of file diff --git a/developer/development-bullet-points/index.html b/developer/development-bullet-points/index.html new file mode 100644 index 0000000000..c0e823a542 --- /dev/null +++ b/developer/development-bullet-points/index.html @@ -0,0 +1 @@ + Bullet points for Tangerine Development - Tangerine Documentation

Bullet points for Tangerine Development

Here are my steps when developing for Tangerine: - Launch ngrok.io to provide https in front of the app. - Check settings in config.sh: o T_HOST_NAME='SOME-NAME.ngrok.io' – this is critical for sync to work. I don’t think PWA’s work w/ IP addresses - must have a domain name. o T_MODULES="['csv','sync-protocol-2, case']" - Launch Tangerine in developer mode: ./develop.sh - Once it is up, drop to console and do the following to get a docker console: docker exec -it tangerine bash - Create a new group: docker exec tangerine create-group "New Group D" case-module - Here is a doc on creating a basic case module group which users sync protocol 2, which has the bi-directional syncing/user mgmt.: https://docs.tangerinecentral.org/developer/creating-clean-dev-content/ - Create a tablet user at Deploy -> Device Users - While you’re in there, go to Deploy -> Devices and create a device. - Go to your Tangerine instance using the URL you configured in T_HOST_NAME. Go to the new group you just created and release a PWA: o Go to “Release Offline App”. In Web Browser Installation, select “Generate Test Release”. o Click “ Release PWA” button. Copy the url it displays once it has generated the PWA. It should generate the PWA using the T_HOST_NAME. - In Chrome, enter a new Profile (makes life easier when testing…) and paste the URL for the PWA. - In the Device Setup , choose “no” for the Device QR code to scan”. Switch to your Tangerine app, go to Deploy -> Devices and edit the device you created earlier and copy the device ID and Token. - Once it is setup, login w/ admin or the user you created. - To generate cases, look at the Case generation page: https://docs.tangerinecentral.org/developer/load-testing/ o Don’t worry about the substitutions part, just crank out some cases, substituting your group name (something like group-98e646c1-77e5-45ef-8a19-31501c2142a3) for GROUP-UUID below: o docker exec tangerine generate-cases 10 GROUP-UUID o After generating cases, sync!

Troubleshooting

If you have problems, check the app-config. File in the group you created. It is at tangerine/data/groups/group-4de0b30c-1c90-4efd-8dcf-e83527109038/client/app-config.json. In the group I setup on the server:

"serverUrl":https://project.server.org/

PWA’s won’t work for that because I no longer have a load balancer setup nor DNS pointing to that instance. So if things are flaky on the groups you generate, this is a good thing to check. Also check the config.sh for the T_HOST_NAME as noted above.

To confirm your config.sh settings are correct, your group’s app-config.json should have: "syncProtocol":"2"," "homeUrl":"case-home", "serverUrl":https://SERVER.nkgrok.io/

\ No newline at end of file diff --git a/developer/docker-network-issues/index.html b/developer/docker-network-issues/index.html new file mode 100644 index 0000000000..18f1e781be --- /dev/null +++ b/developer/docker-network-issues/index.html @@ -0,0 +1,30 @@ + Docker Network Issues - Tangerine Documentation

Docker Network Issues

Overview

If you develop behind a corporate firewall, you may run into issues when building Tangerine from the Dockerfile relating to network access to file resources. Why would this happen? - Your corporate network may use the same ports as the virtual private network that docker creates. - Your local DNS may may configured to use an internal corporate DNS which causes resolution problems when offline.

If you experience these problems, add the following switches to your docker config file:

"default-address-pools": [
+        {
+            "base": "172.80.0.0/16",
+            "size": 24
+        },
+        {
+            "base": "172.90.0.0/16",
+            "size": 24
+        }
+    ],
+  "dns": [
+    "75.75.75.75",
+    "8.8.8.8"
+  ]
+

Background

Networking

Error:

bower polymer#^2.0.0                       ECMDERR Failed to execute "git ls-remote --tags --heads https://github.com/Polymer/polymer.git", exit code of #128 fatal: unable to access 'https://github.com/Polymer/polymer.git/': gnutls_handshake() failed: The TLS connection was non-properly terminated.
+
+Additional error details:
+fatal: unable to access 'https://github.com/Polymer/polymer.git/': gnutls_handshake() failed: The TLS connection was non-properly terminated.
+

Quoting correspondence with a colleague:

"Docker creates bridge networks on the set of ranges 172.[17-31].0.0/16 (and some others) by default. If a server had a Docker network on 172.19.0.0/16, it could receive traffic from the VPN, but it would send its response to the bridge network, where it wouldn’t go anywhere."

Fortunately, we can change the default address pools for Docker networks by changing the configuration for the Docker daemon: https://github.com/moby/moby/pull/36396

I’m setting ours to the default address example in that pull request:"

    "default-address-pools": [
+        {
+            "base": "172.80.0.0/16",
+            "size": 24
+        },
+        {
+            "base": "172.90.0.0/16",
+            "size": 24
+        }
+    ]
+

DNS

Error:

request to https://registry.npmjs.org/@vaadin/vaadin-usage-statistics/-/vaadin-usage-statistics-2.1.0.tgz failed, reason: getaddrinfo EAI_AGAIN registry.npmjs.org registry.npmjs.org:443
+

I think this is where I found this solution:

https://github.com/npm/npm/issues/16661

So, I needed to configure the docker DNS config. The first item is my local ISP (Comcast_ DNS server, the second is Google’s.)

"dns": [ "75.75.75.75", "8.8.8.8" ]

Change them to your needs.

Anyway, the good news is that with both the default-address-pools and dns properties in my docker config, my build works both connected and disconnected to the RTI VPN.

\ No newline at end of file diff --git a/developer/how-tangerine-is-built/index.html b/developer/how-tangerine-is-built/index.html new file mode 100644 index 0000000000..1672207f19 --- /dev/null +++ b/developer/how-tangerine-is-built/index.html @@ -0,0 +1,8 @@ + How Tangerine code is generated - Tangerine Documentation

How Tangerine code is generated

When the develop.sh script is run, the Dockerfile builds tangerine into dist/tangerine-client and copies the built code into builds/apk/www/shell and builds/pwa/release-uuid/app.

Building files

When Dockerfile is complete, it runs entrypoint-development.sh and watches for changes, sending its output to the dev directory:

./node_modules/.bin/ng build --watch --poll 100 --base-href ./ --output-path ./dev 
+

Copy files

If you need to make an apk using the updated code, run the following script:

cd /tangerine/client && \
+rm -rf builds/apk/www/shell && \
+rm -rf builds/pwa/release-uuid/app && \
+cp -r dev builds/apk/www/shell && \
+cp -r pwa-tools/updater-app/build/default builds/pwa && \
+cp -r dev builds/pwa/release-uuid/app
+

Potential workflow after updating a lib

In this workflow, you're testing changes to a library such as tangy-form. Make these changes inside the container in the node_modules directory for your library.

Run ./node_modules/.bin/ng build --base-href ./ --output-path ./dev inside the client dir in the container. It will rebuild all of the libs.

(Note that each time you run the ng build script above it removes the dev directory before building. This may cause problems when you try to list files in that directory. Do a cd .. & cd dev & ls -ls and all will be good.)

Next, run the script in the "Copy files" section to copy these generated build files to the correct location.

If this part of the chain is working, then check the output of the file copy process.

If all is good, release a new APK from the Tangerine UI.

Tips

The release-apk.sh script shows the steps when building an APK.

\ No newline at end of file diff --git a/developer/i18n-translation/index.html b/developer/i18n-translation/index.html new file mode 100644 index 0000000000..66968079df --- /dev/null +++ b/developer/i18n-translation/index.html @@ -0,0 +1,60 @@ + i18n/Translation - Tangerine Documentation

i18n/Translation

In Tangerine there are two kinds of translations, content translations and application translations. Content translations are embedded in form content by Editor Users using <t-lang> tags, while application translations are embedded in application level code using the t function in Web Components, _TRANSLATE function in an Angular TS file, or translate pipe in Angular component templates.

Content Translations

Translations for specific languages are embedded in content, thus portable and specific to that content. The <t-lang> component (https://github.com/ICTatRTI/translation-web-component) is used to detect the language assigned to the HTML doc. In the following example, the label on the hello input will be "Hello" if English is set as the language, "Bonjour" if French is selected as the language.

    <tangy-input 
+        name="hello"
+        label="
+            <t-lang en>Hello</t-lang>
+            <t-lang fr>Bonjour</t-lang>
+        "
+    >
+    </tangy-input>
+

Application Translations

In application code, instead of placing inline translations, a centrally managed JSON file is sourced for replacing strings. At ./translations/translation.fr.json you will find the JSON file use for translations when the French language is selected.

{
+    "Accuracy": "Précision",
+    "Accuracy Level": "Niveau de précision",
+    "Add New User": "Ajouter un nouvel utilisateur",
+    "Add User to Group": "Ajouter un utilisateur à un groupe",
+    ...
+}
+

You'll also find the Russian translation at ./translations/translation.ru.json.

{
+    "Accuracy": "Аккуратность",
+    "Accuracy Level": "Уровень аккуратности",
+    "Add New User": "Добавить нового пользователя",
+    "Add User to Group": "Add User to Group",
+    ...
+}
+

And many more. Each file defines an object where the keys are what to replace in the application and the values are what to replace strings with for that language. Depending on where in the application the string is, there are different techniques for exposing a string to translation.

In Web Components libraries such as <tangy-form> and <tangy-form-editor>, they use a special t function. Translating strings in template literals looks like...

this.shadowRoot.innerHTML = `
+  <h1>
+    ${t('Hello')}
+  </h1>
+  ...
+`
+

Often times Polymer templates are used which won't let you embed functions. In that case, in connectedCallback a this.t object is assembled and then used in the Polymer template.

    connectedCallback() {
+        super.connectedCallback()
+        this.t = {
+            hello: t("Hello")
+        }
+    }
+    template() {
+        return html`
+            [[t.hello]] 
+        `
+    }
+

In Angular Components, the translate pipe is available in templates and _TRANSLATE function for translating in TS files outside of templates.

<h1>
+    {{'Hello'|translate}}
+</h1>
+
    const helloString = _TRANSLATE('Hello')
+

Application Translation Workflow

  1. Add new translatable string(s) to ./translations/translation.en.json.
  2. With develop.sh running, run docker exec tangerine make-translations-consistent to spread this translateable to the other translation json files.
  3. With develop.sh running, run docker exec tangerine export-translations-csvs to spread this translateable to the other translation csv files.
  4. Commit changes to the translations folder.
  5. Send the translations CSVs to corresponding translator.
  6. When all translation CSVs have been updated, with develop.sh running, run docker exec tangerine import-translations-csvs to convert translation CSVs to JSON files.
  7. Add instructions to CHANGELOG upgrade notes that docker exec tangerine translations-update will need to be run to update all groups with updated translation files.

Other notes

Mat-pagination needs a special service to enable use of translation.json - see class/_services/mat-pagination-intl.service.ts

Right to left languages (RTL)

Mat-menu does not support RTL out of the box, but it's simple to get it working: add dir="rtl" to its enclosing element.

<span dir="rtl">&nbsp;&nbsp;&nbsp;
+  <button mat-button [matMenuTriggerFor]="reportsMenu" class="mat-button">{{'Select Report'|translate}}</button>
+  <mat-menu #reportsMenu="matMenu">
+    <button mat-menu-item [matMenuTriggerFor]="groupingMenu">Class grouping</button>
+  </mat-menu>
+  <mat-menu #groupingMenu="matMenu">
+    <button mat-menu-item *ngFor="let item of formList" routerLink="/reports/{{item.id}}/{{item.classId}}">{{item.title}}</button>
+  </mat-menu>
+</span>
+

mat-table also needs some twekas to work - Css:

.mat-column-Name {
+  padding-right:5px;
+}
+
+th.mat-header-cell {
+  text-align: right;
+}
+
\ No newline at end of file diff --git a/developer/index.html b/developer/index.html new file mode 100644 index 0000000000..c70239ccdd --- /dev/null +++ b/developer/index.html @@ -0,0 +1 @@ + Developer Guide Contents - Tangerine Documentation
\ No newline at end of file diff --git a/developer/install-multiple-apks-config/index.html b/developer/install-multiple-apks-config/index.html new file mode 100644 index 0000000000..b135a4eeec --- /dev/null +++ b/developer/install-multiple-apks-config/index.html @@ -0,0 +1,3 @@ + Installing Multiple Tangerine apps on the same tablet - Tangerine Documentation

Installing Multiple Tangerine apps on the same tablet

To install more than one Tangerine app on a tablet, you must configure the packageName and appName properties in the group's app-config.json.

"packageName": "org.rti.tangerine.custom",
+"appName": "Custom"
+

Using these properties would create an APK with the package name org.rti.tangerine.custom. The icon to launch the app would display "Custom".

When uninstalling the app, you would use the updated package name:

shell script adb uninstall org.rti.tangerine.custom

If you don't add these properties, the defaults are: - PACKAGENAME = "org.rti.tangerine" - APPNAME = "Tangerine"

\ No newline at end of file diff --git a/developer/load-testing/index.html b/developer/load-testing/index.html new file mode 100644 index 0000000000..f61466d217 --- /dev/null +++ b/developer/load-testing/index.html @@ -0,0 +1,28 @@ + Load testing - Tangerine Documentation

Load testing

Client-side testing

Generate a PWA. Go to any case record and enter the following in the js console:

this.caseService.generateCases(1)
+
You may change the number of cases generated. It uses the current case as a template for the generated cases. TODO: Use the case-export.json in the group.

You can check how many docs are in the db with:

this.userService.getSharedDBDocCount()
+

Select "Sync Online" to test syncing a large recordset.

Server-side generation

One may populate a vanilla Tangerine instance with records using the cli:

docker exec tangerine generate-uploads 500 group-uuid 2000 100
+

That command generates 500 sets (each of which has 2 records) in batches of 100, posted every 2000 ms. Each doc are generated from templates in server/src/scripts/generate-uploads.

Add the 'class' switch to the end of that command will generate a studentRegistrationDoc in addition to the other 2 docs. (Read server/src/scripts/generate-uploads/bin.js for more details.)

You may need to modify the templates to suit the docs you wish to generate.

Case generation

You may create a group for testing using the create-group command. See the creating clean dev conntent doc for more information. There is a case-module option that creates generic case forms. You may also use your own custom group forms.

Case generation uses a case-export.json file placed in the group directory as the template for record generation. To create this json file, generate a PWA and create a new case. While viewing the case, open the javascript console and use the copy(await this.caseService.export()) command to copy the json. Then paste this data into a case-export.json file. Please note that some groups, such as those created by the case-module mentioned earlier, already have a case-export.json file; however, you may be testing for different scenarios so feel free to create your own.

Create or modify the custom-generators.js file if you have different variable substitutions. This file exports: - customGenerators: An object that has custom functions you may define - customSubstitutions: An array of substitutions.

Here is an example of substitutions:

const substitutions = [
+    {
+        "type": "caseDoc"
+    },
+    {
+        "type": "demoDoc",
+        "formId": "registration-role-1",
+        "substitutions": {
+            "first_name": {
+                "functionName": "firstname",
+                "runOnce":"perCase"
+            },
+            "last_name": {
+                "functionName": "surname",
+                "runOnce":"perCase"
+            },
+            "consent": {
+                "functionName": "yes_no",
+                "runOnce": false
+            }
+        }
+    }
+]
+

In this example, there are two files that can have variable substitution: - caseDoc - This is the case manifest, which has the doc.type === 'case'. No substitutions are listed for this doc. - demoDoc - This is the demographics form that corresponds to the formId, which is "registration-role-1" in the example.

Since the current case-module example does not have any substitutions happening in the caseDoc inputs, there are no entries for substitutions in it. The demoDoc does have substitutions. The substitutions are key/value pairs. The substitutions key is the variable name of the input you wish to substitute, and the substitutions value is an object that may declare several properties: - functionName: how the function is called - runOnce: if the function is executed when the script is initialized per case, or when each doc is generated. the pre-built randomised field you wish to substitute.

In this example, first_name is being populated by the firstname function, which is run when each case is generated.

Case generation also performs other types of randomization. Here are some examples: - firstname: Randomises a female first name - surname: Randomizes a last name - tangerineModifiedOn: Today's date, offet by the running tally of docs being generated. The time is similarly offset. - day: day part of tangerineModifiedOn, padded with 0 if needed. - month: month part of tangerineModifiedOn, padded with 0 if needed. - year: year part of tangerineModifiedOn. - date: year + '-' + month + '-' + day; - participant_id: Random number under 1000000. - participantUuid: A UUID.

Before generating cases, create a device registration in order to properly generate a location property in the generated docs. When setting location, case generation uses the first doc in the group's devices database. If you don't have one, sync won't work properly. Case generation will fail if there are no device registrations.

To generate cases, use the following docker command:

docker exec tangerine generate-cases 1 group-uuid
+

This would generate one case. Change the number to generate more.

Clean things up

To delete all generated records (but keep the views), use bulkdelete.

\ No newline at end of file diff --git a/developer/modules/index.html b/developer/modules/index.html new file mode 100644 index 0000000000..03b36f7c0b --- /dev/null +++ b/developer/modules/index.html @@ -0,0 +1,6 @@ + Tangy Modules - Tangerine Documentation

Tangy Modules

Modules provide additional features to Tangerine, such as: - automatically add forms to the client when a new group is created (via groupNew hook) - data transformation for reporting (via flatFormResponse hook)

Modules: - Class

Steps to add a module - Create an index.js file inside server/src/modules/moduleName using the sample below as a guide. - Implement any relevant hooks. Available hooks: - flatFormResponse - groupNew - declareAppRoutes - clearReportingCache - reportingOutputs - Forms that need to be copied over to the client should be placed in server/src/modules/moduleName.

Activating modules

Add the module name to T_MODULES in config.sh. When a new group is created, the modules listed in T_MODULES will be added to the new group's app-config.json.

T_MODULES="['csv','sync-protocol-2','synapse','case']"
+

If you need to add a module to an existing group, modify the modules property in app-config.json/

   "modules" : [
+      "csv"
+   ],
+

Example module index.js

This example from the class module implements the flatFormResponse and groupNew hooks:

``` const clog = require('tangy-log').clog const fs = require('fs-extra')

module.exports = { hooks: { flatFormResponse: function(data) { return new Promise((resolve, reject) => { debugger; let formResponse = data.formResponse let flatFormResponse = data.flatFormResponse if (formResponse.metadata && formResponse.metadata.studentRegistrationDoc && formResponse.metadata.studentRegistrationDoc.classId) { let studentRegistrationDoc = formResponse.metadata.studentRegistrationDoc flatFormResponse[sr_classId] = studentRegistrationDoc.classId; flatFormResponse[sr_student_name] = studentRegistrationDoc.student_name; flatFormResponse[sr_student_id] = studentRegistrationDoc.id; flatFormResponse[sr_age] = studentRegistrationDoc.age; flatFormResponse[sr_gender] = studentRegistrationDoc.gender; } resolve({flatFormResponse, formResponse}) }) }, groupNew: function(data) { return new Promise(async (resolve, reject) => { const {groupName, appConfig} = data clog("Setting homeUrl to dashboard and uploadUnlockedFormReponses to true.") appConfig.homeUrl = "dashboard" appConfig.uploadUnlockedFormReponses = true // copy the class forms try { await fs.copy('/tangerine/server/src/modules/class/', /tangerine/client/content/groups/${groupName}) clog("Copied class module forms.") } catch (err) { console.error(err) } resolve(data) }) }, } } ```

This code will be automatically run when the TangyModules (server/src/modules/index.js) is run.

Hooks

Example:

const data = await tangyModules.hook('groupNew', {groupName, appConfig})
+
\ No newline at end of file diff --git a/developer/reporting-mango-tips/index.html b/developer/reporting-mango-tips/index.html new file mode 100644 index 0000000000..57063b90d6 --- /dev/null +++ b/developer/reporting-mango-tips/index.html @@ -0,0 +1,24 @@ + Tips for making queries for Reports using Mango - Tangerine Documentation

Tips for making queries for Reports using Mango

Mango is a query language available to Couchdb based upon MongoDB.

General info about Mango:

Some things to watch out for:

Sorting

Add the key you're sorting upon - in the following case, it is tangerineModifiedOn - to the index:

 await createIndex({
+    index: {
+      fields: [
+        'type',
+        'status',
+        'tangerineModifiedOn'
+      ]
+    }
+  })
+

$or and $ne

Mango abandons the indexes and does live queries, which can cause the query to fail. For $ne you can do a partial filter to improve performance.

Here is a good discussion of the issue w/ these Mango expressions: https://stackoverflow.com/a/41897093

Here is an example of this issue in Tangerine: #2367

Example lifted from the Couch doc on Partial Indexes:

To improve response times, we can create an index which excludes documents where "status": { "$ne": "archived" } at index time using the "partial_filter_selector" field:
+
+{
+  "index": {
+    "partial_filter_selector": {
+      "status": {
+        "$ne": "Open"
+      }
+    },
+    "fields": ["type", "status", "tangerineModifiedOn"]
+  },
+  "ddoc" : "type-not-open",
+  "type" : "json"
+}
+
\ No newline at end of file diff --git a/developer/reverse-proxy-for-developers/index.html b/developer/reverse-proxy-for-developers/index.html new file mode 100644 index 0000000000..b3dc053f18 --- /dev/null +++ b/developer/reverse-proxy-for-developers/index.html @@ -0,0 +1,18 @@ + Reverse Proxy for Developers - Tangerine Documentation

Reverse Proxy for Developers

Reverse proxy software

local-ssl-proxy is a Node.js app that can be used to proxy requests from a local development server to a remote server over HTTPS. This is an alternative to using a reverse proxy tunnel service such as ngrok.io or tunnelto.dev.

Generate SSL certificates

Here's a nice primer on creating a self-signed SSL certificate: https://deliciousbrains.com/ssl-certificate-authority-for-local-https-development/ I've lifted these examples from that article. Although this example focuses on MacOS, the primer in the link has examples for Linux and Windows.

In the following example, the code creates a key and cert for a local dev server named tangy.test. Note that this script does not have the -des3 switch, which forces the use of a password, because the script is intended for use with local development servers.

openssl genrsa -out tangy.test.key 2048

You'll answer a bunch of questions. The most important one is the Common Name (e.g. server FQDN or YOUR name). Enter the name of your local dev server here, e.g. tangy.test.

openssl req -new -key tangy.test.key -out tangy.test.csr

You should now have two files: myCA.key (your private key) and myCA.pem (your root certificate).

Adding the Root Certificate to macOS Keychain

sudo security add-trusted-cert -d -r trustRoot -k "/Library/Keychains/System.keychain" myCA.pem

The tutorial has examples of adding the root certs to other devices, which might be handy for Android and IOS development.

Creating CA-Signed Certificates

Now create tangy.test.ext:

authorityKeyIdentifier=keyid,issuer
+basicConstraints=CA:FALSE
+keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
+subjectAltName = @alt_names
+
+[alt_names]
+DNS.1 = tangy.test
+

The final step:

openssl x509 -req -in tangy.test.csr -CA myCA.pem -CAkey myCA.key \ -CAcreateserial -out tangy.test.crt -days 825 -sha256 -extfile tangy.test.ext

We now have three files: tangy.test.key (the private key), tangy.test.csr (the certificate signing request, or csr file), and tangy.test.crt (the signed certificate). We can configure local web servers to use HTTPS with the private key and the signed certificate.

Using local-ssl-proxy

At this point you can launch Tangerine, which will respond to requests on port 80. Then launch local-ssl-proxy:

local-ssl-proxy --source 443 --target 80 --cert ~/ssl/server.crt --key ~/ssl/server.key

You should be able to access Tangerine via https://localhost. Next step - configure your local dev domain in DNS:

DNS settings

Add your local dev domain to /etc/hosts. The domain 'tangy.test' is used in this example; replace with your own domain:

##
+# Host Database
+#
+# localhost is used to configure the loopback interface
+# when the system is booting.  Do not change this entry.
+##
+127.0.0.1       localhost
+255.255.255.255 broadcasthost
+::1             localhost
+127.0.0.1       tangy.test
+

Now you should be able to access Tangerine using https://tangy.test`.

\ No newline at end of file diff --git a/developer/supporting-custom-elements/index.html b/developer/supporting-custom-elements/index.html new file mode 100644 index 0000000000..4deb54e8f3 --- /dev/null +++ b/developer/supporting-custom-elements/index.html @@ -0,0 +1,6 @@ + Supporting custom elements and external libs - Tangerine Documentation

Supporting custom elements and external libs

Adding new elements

To add a new custom element or to add support for new polymer or other web components, you must make them accessible to Angular: - add to package.json - import into polyfills

You also need to add CUSTOM_ELEMENTS_SCHEMA to your module to support custom tags in your templates:

schemas: [CUSTOM_ELEMENTS_SCHEMA],
+

Manual imports

Some libs need to be imported manually because they are not available as ES6 modules. Add the lib using a script tag -

<script src="./libs/plotly-latest.min.js"></script>
+

and then add to angular.json:

"assets": [
+    "src/libs/plotly-latest.min.js"
+]
+

Resolving incompatibilities

The "skipLibCheck": true switch in tscondig.json will causes type checking of declaration files (files with extension .d.ts) to be skipped. (Stack Overflow discussion) If you run into an error such as ERROR in node_modules/tangy-form/tangy-form-response-model.d.ts:18:17 - error TS1039: Initializers are not allowed in ambient contexts. this switch may be useful to getting the app to compile. We decided to not use it - simply removing it worked fine - but if it's a choice between the app compiling or not, it's worth using.

\ No newline at end of file diff --git a/developer/sync-sessions/index.html b/developer/sync-sessions/index.html new file mode 100644 index 0000000000..f51b1013cd --- /dev/null +++ b/developer/sync-sessions/index.html @@ -0,0 +1,6 @@ + Sync Sessions - Tangerine Documentation

Sync Sessions

A deviceToken, which is persisted in the client device record, is used for authentication with the server and is passed in the syncSessionUrl by sync.service. This is passed using the following code:

const syncSessionInfo = <SyncSessionInfo>await this.http.get(`${syncDetails.serverUrl}sync-session-v2/start/${syncDetails.groupId}/${syncDetails.deviceId}/${syncDetails.deviceToken}`).toPromise()
+

The syncSessionService.start() method verifies the token, starts a sync session, and returns the following object:

<SyncSessionInfo>{
+        syncSessionUrl: `${config.protocol}://${syncUsername}:${syncPassword}@${config.hostName}/db/${groupId}`,
+        deviceSyncLocations: device.syncLocations
+      }
+

This syncSessionUrl is used to create the connection to the Couchdb for replication.

\ No newline at end of file diff --git a/developer/tangerine-dev-tutorial-notes/index.html b/developer/tangerine-dev-tutorial-notes/index.html new file mode 100644 index 0000000000..e0c32a6da1 --- /dev/null +++ b/developer/tangerine-dev-tutorial-notes/index.html @@ -0,0 +1,12 @@ + Commands - Tangerine Documentation

Commands

Create group and generate case(es)

docker exec tangerine create-group "CM-1" case-module
+
+docker exec tangerine generate-cases 1 group-09a2a880-1317-4cbf-b944-0fd059fa7007
+

Open docker shell

docker exec -it tangerine bash
+

Refresh code

Run this inside tangerine docker shell

cd /tangerine/client && rm -rf builds/apk/www/shell 
+&& rm -rf builds/pwa/release-uuid/app && cp -r dev builds/apk/www/shell 
+&& cp -r pwa-tools/updater-app/build/default builds/pwa 
+&& cp -r dev builds/pwa/release-uuid/app
+

ADB commands

adb devices
+
adb install data/client/releases/prod/apks/group-09a2a880-1317-4cbf-b944-0fd059fa7007/platforms/android//app/build/outputs/apk/debug/app-debug.apk
+
adb uninstall org.rti.tangerine
+
\ No newline at end of file diff --git a/developer/tangerine-globals/index.html b/developer/tangerine-globals/index.html new file mode 100644 index 0000000000..62cbc462cf --- /dev/null +++ b/developer/tangerine-globals/index.html @@ -0,0 +1,5 @@ + Globals in Tangerine - Tangerine Documentation

Globals in Tangerine

Globals in memory

In-memory globals won't survive refreshing the browser.

We are caching important configuration files (app-config.json, forms.json, location-list.json) to avoid having to keep fetching those docs from the db.

Use the following code to take advantage of this caching: - await this.appConfigService.getLocationList(); - await this.tangyFormsInfoService.getFormsInfo(); - await this.appConfigService.getAppConfig; - await this.tangyFormService.getFormMarkup(this.eventFormDefinition.formId);

CaseDefinitionsService also has implements of caseDefinitions, but that is not exposed publicly. More info in this PR: https://github.com/Tangerine-Community/Tangerine/pull/1991

Globals that are stored in a database

Database variables will persist after page refreshes or app reboots.

Use VariableService. Stores data in 'tangerine-variables' pouchdb as a key/value pair. The key is the _id in the doc. The value can be a string, JSON object, or any other data type that can be persisted in a pouchdb.

await this.variableService.set('tangerine-device-is-registered', true)
+
await this.variableService.get('tangerine-device-is-registered')
+

Widely-used Configuration Variables

Server

They are not globals, but they are mighty useful. The TangerineConfigService provides variables set in config.sh. Expose it in your constructor:

private readonly configService: TangerineConfigService,
+
And then you may use it:

const userOneUsername = this.configService.config().userOneUsername
+

Client

Use await this.appConfigService.getAppConfig; to fetch app-config.json settings in client.

\ No newline at end of file diff --git a/developer/testing-conflicts/index.html b/developer/testing-conflicts/index.html new file mode 100644 index 0000000000..e5d47d44a4 --- /dev/null +++ b/developer/testing-conflicts/index.html @@ -0,0 +1,31 @@ + Conflicts - Tangerine Documentation

Conflicts

The goal is to follow the CRDT (conflict-free replicated data type) pattern in resolving conflicts. When the app tries to merge two conflicting records, how should it sync the conflicting values: which value should win? The afore-mentioned Wikipedia page offers some guidance: "As an example, a one-way Boolean event flag is a trivial CRDT: one bit, with a value of true or false. True means some particular event has occurred at least once. False means the event has not occurred. Once set to true, the flag cannot be set back to false. (An event, having occurred, cannot un-occur.) The resolution method is "true wins": when merging a replica where the flag is true (that replica has observed the event), and another one where the flag is false (that replica hasn't observed the event), the resolved result is true — the event has been observed."

We have not yet reached this level of conflict resolution. We have first started with comparing data from Event Forms, detecting some basic conflicts such as missing formResponseId, complete, or required properties or detecting if there is a new event form and then merging according to rules specific to each difference. In general, the event form conflicts are resolved by adding the missing property or form. For metadata that are in conflict, the most recent metadata is merged (wins). There is also a check for new events.

Unit tests are available that test the conflicts mentioned above. You can also create scenarios on a tablet.

Testing Conflicts on a tablet

Tips

After each scenario, it is useful to run Sync to make sure that no more docs need to be sync'd:

Status: Complete
+   Pulled from the server: 0
+   Pushed to the server: 0
+

Supported Scenarios

DiffType: EventForm - Tablet 1 opens but doesn't complete Event Form, Tablet 2 opens and completes Event Form

Steps

Setup: - Create a new case with pwa1. Fill out "Registration for Role 1" - enter '0' for "How many participant of type Role 2 would you like to enroll in this case?", submit. Create a New Event of type "An Event with an event form you can delete". Sync. - In PWA2, sync.

Create a divergence: - In PWA1, open the form and exit (don't submit form). This should create a diverging tree in the revisions. - In PWA2, Enter the case you just synced. Enter the event of type "An Event with an event form you can delete" and complete the form in "An Event with an Event Form you can delete." Sync.

Syncing to create the conflict: - In PWA1, Sync. This should create a conflict. Note that Sync status displays "Conflicts detected." This conflict is resolved on the client and sync'd to the server. - Check data/issues on server. There should be a new issue, which should display the following:

Merged: true
+DiffTypes:
+    (1) DIFF_TYPE__METADATA
+
- In PWA2, sync. This should NOT create a conflict. - Check to see that data is identical on both PWA's.

DiffType: Event - Tablet 1 creates an new Event and Tablet 2 creates a new Event

Steps

Setup: - Create a new case with PWA1. Fill out "Registration for Role 1" - enter '0' for "How many participant of type Role 2 would you like to enroll in this case?", submit. Sync - In PWA2, sync. Enter the case you just synced. Create a New Event of type "An Event with an event form you can delete" and complete the form in "An Event with an Event Form you can delete." Sync.

Create a divergence: - In PWA1, enter the same case (don't sync yet) and create a New Event of type "An Event with an event form you can delete". Complete the form in "An Event with an Event Form you can delete."

Syncing to create the conflict: - In PWA1, sync. This should create a conflict. Note that Sync status displays "Conflicts detected." This conflict is resolved on the client and sync'd to the server. - Check data/issues on server. There should be a new issue, which should display the following:

Merged: true
+DiffTypes:
+
+    (1) DIFF_TYPE__EVENT
+    (1) DIFF_TYPE__EVENT_FORM
+    (1) DIFF_TYPE__METADATA
+

  • Check the case on PWA1. There should be 2 instances of "An Event with an Event Form you can delete" - one from PWA1, and another from PWA2.
  • In PWA2, sync. This should create a conflict. Note that Sync status displays "Conflicts detected." This conflict is resolved on the client and sync'd to the server.
  • Check data/issues on server. There should be a new issue, which should display the following:
    Merged: true
    +DiffTypes:
    +
    +    (1) DIFF_TYPE__EVENT
    +    (1) DIFF_TYPE__EVENT_FORM
    +    (1) DIFF_TYPE__METADATA
    +
  • Check the case on PWA2. There should be 2 instances of "An Event with an Event Form you can delete" - one from PWA1, and another from PWA2.

DiffType: EventForm - Tablet 1 creates a new Event Form and Tablet 2 makes some other change

Steps

Setup: - Create a new case with PWA1. Fill out "Registration for Role 1" - enter '0' for "How many participant of type Role 2 would you like to enroll in this case?", submit. Create a New Event of type "An Event with an event form you can delete"." Open that new event but do not click on the form "An Event Form you can delete". Sync. - In PWA2, sync. Enter the case you just synced. View the "Registration for Role 1" form. Sync.

Create a divergence: - In PWA1, Enter the same case (don't sync yet) and enter a New Event of type "An Event with an event form you can delete". Complete the form in "An Event with an Event Form you can delete."

Syncing to create the conflict: - In PWA1, sync. This should create a conflict. Note that Sync status displays "Conflicts detected." This conflict is resolved on the client and sync'd to the server. - Check data/issues on server. There should be a new issue, which should display the following:

Merged: true
+DiffTypes:
+
+(1) DIFF_TYPE__METADATA
+
- In PWA2, sync. This should create a conflict. Note that Sync status displays "Conflicts detected." This conflict is resolved on the client and sync'd to the server. - Check data/issues on server. There should be a new issue, which should display the following:
Merged: true
+DiffTypes:
+
+    (1) DIFF_TYPE__METADATA
+
- Check the case on PWA2. There should be 1 instances of "An Event with an Event Form you can delete" - with the form completed from PWA1

DiffType: Metadata - Change location on Tablet 1 and Tablet 2

Steps: - In PWA1, pull up the case you just created. Submit a "Change Location of Case" form, setting it for Facility 1. Don't Sync. - In PWA2, pull up the same case. Submit a "Change Location of Case" form, setting it for Facility 2. Sync. - In PWA1, sync. Note the error displayed:

Status: Error
+4 docs synced; 0 pending; ERROR: "Document update conflict"
+
Sync again. Note that Sync status displays "Conflicts detected." This conflict is resolved on the client and sync'd to the server.

  • In PWA1, pull up the case. Note that there are two "Change location of case" forms, one for Facility 1 and another for Facility 2. In the js console, enter T.case._case.location.facility. It should display "K0xhy1Su".
  • In PWA2, sync. Note the error displayed:

    Status: Error
    +4 docs synced; 0 pending; ERROR: "Document update conflict"
    +
    Sync again. Note that Sync status displays "Conflicts detected." This conflict is resolved on the client and sync'd to the server.

  • In PWA2, pull up the case. Note that there are two "Change location of case" forms, one for Facility 1 and another for Facility 2. In the js console, enter T.case._case.location.facility. It should display "K0xhy1Su".

DiffType: Metadata - Modify Case variables on Tablet 1 and Tablet 2

TODO: Create a form in the Case Module that uses setVariable and getVariable function

Scenarios not yet supported

DiffType: EventForm - Tablet 1 removes an Event Form and Tablet 2 makes some other change

DiffType: EventForm - Tablet 1 makes an Event Form required and Tablet 2 makes some other change

DiffType: EventForm - Tablet 1 makes adds an Event Form variable and Tablet 2 makes some other change

DiffType: EventForm - Tablet 1 makes modifies existing Event Form variable and Tablet 2 makes some other change

DiffType: EventForm - Tablet 1 makes modifies existing Event Form variable and Tablet 2 modifies the same Event Form variable with same value

DiffType: EventForm - Tablet 1 makes modifies existing Event Form variable and Tablet 2 modifies the same Event Form variable with different value

Exploring unexpected sync conflicts

DiffType: Metadata - Two cases view the same case but make no modification

A metadata conflict is easy to create: whenever a case is viewed, its metadata is modified.

Steps: - launch 2 PWA's with the group, based on the case module - docker exec tangerine create-group "Test Auto-merge 1" case-module - consider editing the "Registration for Role 1" "Registration" section by changing the QR code into an input, just to make testing easier. - Create a new case with pwa1. Fill out "Registration for Role 1" - enter '0' for "How many participant of type Role 2 would you like to enroll in this case?", submit, and sync. - in PWA2, sync, and open the new case. Create a New Event of type "An Event with an event form you can delete" . Go into the event and form and submit the "An Event Form you can delete" form. Sync. Notice that so far, no new conflicts have been created. - In PWA1, sync. Note that Sync status displays "Conflicts detected." Check data/issues on server - should be type (1) DIFF_TYPE__METADATA. Merged: true. - In PWA2, sync. Note that Sync status displays "Pulled from the server: 1".

DiffType: EventForm - data conflict 1 - Don't touch the event

So far, this has not made a conflict for me...

Steps: - Create a new case with pwa1. Fill out "Registration for Role 1" - enter '0' for "How many participant of type Role 2 would you like to enroll in this case?", submit. On the same case, create a New Event of type "An Event with an event form you can delete". Don't view that event or enter data in its form. Sync. - In PWA2, sync. Enter the case you just synced and complete the form in "An Event with an Event Form you can delete." Sync. - In PWA1, sync. The new form does not appear. Do a hard refresh. The new form should now appear in the case. Sync. - In PWA2, sync. Conflicts arise. Or not. Check data/issues on server - should be type (1) DIFF_TYPE__METADATA. Merged: true. There is a 50/50 chance this record won't have a conflict...

DiffType: EventForm - data conflict 2 - Touch the event

So far, this has not made a conflict for me...

Steps: - Create a new case with pwa1. Fill out "Registration for Role 1" - enter '0' for "How many participant of type Role 2 would you like to enroll in this case?", submit. On the same case, create a New Event of type "An Event with an event form you can delete". View the event, but don't view the form. Sync. - In PWA2, sync. Enter the case you just synced and complete the form in "An Event with an Event Form you can delete." Sync. - In PWA1, sync. The new form does not appear. Do a hard refresh. The new form should now appear in the case. Sync. - In PWA2, sync. Conflicts arise. Or not. Check data/issues on server - should be type (1) DIFF_TYPE__METADATA. Merged: true. There is a 50/50 chance this record won't have a conflict...

DiffType: EventForm - data conflict 3 - Open but don't save the form

Steps: - Create a new case with pwa1. Fill out "Registration for Role 1" - enter '0' for "How many participant of type Role 2 would you like to enroll in this case?", submit. On the same case, create a New Event of type "An Event with an event form you can delete". View the event, then view the form, but don't submit it. Sync. - In PWA2, sync. Enter the case you just synced and complete the form in "An Event with an Event Form you can delete." Sync. - In PWA1, sync. The new form does not appear. Do a hard refresh. The new form should now appear in the case. Sync. - In PWA2, sync. Conflicts arise. Or not. Check data/issues on server - should be type (1) DIFF_TYPE__METADATA. Merged: true. There is a 50/50 chance this record won't have a conflict... - So far, this has not made a conflict for me...

\ No newline at end of file diff --git a/developer/upgrades/index.html b/developer/upgrades/index.html new file mode 100644 index 0000000000..b2430603aa --- /dev/null +++ b/developer/upgrades/index.html @@ -0,0 +1,58 @@ + Upgrades - Tangerine Documentation

Upgrades

Implementing Upgrades

There are two ways to implement upgrades to Tangerine configuration or databases that cannot be automatically upgraded: - via a shell script, runnable via docker exec -it tangerine /tangerine/server/src/upgrade/v3.9.0.js - adding to the updates.ts array. This will run automatically when the client app starts if a new version was deployed.

Upgrading a server

You must run each version's server image before running its upgrade. for example, if you are upgrading from v3.6.4 to v3.13.0, follow these steps:

git clone https://github.com/Tangerine-Community/Tangerine.git
+cd Tangerine
+git fetch origin
+git checkout v3.6.4
+cp config.defaults.sh config.sh
+# configure T_HOST_NAME, T_PROTOCOL (https), and T_MODULES (csv)
+./start.sh v3.6.4
+docker exec -it tangerine reporting-cache-clear
+git checkout v3.7.1
+./start.sh v3.7.1
+docker exec tangerine translations-update
+git checkout v3.8.0
+./start.sh v3.8.0
+docker exec -it tangerine /tangerine/server/src/upgrade/v3.8.0.js
+git checkout v3.8.1
+mv config.sh config.sh_backup
+cp config.defaults.sh config.sh
+# To edit both files in vim you would run...
+vim -O config.sh config.sh_backup
+# No upgrade script for this relese.
+git checkout v3.9.0
+./start.sh v3.9.0
+docker exec -it tangerine /tangerine/server/src/upgrade/v3.9.0.js
+git checkout v3.10.0
+./start.sh v3.10.0
+docker exec -it tangerine /tangerine/server/src/upgrade/v3.10.0.js
+git checkout v3.11.0
+./start.sh v3.11.0
+docker exec -it tangerine /tangerine/server/src/upgrade/v3.11.0.js
+# There was a bug in 3.11.0 that causes a blank screen in earlier APK's
+# It is resolved in v3.12.
+git checkout v3.12.0
+./start.sh v3.12.0
+# Run upgrade
+docker exec -it tangerine reporting-cache-clear 
+git checkout v3.13.0
+./start.sh v3.13.0
+docker exec -it tangerine reporting-cache-clear 
+docker exec -it tangerine /tangerine/server/src/upgrade/v3.13.0.js
+# Remove previous Tangerine version's Docker images.
+docker rmi tangerine/tangerine:v3.6.4
+docker rmi tangerine/tangerine:v3.8.0
+docker rmi tangerine/tangerine:v3.8.1
+docker rmi tangerine/tangerine:v3.9.0
+docker rmi tangerine/tangerine:v3.10.0
+docker rmi tangerine/tangerine:v3.11.0
+docker rmi tangerine/tangerine:v3.13.0
+

Limits to Upgrades

Read the instructions in the CHANGELOG.md.

If testing a pre-v3.8 APK, it will fail to run on v3.8 or higher due to lack of newer Cordova plugins.

Update tips

Be aware that sync-protocol 2 uses a shared database; therefore, you don't want to do the same update whenever a different user logs in.

The requiresViewsRefresh property will update All Default User Docs, which may place too much load on the tablet when it re-indexes those views.

The following code checks for that scenario and show how to update a single view:

  {
+    requiresViewsUpdate: false,
+    script: async (userDb, appConfig, userService: UserService) => {
+      // syncProtocol uses a single shared db for all users. Update only once.
+      if (appConfig.syncProtocol === '2' && localStorage.getItem('ran-update-v3.9.0')) return
+      console.log('Updating to v3.9.0...')
+      await userDb.put(TangyFormsDocs[0])
+      await userDb.query('responsesUnLockedAndNotUploaded')
+      localStorage.setItem('ran-update-v3.9.0', 'true')
+    }
+
\ No newline at end of file diff --git a/developer/viewing-forms-and-data/index.html b/developer/viewing-forms-and-data/index.html new file mode 100644 index 0000000000..57e8bc1d44 --- /dev/null +++ b/developer/viewing-forms-and-data/index.html @@ -0,0 +1,23 @@ + Viewing Forms and Form Data - Tangerine Documentation

Viewing Forms and Form Data

Use TangyFormService to retrieve form definitions and response data. The revision is used to get the correct version of the form.

    this.formResponse = await this.tangyFormService.getResponse(this.eventForm.formResponseId)
+    const tangyFormMarkup = await this.tangyFormService.getFormMarkup(this.eventFormDefinition.formId, this.formResponse.formVersionId)
+

But there are other ways of getting data out of Tangerine. First you need to see where you are getting data from.

Mapping of components to forms

EventFormListItemComponent - listing of forms in an event CaseEventListItemComponent - listing of events (such as Followup ANC Visits) in a case.

Helper functions already in components

In the component for a list, helper functions may already expose the properties you need to populate a template. In EventFormListItemComponent, notice the variable exposed:

    const response = await this.formService.getResponse(this.eventForm.formResponseId)
+    const getValue = (variableName) => {
+// more code inside getValue();
+      }, {})
+// snip
+    const caseInstance = this.case
+    const caseDefinition = this.caseDefinition
+    const caseEventDefinition = this.caseEventDefinition
+    const caseEvent = this.caseEvent
+    const eventForm = this.eventForm
+    const eventFormDefinition = this.eventFormDefinition
+    const formatDate = (unixTimeInMilliseconds, format) => moment(new Date(unixTimeInMilliseconds)).format(format)
+    const TRANSLATE = _TRANSLATE
+    eval(`this.renderedTemplateListItemIcon = this.caseDefinition.templateEventFormListItemIcon ? \`${this.caseDefinition.templateEventFormListItemIcon}\` : \`${this.defaultTemplateListItemIcon}\``)
+    eval(`this.renderedTemplateListItemPrimary = this.caseDefinition.templateEventFormListItemPrimary ? \`${this.caseDefinition.templateEventFormListItemPrimary}\` : \`${this.defaultTemplateListItemPrimary}\``)
+    eval(`this.renderedTemplateListItemSecondary = this.caseDefinition.templateEventFormListItemSecondary ? \`${this.caseDefinition. v}\` : \`${this.defaultTemplateListItemSecondary}\``)
+
If there is not a response for a form, response will be false; therefore, if you do a getValue() in your template, be sure to test if response is true.

If you wish to display the startDatetime in your template, note that is is part of the response object - it is returned as response.startDatetime. In other cases - for values inside the form - use getValue(variableName) - but test if response is true first! Also, remember that the variableName is one of the id's in the inputs array, which is inside each item in the items array.

Testing your templates

Here's an easy way to test your template code: in the js console, use the copy() function to copy the value for your template:

copy(this.caseDefinition.templateEventFormListItemSecondary)
+
Then add the fields or functions you need. In this case, I'm adding a getValue:

`<t-lang en>Status</t-lang><t-lang fr>Statut</t-lang>: ${!eventForm.complete ? '<t-lang en>Incomplete</t-lang><t-lang fr>Incomplète</t-lang>' : '<t-lang en>Complete</t-lang><t-lang fr>Achevée</t-lang>'} ${response ? `Version: ${getValue("content_release_version")}`: ''}`
+

Output:

"<t-lang en>Status</t-lang><t-lang fr>Statut</t-lang>: <t-lang en>Complete</t-lang><t-lang fr>Achevée</t-lang> Start date: 3/13/2020, 11:25:19 AM"
+

Note that I was testing for existence of response, and also nesting templates to show the "Version" text if there was a value for content_release_version.

Another example:

<t-lang en>Status</t-lang><t-lang fr>Statut</t-lang>: ${!eventForm.complete ? '<t-lang en>Incomplete</t-lang><t-lang fr>Incomplète</t-lang>' : '<t-lang en>Complete</t-lang><t-lang fr>Achevée</t-lang>'} ${response ?Start date: ${response.startDatetime}: ''}

debugging templates

To make the dev tool stop on a breakpoint in a Case Definition's template, add the following debugger statement to the content of the template.

${(()=>{debugger})()}
+

alt text

When that template loads, the Chrome devtools will pause and you can inspect local variables/functions available and try running them in the console. Note that different templates will have different helper functions and variables available.

alt text

\ No newline at end of file diff --git a/editor/advanced-form-programming/globals/index.html b/editor/advanced-form-programming/globals/index.html new file mode 100644 index 0000000000..86837a9ef6 --- /dev/null +++ b/editor/advanced-form-programming/globals/index.html @@ -0,0 +1,40 @@ + Global Variables - Tangerine Documentation

Global Variables

Tangerine-specific variables are available in the T global variable. These are exposed in app.component.ts. Example:

this.window.T = {
+      form: {
+        Get: Get
+      },
+      router,
+      http,
+      user: userService,
+      lockBox: lockBoxService,
+      syncing: syncingService,
+      syncCouchdbService: syncCouchdbService, 
+      sync: syncService,
+      appConfig: appConfigService,
+      update: updateService,
+      search: searchService,
+      device: deviceService,
+      tangyFormsInfo: tangyFormsInfoService,
+      tangyForms: tangyFormService,
+      formTypes: formTypesService,
+      case: caseService,
+      cases: casesService,
+      caseDefinition: caseDefinitionsService,
+      languages: languagesService,
+      variable: variableService,
+      classForm: classFormService,
+      classDashboard: dashboardService,
+      translate: window['t']
+    }
+

Additional T properties may be added in other parts of the Tangerine codebase.

Usage

Examples of T global usage are throughout these docs, but here are a few:

To load and query the client database with options to get a specific revision:

const db = await T.user.getUserDatabase()
+db.get('foo',{rev:'4-uuid', latest:false})
+

When writing queries or organizing the javascript logic to fetch the results, use the globally-exposed T.form.Get function to get the value of inputs; this will save you from having to wrote deeply nested code (doc.items[0].inputs[3].value[0].value)

T.form.Get(doc, 'consent')

// 3 ingredients are needed to set an Event Variable.
+const eventId = '123'
+const variableName = 'foo'
+const variableValue = 'bar'
+
+// Set Event Variable.
+T.case.setVariable(`${eventId}-${variableName}`, variableValue)
+
+// Get Event Variable.
+const shouldBeValueOfBar = T.case.getVariable(`${eventId}-${variableName}`)
+

There is an older document, Tangerine globals, that describes some of the functions that are now attached to the T global.

\ No newline at end of file diff --git a/editor/advanced-form-programming/index.html b/editor/advanced-form-programming/index.html new file mode 100644 index 0000000000..65bd93a3af --- /dev/null +++ b/editor/advanced-form-programming/index.html @@ -0,0 +1 @@ + Overview - Tangerine Documentation

Overview

Tangerine contains an extremely extensible foundational framework that allows the form developer to highly customize the core functionality, actions, and progression. The following section provides guidance and common examples for extending Tangerine.

TODO: INSERT TABLE OF CONTENTS FOR THIS SECTION AND HIGH-LEVEL OVERVIEW

\ No newline at end of file diff --git a/editor/advanced-form-programming/kiosk-or-fullscreen-modes/index.html b/editor/advanced-form-programming/kiosk-or-fullscreen-modes/index.html new file mode 100644 index 0000000000..cff29e824f --- /dev/null +++ b/editor/advanced-form-programming/kiosk-or-fullscreen-modes/index.html @@ -0,0 +1 @@ + Kiosk or Fullscreen modes - Tangerine Documentation

Kiosk or Fullscreen modes

Kiosk

Kiosk mode is enabled app-wide by adding "kioskMode": true to the group's app-config.json file. This enables the 'Kiosk Mode' item in the menu. Clicking this item sets kioskModeEnabled to true and removes the top toolbar.

The app-config.json property - exitClicks - enables admin to set number of clicks to exit kioskMode. Default is 5. User must click the top of the screen 5 times within 2 seconds.

Fullscreen mode

Fullscreen mode is activated at the form level by setting "fullscreen": true in tangy-form editor. The current code employs a workaround to deal with a bug in APK's that prevents exit fullscreen from working by using a listener for 'enter-fullscreen' or 'exit-fullscreen' to set this.kioskModeEnabled = true or false, which removes the top bar.

\ No newline at end of file diff --git a/editor/advanced-form-programming/local-content-development/index.html b/editor/advanced-form-programming/local-content-development/index.html new file mode 100644 index 0000000000..9bd32cb8bb --- /dev/null +++ b/editor/advanced-form-programming/local-content-development/index.html @@ -0,0 +1,10 @@ + Local content development - Tangerine Documentation

Local content development

Local content development with Tangerine Preview

Tangerine Preview is a command line tool for previewing the Tangerine content you are working on your local computer. It work on Windows, Mac, and Linux.

Install

Before you install tangerine-preview, make sure to install node.js.

If you are on macOS, you will need to set permissions to allow for global installs by running the following command.

sudo chown -R `whoami` /usr/local/lib/node_modules
+

For all platforms, open a command line terminal and run Tangerine Preview install command.

npm install -g tangerine-preview
+

Preview your content

Open a command prompt, change directory to your content that you would like to preview, then run the tangerine-preview command.

cd your-project
+tangerine-preview
+

Lastly, open Google Chrome to http://localhost:3000

As you make content changes, they will be synced to the app. Reload your web browser and you'll see the changes.

Update tangerine-preview

When new releases come out for tangerine, tangerine-preview will also be updated. To update, open a command prompt and run the install command again.

If you have installed tangerine-preview in the past, you'll need to uninstall it first.

npm install -g tangerine-preview
+

The following will install tangerine-preview at the most recent version.

npm install -g tangerine-preview
+

If you need a specific version of tangerine-preview, you can specify the version when installing. For example...

npm install -g tangerine-preview@3.18.5
+

Check your currently install version

npm list -g tangerine-preview
+

Set up VS Code with Syntax Highlighting for on-open, on-change, etc.

Ideally we would have a VS Code plugin for you to install. Until then, this is our workaround. If you are interested in helping with the development of a VS Code plugin, feel free to reach out to us via the issue queue.

Step 1

Open Visual Studio configuration file. If you have the VS Code CLI installed, run the following in a terminal.

code /Applications/Visual\ Studio\ Code.app/Contents/Resources/app/extensions/html/syntaxes/html.tmLanguage.json
+

Step 2

Find on(s(c and replace with on-open|on-change|on-submit|skip-if|hide-if|dont-skip-if|disable-if|valid-if|discrepancy-if|warn-if|on(s(c.

Screen Shot 2020-02-13 at 11 58 59 AM

Step 3

Restart Visual Studio.

\ No newline at end of file diff --git a/editor/assets/backup-files-listing.png b/editor/assets/backup-files-listing.png new file mode 100644 index 0000000000..1f2fd6c725 Binary files /dev/null and b/editor/assets/backup-files-listing.png differ diff --git a/editor/assets/close-app-completely_sm.jpg b/editor/assets/close-app-completely_sm.jpg new file mode 100644 index 0000000000..81ed02e0c3 Binary files /dev/null and b/editor/assets/close-app-completely_sm.jpg differ diff --git a/editor/assets/restore-backup-feature_sm.jpg b/editor/assets/restore-backup-feature_sm.jpg new file mode 100644 index 0000000000..1589f9ce68 Binary files /dev/null and b/editor/assets/restore-backup-feature_sm.jpg differ diff --git a/editor/assets/restore-files-listing.png b/editor/assets/restore-files-listing.png new file mode 100644 index 0000000000..f9b23abcb4 Binary files /dev/null and b/editor/assets/restore-files-listing.png differ diff --git a/editor/assets/tangy-restore-backup-button_sm.jpg b/editor/assets/tangy-restore-backup-button_sm.jpg new file mode 100644 index 0000000000..478591d613 Binary files /dev/null and b/editor/assets/tangy-restore-backup-button_sm.jpg differ diff --git a/editor/case-module/case-schedule-templates.png b/editor/case-module/case-schedule-templates.png new file mode 100644 index 0000000000..25ec471edc Binary files /dev/null and b/editor/case-module/case-schedule-templates.png differ diff --git a/editor/case-module/case-service-api/index.html b/editor/case-module/case-service-api/index.html new file mode 100644 index 0000000000..c7b22cf8a4 --- /dev/null +++ b/editor/case-module/case-service-api/index.html @@ -0,0 +1,83 @@ + API: caseService - Tangerine Documentation

API: caseService

Warning

Use of the caseService Class is only available when the case module is enabled on the server.

TODO: Link to the page that describes how to enable the case service

The caseService class allows the form developer to programmatically interact with the case from within the form. The Case Service is activated in Tangerine when the user opens a view associated with a Case. Given a case's unique identifier, the Case Service loads into memory both the case document and the case definition document associated with the case. The Case Service also sets some reference variables for easy access to the most used case items like the participants, events and event forms.

Unless otherwise noted, the Case Service APIs that operate on the version of the case in memory. When programming within forms, any changes to the case document are saved to couchdb when the on-submit logic completes. In more advanced programming workflows, the load() and save() APIs can be used to specify when case documents are loaded into memory or saved to couchdb.

Case APIs


id

Returns the unique identifier of the currently loaded case.


participants

Returns the list of participants associated with the case or an empty array if none exist.


events

Returns the list of events associated with the case or an empty array if none exist.


forms

Returns the list of forms for all Case Events associated with the case or an empty array if none exist.


roleDefinitions

Returns the list of Case Roles associated with this case from the Case Definition.


eventFormDefinitions

Returns the list of Event Form Definitions associated with this case type form the Case Definition.


caseEventDefinitions

Returns the list of Case Event Definitions associated with this case type form the Case Definition.


changeLocation

Change the location of a Case. This also changes the location information on all related Form Responses.

Parameters

Param Type Description
location object An object where the properties are the levels and the values are the location node IDs

Example


setVariable

Set a Case level variable in a Case.

Parameters

Param Type Description
variableName string The variable name.
value any The value for the variable.

Example

caseService.setVariable('participant_id', getValue('participant_id'))
+caseService.setVariable('first_name', getValue('first_name'))
+caseService.setVariable('last_name', getValue('last_name'))
+
- example code


getVariable

Get a Case level variable in a Case.

Parameters

Param Type Description
variableName string The variable name.

Returns

The value requested. May be any data type that was set.

Example

if (!caseService.getVariable('status')) {
+  caseService.setVariable('status', 'screening')
+}
+
- example code


getCurrentCaseEventId

Returns the unique identifier for the Case Event currently loaded in memory. This is a useful function for safely getting the Case Event Id for use in other APIs that take caseEventId as a parameter.

Parameters

None

Returns

The unique identifier for the currently loaded Case Event. Returns undefined if a Case Event is not currently in view.

Example

if (caseService.getCurrentCaseEventId()) {
+  caseService.setEventEstimatedDay(caseService.getCurrentCaseEventId(), moment())
+}
+

getCurrentEventFormId

Returns the unique identifier for the Case Event currently loaded in memory. This is a useful function for safely getting the Event Form Id for use in other APIs that take eventFormId as a parameter.

Parameters

None

Returns

The unique identifier for the currently loaded Event Form. Returns undefined if an Event Form is not currently in view.

Example

if (caseService.getCurrentCaseEventId() && caseService.getCurrentEventFormId()) {
+  caseService.markEventFormRequired(caseService.getCurrentCaseEventId(), caseService.getCurrentEventFormId())
+}
+

Case Event API


createEvent

Dynamically create an instance of an event (defined in the case json) and add it to the current case

Parameters

Param Type Description
eventDefinitionId string Event Definition ID from the Case Definition Document
createRequiredEventForms (Optional) boolean (Default: False) Instantiate any required forms within the Event

Returns

CaseEvent object

Example

const event1 = caseService.createEvent('event-definition-cf58ca')
+const event2 = caseService.createEvent('event-definition-682ca6', true)
+

setEventName

Set a custom name for the Case Event to be displayed in the Case Event list for the current case. The string value passed as the name parameter can be resolved from any javascript that returns a string.

Parameters

Param Type Description
eventId string (UUID) Event Instance ID from the Case
name string Name for the event

Example

caseService.setEventEstimatedDay(caseService.getCurrentCaseEventId(), "First Visit")
+

setEventEstimatedDay

Set the estimated day of expected date completion. This is used by the calendar for displaying events. TODO: Improve Description

Parameters

Param Type Description
eventId string (UUID) Event Instance ID from the Case
timeInMs number Unix timestamp for the event date/time

Example

caseService.setEventEstimatedDay(event1.id, now)
+caseService.setEventEstimatedDay(caseService.getCurrentCaseEventId(), 1592359411)
+

setEventScheduledDay

Set the scheduled day of expected date completion. This is used by the calendar for displaying events. TODO: Improve Description

Parameters

Param Type Description
eventId string (UUID) Event Instance ID from the Case
timeInMs number Unix timestamp for the event date/time

Example

caseService.setEventScheduledDay(event1.id, now)
+caseService.setEventScheduledDay(caseService.getCurrentCaseEventId(), 1592359411)
+

setEventWindow

Set the expected event completion. TODO: Improve Description

Parameters

Param Type Description
eventId string (UUID) Event Instance ID from the Case
windowStartDayTimeInMs number Unix timestamp for the event start date/time
windowEndDayTimeInMs number Unix timestamp for the event end date/time

Example

caseService.setEventWindow(event1.id, 1592359411, 1592618611)
+caseService.setEventWindow(caseService.getCurrentCaseEventId(), 1592359411, 1592618611)
+

setEventOccurredOn

Set the date in which the event occurred TODO: Improve Description

Parameters

Param Type Description
eventId string (UUID) Event Instance ID from the Case
timeInMs number Unix timestamp for the event completion date/time

Example

caseService.setEventOccurredOn(event1.id, now)
+caseService.setEventOccurredOn(caseService.getCurrentCaseEventId(), 1592359411)
+

disableEventDefinition

Prevent creation of an via the new event menu.

Parameters

Param Type Description
eventDefinitionId string (UUID) Event Definition ID from the Case Definition

Example

caseService.disableEventDefinition('event-definition-1')
+

activateCaseEvent

The inactive flag on Case Event controls its appearance in the Case Event list. When a Case Event is created, the inactive flag is not set on the Case Event. This API adds the inactive flag to the Case Event and marks the flag as false. The inverse API deactivateCaseEvent sets the flag to true. Case Event with no inactive flag or having the flag set to false will appear in the Case Event list.

Parameters

Param Type Description
eventId string (UUID) Event Instance ID from the Case

Example

// All currently hidden (inactive) case events in the case with the event-definition of 'optional-event-dev' will be shown in the Case Event list
+for (let caseEvent in caseService.case.caseEvents.filter(event => event.caseEventDefinitionId === 'optional-event-def')) {
+  if (caseEvent.inactive) {
+    caseService.activateCaseEvent(caseEvent.id)
+  }
+}
+

deactivateCaseEvent

The inactive flag on Case Event controls its appearance in the Case Event list. When a Case Event is created, the inactive flag is not set on the Case Event. This API adds the inactive flag to the Case Event and marks the flag as true. The inverse API activateCaseEvent sets the flag to false. Case Event with the flag set to true will appear in the Case Event list.

Parameters

Param Type Description
eventId string (UUID) Event Instance ID from the Case

Example

// All case events in the case with the event-definition of 'optional-event-dev' will be hidden in the Case Event list
+for (let caseEvent in caseService.case.caseEvents.filter(event => event.caseEventDefinitionId === 'optional-event-def')) {
+  caseService.deactivateCaseEvent(caseEvent.id)
+}
+

Event Form API


createEventForm

Dynamically create an instance of an event form (defined in the case json) and add it to the current Case.

Parameters

Param Type Description
caseEventId string Event ID of the Event you want to add this Form to
eventFormDefinitionId string Event Form Definition ID of the Event Form ou want to create
participantId (optional) string ID of the Participant this Event Form is for.

Returns

EventForm object

Example

const eventForm = caseService.startEventForm(caseEvent.id, 'event-form-definition-fdkai3', participant.id)
+

deleteEventForm

Dynamically delete an instance of an event form.

Parameters

Param Type Description
caseEventId string Event ID of the Event you want to add this Form to
eventFormId string Event Form ID of the Event Form you want to delete

Example

// Find a Case Event and corresponding Event Form instance to delete.
+const caseEvent = caseService
+  .case
+  .events
+  .find(event => event.eventDefinitionId === 'event-definition-1')
+const eventForm = caseEvent
+  .eventForms
+  .find(eventForm => eventForm.eventFormDefinitionId === 'event-form-definition-1')
+// Delete the EventForm.
+caseService.deleteEventForm(caseEvent.id, eventForm.id)
+

setEventFormData

Set a custom piece of data for an event form. Note this is a separate data collection than the data on the Form Response related to the Event Form.

Parameters

Param Type Description
caseEventId string Event ID of the Event the Event Form lives in
eventFormId string Event Form ID of the Event Form you want to set data on
variableName string Variable name you are setting
value any Value you want to set

Example

caseService.setEventFormData(caseEvent.id, eventForm.id, 'foo', 'bar')
+

getEventFormData

Get a custom piece of data for an event form. Note this is a separate data collection than the data on the Form Response related to the Event Form.

Parameters

Param Type Description
caseEventId string Event ID of the Event the Event Form lives in
eventFormId string Event Form ID of the Event Form you want to get data of
variableName string Variable name you are getting

Example

const fooData = caseService.getEventFormData(caseEvent.id, eventForm.id, 'foo')
+

markEventFormRequired

Mark an Even Form instance as being required. You might do this on a form that was optional, but it has come to lite that it must be filled out before the event is marked as complete.

Parameters

Param Type Description
caseEventId string Event ID of the Event the Event Form lives in
eventFormId string Event Form ID of the Event Form you want to mark required

Example

// Create an EventForm in the current event. You could also find one.
+const form2 = caseService.createEventForm(caseEvent.id, 'event-form--mark-form-as-required-example-2', participant.id)
+// Mark EventForm as required.
+caseService.markEventFormRequired(caseEvent.id, form2.id)
+

markEventFormNotRequired

Mark and event form as not required.

Parameters

Param Type Description
caseEventId string Event ID of the Event the Event Form lives in
eventFormId string Event Form ID of the Event Form you want to mark not required

Example

// Mark EventForm as required.
+caseService.markEventFormRequired(caseEvent.id, eventForm.id)
+

markEventFormComplete

Mark an event form as complete. In some case workflows a form may no longer need to be filled. This API marks an Event Form as complete so that the data collector does does not attempt to complete the form.

Parameters

Param Type Description
caseEventId string Event ID of the Event the Event Form lives in
eventFormId string Event Form ID of the Event Form you want to mark not required

Example

// Mark EventForm as required.
+caseService.markEventFormRequired(caseEvent.id, eventForm.id)
+

activateEventForm

The inactive flag on Event Form controls its appearance in the Event Form list. When a Event Form is created, the inactive flag is not set on the Event Form. This API adds the inactive flag to the Event Form and marks the flag as false. The inverse API deactivateEventForm sets the flag to true. Event Form with no inactive flag or having the flag set to false will appear in the Event Form list.

Parameters

Param Type Description
caseEventId string Event ID of the Event the Event Form lives in
eventFormId string Event Form ID of the Event Form you want to mark active

Example

caseService.activateEventForm(caseEventId, eventFormId)
+

deactivateEventForm

The inactive flag on Event Form controls its appearance in the Event Form list. When a Event Form is created, the inactive flag is not set on the Event Form. This API adds the inactive flag to the Event Form and marks the flag as true. The inverse API activateEventForm sets the flag to false. Event Form with the flag set to true will appear in the Event Form list.

Parameters

Param Type Description
caseEventId string Event ID of the Event the Event Form lives in
eventFormId string Event Form ID of the Event Form you want to mark inactive

Example

caseService.deactivateEventForm(caseEventId, eventFormId)
+

Participant API


createParticipant

Create a participant in a Case.

Parameters

Param Type Description
caseRoleId string A Case Role ID as defined in the Case Role Definitions

Returns

Participant object

Example

const participantRole1 = caseService.createParticipant('role-1')
+

setParticipantData

Set some data for a specific Participant.

Parameters

Param Type Description
participantId string ID of the participant
variableName string Variable name you are setting
value any Value you want to set

Example

Create a participant and set some data from the current form...

const participantRole1 = caseService.createParticipant('role-1')
+caseService.setParticipantData(participantRole1.id, 'participant_id', getValue('participant_id'))
+caseService.setParticipantData(participantRole1.id, 'first_name', getValue('first_name'))
+caseService.setParticipantData(participantRole1.id, 'last_name', getValue('last_name'))
+

Set data on participant whom this Event Form is assigned to using the global participant object that is active when an EventForm is open that has an assigned Participant...

caseService.setParticipantData(participant.id, 'first_name', getValue('first_name'))
+caseService.setParticipantData(participant.id, 'last_name', getValue('last_name'))
+

Find a participant by role and set some data from the current form...

const participantRole1 = caseService.case.participants.find(participant => paritipcant.caseRoleId === 'role-1')
+caseService.setParticipantData(participantRole1.id, 'participant_id', getValue('participant_id'))
+caseService.setParticipantData(participantRole1.id, 'first_name', getValue('first_name'))
+caseService.setParticipantData(participantRole1.id, 'last_name', getValue('last_name'))
+


getParticipantData

Get some data for a specific Participant.

Parameters

Param Type Description
participantId string ID of the participant
variableName string Variable name you are getting

Returns

This function returns the value of the variable you requested which may consist of any data type you set it to.

Example

Get participant data of the form whom is assigned to the current form...

caseService.getParticipantData(participant.id, 'first_name')
+

Notification API

Notifications appear in the Case view to provide instructions or extra information to the users who are filling out forms. The programmer uses the following APIs to create notifications in teh Case interface. Notifications can be persistant or dismisable depending on the use case.


createNotification

Create a notificaiton and display it in the Case view

Parameters

Param Type Description
label string Short text used for the title
description string Longer text description
link string Url link internal or external
icon string Text name of a system icon
color string Hexadecimal value of a color (e.g. #CCC)
enforceAttention boolean If true, change focus to the notification when it is displayed
persist boolean If true, notification can only be dismissed programatically

Example

caseService.createNotification('Alert: Case Needs you attention', 'The Case needs review with a supervisor.', '', 'notification_important', '#CCC', true, false)
+

openNotification

Sets the status of a notificaiton to Open so it will display to the user.

Parameters

Param Type Description
notificationId string Short text used for the title

Example

This code re-opens any Closed notifications that have a label that starts with 'Alert'.

if (case.notifications) {
+  const notifications = case.notifications.filter(n => n.label.startsWith('Alert') && n.status === NotificationStatus.Closed)
+  for (let notification in notifications) {
+    caseService.openNotificaiton(notification.id)
+  }
+}
+


closeNotification

Sets the status of a notificaiton to Closed so it will be hidden from the user.

Parameters

Param Type Description
notificationId string Short text used for the title

Example

This code closes notifications that have a label that starts with 'Alert'.

if (case.notifications) {
+  const notifications = case.notifications.filter(n => n.label.startsWith('Alert') && n.status === NotificationStatus.Open)
+  for (let notification in notifications) {
+    caseService.closeNotificaiton(notification.id)
+  }
+}
+

\ No newline at end of file diff --git a/editor/case-module/custom-case-reports/index.html b/editor/case-module/custom-case-reports/index.html new file mode 100644 index 0000000000..c70110bcc1 --- /dev/null +++ b/editor/case-module/custom-case-reports/index.html @@ -0,0 +1,3 @@ + Custom Case Reports - Tangerine Documentation

Custom Case Reports

The Custom Case Reports features enables the developer to create custom reports or a dashboard. This feature is accessible using a tab when viewing a case. To enable this feature, add showCaseReports: true to the group's app-config.json.

Demo and description of this feature's assets

This demo is based on the case module content set; therefore, you might want to install that in order to follow the demo.

There are three files used for custom reports, which must be placed in the group directory.

  1. queries.js - has the map/reduce queries used to index the docs. Note that the sample queries.js file has a function called registrationResults with a map and a reduce query. The map function is explained above; the reduce uses Couchdb's built-in _sum keyword.

The queries in this doc are installed when the app is installed. Support for updating these queries is not yet implemented.

  1. reports.js - runs the queries and fills out the content for reports.html. The reduce function is used to provide the counts in report2. The options in report2 options = {reduce: true} activate the reduce function.

  2. reports.html - provides a basic html frame for the data.

Helper functions

When writing queries or organizing the javascript logic to fetch the results, use the globally-exposed T.form.Get function to get the value of inputs; this will save you from having to wrote deeply nested code (doc.items[0].inputs[3].value[0].value)

T.form.Get(doc, 'consent')

Other helper function are available:

  • T.user: userService
  • T.case: caseService

Development in Tangerine Preview

Developers can use Tangerine-Preview as a test environment when developing custom case reports. When updating the version of a query in the queries.js file, run the following commands in the Chrome Dev Console to make Tangerine-Preview run the documents through the new version of the query:

var userdb = await T.user.getUserDatabase()
+await T.update.updateCustomViews(userDb)
+
\ No newline at end of file diff --git a/editor/case-module/debug-case-templates.png b/editor/case-module/debug-case-templates.png new file mode 100644 index 0000000000..b56a3ef955 Binary files /dev/null and b/editor/case-module/debug-case-templates.png differ diff --git a/editor/case-module/developing-custom-reports/index.html b/editor/case-module/developing-custom-reports/index.html new file mode 100644 index 0000000000..acb3ef03de --- /dev/null +++ b/editor/case-module/developing-custom-reports/index.html @@ -0,0 +1 @@ + Develop custom reports for Client - Tangerine Documentation
\ No newline at end of file diff --git a/editor/case-module/get-and-set-event-data/index.html b/editor/case-module/get-and-set-event-data/index.html new file mode 100644 index 0000000000..a36b037675 --- /dev/null +++ b/editor/case-module/get-and-set-event-data/index.html @@ -0,0 +1,11 @@ + Get and set event data - Tangerine Documentation

Get and set event data

For lack of a T.case.setEventData()/T.case.getEventData() API, use T.case.setVariable and T.case.getVariable with variable names that are namespaced using the Event ID.

// 3 ingredients are needed to set an Event Variable.
+const eventId = '123'
+const variableName = 'foo'
+const variableValue = 'bar'
+
+// Set Event Variable.
+T.case.setVariable(`${eventId}-${variableName}`, variableValue)
+
+// Get Event Variable.
+const shouldBeValueOfBar = T.case.getVariable(`${eventId}-${variableName}`)
+

There is an example in the case-module Content Set which consists of three parts:

\ No newline at end of file diff --git a/editor/case-module/how-to-create-a-workflow-for-changing-case-location/index.html b/editor/case-module/how-to-create-a-workflow-for-changing-case-location/index.html new file mode 100644 index 0000000000..71d97c0337 --- /dev/null +++ b/editor/case-module/how-to-create-a-workflow-for-changing-case-location/index.html @@ -0,0 +1,10 @@ + How to create a workflow for changing a Case's location - Tangerine Documentation

How to create a workflow for changing a Case's location

Using a combination of a <tangy-location> input on a form and some logic in the same form's on-submit hook, you can empower users to reassign a Case to a new location. In the Case Module's Content Set we have a "Change Location of Case" Case Event Definition, a "Change Location of Case" Event Form, and corresponding "Change Location of Case" Form. We'll use that Content Set as our example.

The form could be in any event, or even it's own event as it is in the Case Module content set. The important part is in the form you place the <tangy-location> for selecting a new locatoin to assign the case to. The following shows the markup of a form that uses the caseService.changeLocation() API to change the location of a Case given the value selected in the <tangy-location> input.

<tangy-form title="Change location of case" id="change-location-of-case"
+  on-submit="
+    caseService.changeLocation(inputs.new_location_assignment.value)
+  "
+>
+  <tangy-form-item id="item1">
+    <tangy-location label="Choose a location to assign this case to." name="new_location_assignment" required></tangy-location>
+  </tangy-form-item>
+</tangy-form>
+
\ No newline at end of file diff --git a/editor/case-module/how-to-pass-data-between-forms-in-a-case/index.html b/editor/case-module/how-to-pass-data-between-forms-in-a-case/index.html new file mode 100644 index 0000000000..7e040e8027 --- /dev/null +++ b/editor/case-module/how-to-pass-data-between-forms-in-a-case/index.html @@ -0,0 +1,26 @@ + How to pass data between Forms in a Case - Tangerine Documentation

How to pass data between Forms in a Case

Let's say you had two forms in a Case, Form X and Form Y. In Form X, you collect the respondent's first name and last name, meanwhile in Form Y you want to confirm the data they entered on Form X.

Form X

In Form X, we bubble up the first_name and last_name variables on Form X to the first_name and last_name variables at the Case level.

<tangy-form
+  id="form-x"
+  title="Form X"
+  on-submit="
+    T.case.setVariable('first_name', getValue('first_name'))
+    T.case.setVariable('last_name', getValue('last_name'))
+  "
+>
+  <tangy-form-item id="item1">
+    <tangy-input name="first_name" label="First name" required>
+    <tangy-input name="last_name" label="Last name" required>
+  </tangy-form-item>
+</tangy-form>
+

Form Y

In Form Y, we get the Case level variables of first_name and last_name and set them on the first_name and last_name inputs. This results in the form loading with the previously entered first and last names already filled out.

<tangy-form id="form-y" title="Form Y">
+  <tangy-form-item
+    id="item1"
+    on-open="
+      inputs.first_name.value = T.case.getVariable('first_name')
+      inputs.last_name.value = T.case.getVariable('last_name')
+    "
+  >
+    <tangy-input name="first_name" label="Confirm your first name" required>
+    <tangy-input name="last_name" label="Confirm your last name" required>
+  </tangy-form-item>
+</tangy-form>
+
\ No newline at end of file diff --git a/editor/case-module/how-to-use-form-response-data-in-an-event-form-listing.gif b/editor/case-module/how-to-use-form-response-data-in-an-event-form-listing.gif new file mode 100644 index 0000000000..c38018d74b Binary files /dev/null and b/editor/case-module/how-to-use-form-response-data-in-an-event-form-listing.gif differ diff --git a/editor/case-module/how-to-use-form-response-data-in-an-event-form-listing/index.html b/editor/case-module/how-to-use-form-response-data-in-an-event-form-listing/index.html new file mode 100644 index 0000000000..c6afeb3a77 --- /dev/null +++ b/editor/case-module/how-to-use-form-response-data-in-an-event-form-listing/index.html @@ -0,0 +1,13 @@ + How to use Form Response data in an Event Form Listing - Tangerine Documentation

How to use Form Response data in an Event Form Listing

The following tutorial uses content from the case-module Content Set found here.

how-to-use-form-response-data-in-an-event-form-listing

Step 1: On submit of a Form, capture data as a Case Level variable

In Step 2, we'll template out data for the Event Form listing, but before we can do that we need to transfer some data from a form up to the Event Form data in the Case using the T.case.setEventFormData API.

File: ./template-event-form-listing/form.html

<tangy-form
+  id="template-event-form-listing"
+  title="Template Event Form Listing"
+  on-submit="
+    T.case.setEventFormData(caseEvent.id, eventForm.id, 'title', getValue('title'))
+  "
+>
+  <tangy-form-item id="item-1">
+    <tangy-input type="text" name="title" label="Set the custom title for this Event Form."></tangy-input>
+  </tangy-form-item>
+</tangy-form>
+

Step 2: Use templateCaseEventListItemPrimary property in the Case Definition to print the Case variable in the Event Listing

After a user has submitted the Event Form mentioned above, we can now use the Event Form data for templating out Event listings. In the Case Definition, we add ternary to check if the variable exists, if it does then print it out in the listing, else show the name of the Event Form Definition.

Section of File: ./case-definition-1.json

  "templateEventFormListItemPrimary": "<span>${eventForm?.data?.title ? eventForm.data.title : eventFormDefinition.name}</span>",
+

\ No newline at end of file diff --git a/editor/case-module/how-to-use-form-response-data-in-an-event-listing.gif b/editor/case-module/how-to-use-form-response-data-in-an-event-listing.gif new file mode 100644 index 0000000000..9dc770e967 Binary files /dev/null and b/editor/case-module/how-to-use-form-response-data-in-an-event-listing.gif differ diff --git a/editor/case-module/how-to-use-form-response-data-in-an-event-listing/index.html b/editor/case-module/how-to-use-form-response-data-in-an-event-listing/index.html new file mode 100644 index 0000000000..f993368b89 --- /dev/null +++ b/editor/case-module/how-to-use-form-response-data-in-an-event-listing/index.html @@ -0,0 +1,13 @@ + How to use Form Response data in an Event Listing - Tangerine Documentation

How to use Form Response data in an Event Listing

The following tutorial uses content from the case-module Content Set found here.

how-to-use-form-response-data-in-an-event-listing

Step 1: On submit of a Form, capture data as a Case Level variable

In Step 2, we'll template out data for the event listing, but before we can do that we need to transfer some data from a form up to the Case level variable. In the example below, we bubble up the title variable in the form to a title variable on the Case. However, note how we prepend the current Event ID on the case level title variable. This is important to do if you are going to bubble up variables that may have the same name across events. By prepending the current Event ID, we guarantee that any other Event that bubbles up a title variable will not overwrite the title variable for this Event.

File: ./template-event-listing/form.html

<tangy-form
+  id="template-event-listing"
+  title="Template Event Listing"
+  on-submit="
+    T.case.setVariable(`${caseEvent.id}-title`, getValue('title'))
+  "
+>
+  <tangy-form-item id="item-1">
+    <tangy-input type="text" name="title" label="Set the custom title for this event."></tangy-input>
+  </tangy-form-item>
+</tangy-form>
+

Step 2: Use templateCaseEventListItemPrimary property in the Case Definition to print the Case variable in the Event Listing

After a user has submitted the Event Form mentioned above, we can now get that Case level variable when templating out Event listings. In the Case Definition, we add ternary to check if the variable exists, if it does then print it out in the listing, else show the name of the Event Definition. Note how we are again referencing the variable name by preprending the Event ID on the variable name. This ensures we are getting the title variable for that specific event and not accidentally overriding other/all event listings.

Section of File: ./case-definition-1.json

  "templateCaseEventListItemPrimary": "<span>${T.case.getVariable(`${caseEvent.id}-title`) ? T.case.getVariable(`${caseEvent.id}-title`) : caseEventDefinition.name}</span>",
+

\ No newline at end of file diff --git a/editor/case-module/index.html b/editor/case-module/index.html new file mode 100644 index 0000000000..eda0b90561 --- /dev/null +++ b/editor/case-module/index.html @@ -0,0 +1,83 @@ + Case Module - Tangerine Documentation

Case Module

Case Module allows us to define Case Definitions for different purposes such as following a participant in a drug trial over the course of many events, where each event may require many forms to be filled out. In order to create and find cases, you will need to configure the "case-home" as the "homeUrl" value in app-config.json.

Configuring Cases

Case Module allows us to define Case Definitions for different purposes such as following a participant in a drug trial over the course of many events, where each event may require many forms to be filled out.

To configure cases, there are four files to modify.

First add a reference to the new Case Definition in the case-definitions.json. Here is an example of a case-definitions.json file that references two Case Definitions.

File: case-definitions.json

[
+  {
+    "id": "case-definition-1",
+    "name": "Case Definition 1",
+    "src": "./assets/case-definition-1.json"
+  },
+  {
+    "id": "case-definition-2",
+    "name": "Case Definition 2",
+    "src": "./assets/case-definition-2.json"
+  }
+]
+

Then create the corresponding Case Definition file...

File: case-definition-1.json

{
+  "id": "case-definition-1",
+  "formId": "case-definition-1-manifest",
+  "name": "Case Definition 1",
+  "description": "Description...",
+  "startFormOnOpen": {
+    "eventId": "event-definition-1",
+    "eventFormId": "event-form-1"
+  },
+  "eventDefinitions": [
+   {
+      "id": "event-definition-1",
+      "name": "Event Definition 1",
+      "description": "Description...",
+      "repeatable": false,
+      "required": true,
+      "eventFormDefinitions": [
+        {
+          "id": "event-form-definition-1",
+          "formId": "form-1",
+          "name": "Form 1",
+          "required": true,
+          "repeatable": false
+        }
+      ]
+    }
+  ]
+}
+

Case Definition Templates

As a Data Collector uses the Client App, they navigate a Case's hierarchy of Events and Forms. Almost every piece of information they see can be overriden to display custom variables and logic by using the Case Definition's templates. This section describes the templates available and what variables are available. Note that all templates are evaluated as Javascript Template Literals. There are many good tutorials online about how to use Javascipt Template Literals, here are a couple of Javascript Template Literals examples that we reference often for things like doing conditionals and loops.

Schedule

case schedule templates

templateScheduleListItemIcon default:

"templateScheduleListItemIcon": "${caseEvent.status === 'CASE_EVENT_STATUS_COMPLETED' ? 'event_note' : 'event_available'}"
+

templateScheduleListItemPrimary default:

"templateScheduleListItemPrimary": "<span>${caseEventDefinition.name}</span> in Case ${caseService.case._id.substr(0,5)}"
+

templateScheduleListItemSecondary default:

"templateScheduleListItemSecondary": "<span>${caseInstance.label}</span>"
+

Variables available: - caseService: CaseService - caseDefinition: CaseDefinition - caseEventDefinition: CaseEventDefinition - caseInstance: Case - caseEvent: CaseEvent

Debugging Case Definition Templates

debug case templates

Configuring forms.json

The case references a Form in the formId property of the Case Definition. Make sure there is a form with that corresponding Form ID listed in forms.json with additional configuration for search.

File: forms.json

[
+  {
+    "id" : "case-definition-1-manifest",
+    "type" : "case",
+    "title" : "Case Definition 1 Manifest",
+    "description" : "Description...",
+    "listed" : true,
+    "src" : "./assets/case-definition-1-manifest/form.html",
+    "searchSettings" : {
+      "primaryTemplate" : "${searchDoc.variables.status === 'Enrolled' ? `Participant ID: ${searchDoc.variables.participant_id} &nbsp; &nbsp; &nbsp; Enrollment Date: ${(searchDoc.variables.enrollment_date).substring(8,10) + '-' + (searchDoc.variables.enrollment_date).substring(5,7)+ '-' + (searchDoc.variables.enrollment_date).substring(0,4)}` : `Screening ID: ${searchDoc._id.substr(0,6)}  &nbsp; &nbsp; &nbsp; Screening Date: ${searchDoc.variables.screening_date ? searchDoc.variables.screening_date : 'N/A' }` }",
+      "shouldIndex" : true,
+      "secondaryTemplate" : "${searchDoc.variables.status === 'Enrolled' ? `Name: ${searchDoc.variables.first_name} ${searchDoc.variables.last_name}  &nbsp; &nbsp; &nbsp; Location: ${searchDoc.variables.location}  &nbsp; &nbsp; &nbsp; Status: Enrolled &nbsp; &nbsp; &nbsp;` : `Status: Not enrolled  &nbsp; &nbsp; &nbsp;` }",
+      "variablesToIndex" : [
+        "first_name",
+        "last_name",
+        "status",
+        "location",
+        "participant_id",
+        "enrollment_date",
+        "screening_date"
+      ]
+    },
+  }
+]
+

The properties in forms.json often change; check the Content Sets for current examples.

The variables listed in variablesToIndex will be available for searching on with the Tablet level search as well as in Editor. You also must set "shouldIndex" : true.

All forms that should not be searched must also have the searchSettings configured. Here is an example that must be implemented:

    "searchSettings" : {
+      "shouldIndex" : false,
+      "primaryTemplate" : "",
+      "secondaryTemplate" : "",
+      "variablesToIndex" : [
+      ]
+    },
+

To configure the QR code search on Tablets, in your app-config.json, there is a "barcodeSearchMapFunction" property. This is a map function for receiving the value of the Search Scan for you to parse out and return the value that should be used for search. A data variable is passed in for you to parse, and the return value is what ends up in the search bar as a text search.

Example:

if ((JSON.parse(data)).participant_id) { 
+  return (JSON.parse(data)).participant_id
+} else { 
+  throw 'Incorrect format'
+} 
+

Configuring two-way sync

Because you may need to share cases across devices, configuring two-way sync may be necessary. See the Two-way Sync Documentation for more details. Note that you sync Form Responses, and it's the IDs of that you'll want to sync in the "formId" of the Case Definition in order to sync cases.

Configuring the Schedule

One of the two tabs that Data Collectors see when they log into Tangerine is a "Schedule" tab. This schedule will show Case Event's on days where they are have an estimated day, scheduled day, and/or occurred on day. You can set these three dates on an event using the following APIs.

caseService.setEventEstimatedDay(idOfEvent, timeInUnixMilliseconds)
+caseService.setEventOccurredOn(idOfEvent, timeInUnixMilliseconds)
+caseService.setEventScheduledDay(idOfEvent, timeInUnixMilliseconds)
+
\ No newline at end of file diff --git a/editor/case-module/on-device-data-corrections-using-issues/index.html b/editor/case-module/on-device-data-corrections-using-issues/index.html new file mode 100644 index 0000000000..bf7f9ac7f2 --- /dev/null +++ b/editor/case-module/on-device-data-corrections-using-issues/index.html @@ -0,0 +1,9 @@ + On Device Data Corrections using Issues - Tangerine Documentation

On Device Data Corrections using Issues

Setup

App Configuration

In your group's app-config.json (and app-config.defaults.json) add the following JSON settings. The first will make the list of Issues appear as a tab on the homescreen, the second will make a "New Issue" button appear when viewing submitted Event Forms.

{
+  "showIssues": true,
+  "allowCreationOfIssues": true
+}
+

Template the Issue Title and Description

When creating an Issue on a Device, users are not allowed the opportunity to add define a Title and Description for the Issue they are creating. For this reason, you will find it helpful to develop templates for the Issue Title and Descriptions to give them context. These templates are built on a per Case Definition basis and are top level properties in any Case Definition.

{
+  "templateIssueTitle": "Issue for ${caseDefinition.name} with Participant ID of ${T.case.getVariable('participant_id')}, by ${userName}",
+  "templateIssueDescription": ""
+}
+

Usage

When configured, Data Collectors will have the opportunity to create Issues where they propose changes to forms. After clicking "New Issue", they will fill out the form with any changes they see fit. When submitting the form will save the proposed changes in the Issue. The Issue will then be synced up to the server for a Data Manager to review. If the Data Manager approves, they may merge the proposed changes from the server. When proposed changes are merged, the status of the Issue will change to closed and that status change of the Issue will replicate down to the Device on that Device's next sync. Note however the merged changes to the form will not be replicated down to the Device unless two-way sync is set up for that form.

\ No newline at end of file diff --git a/editor/case-module/prepare-form-logic-for-issues/index.html b/editor/case-module/prepare-form-logic-for-issues/index.html new file mode 100644 index 0000000000..5e8e68ad26 --- /dev/null +++ b/editor/case-module/prepare-form-logic-for-issues/index.html @@ -0,0 +1,26 @@ + Preparing form logic for compatibility with Issues - Tangerine Documentation

Preparing form logic for compatibility with Issues

Issues is a server level feature in Tangerine that allows privileged users to make proposals on changes to Form Responses. When a user is proposing changes, the system unlocks an already completed form. Form Developers must be careful when writing logic they only expect to be ran once in a form.

For example, setting a "caseOpenedOn" variable in the on-open of a form. When a form is reopened in an Issue, the logic could potentially overwrite the "caseOpenedOn" variable using the current date of the proposed change.

<tangy-form
+    on-open="
+        T.case.setVariable('caseOpenedOn', moment.now())
+    "
+>
+

This can be remedied by using the T.case.isIssueContext() helper function to ensure our variable setting does not happen in a reopen in the issue context.

<tangy-form
+    on-open="
+        if (!T.case.isIssueContext()) {
+            T.case.setVariable('caseOpenedOn', moment.now())
+        }
+    "
+>
+

The same is true if you have any <tangy-form-item> level on-open or on-change logic that should not run in when being modified in an Issue. However, writing <tangy-form> logic for on-submit is a little different because on-submit code will not run when in the Issue context. Instead, if some code does need to run, place it in the on-resubmit code for a <tangy-form>.

Note in the following example how we only set caseOpenedOn in on-submit. This ensure this variable will only be set once. However, note how put the setting of firstName and lastName in the on-resubmit. This ensures that if the proposed change to this Form Response changes the firstName or lastName values, the Case will be updated to reflect this proposed change.

<tangy-form
+    on-submit="
+        T.case.setVariable('caseOpenedOn', moment.now())
+        T.case.setVariable('firstName', getValue('firstName'))
+        T.case.setVariable('lastName', getValue('firstName'))
+    "
+    on-resubmit="
+        T.case.setVariable('firstName', getValue('firstName'))
+        T.case.setVariable('lastName', getValue('firstName'))
+    "
+>
+

There are also many input types such as GPS and Signature where it often does not make sense to recollect data upon submitting a proposal on an Issue. Consider adding disable-if="T.case.isIssueContext()" to these inputs.

<tangy-gps name="gps" disable-if="T.case.isIssueContext()"></tangy-gps>
+<tangy-signature name="signature" disable-if="T.case.isIssueContext()"></tangy-signature>
+
\ No newline at end of file diff --git a/editor/case-module/role-base-access/index.html b/editor/case-module/role-base-access/index.html new file mode 100644 index 0000000000..88a3aa8560 --- /dev/null +++ b/editor/case-module/role-base-access/index.html @@ -0,0 +1,7 @@ + Role Based Access - Tangerine Documentation

Role Based Access

A Device Users' Role can be used restrict access to specific Case Events, Event Forms, and inputs on Forms.

To retrict access to an input on a form by role, use the T.user.getRoles() function to get the roles of the currently logged in user.

<tangy-input
+  name="example"
+  label="Example"
+  show-if="T.user.getRoles().includes('admin')"
+>
+</tangy-input>
+
\ No newline at end of file diff --git a/editor/content-sets/index.html b/editor/content-sets/index.html new file mode 100644 index 0000000000..a11ea8a926 --- /dev/null +++ b/editor/content-sets/index.html @@ -0,0 +1 @@ + Content sets - Tangerine Documentation

Content sets

Content Sets

Content Sets are groups of forms and configuration you can use as a template for new groups.

Anatomy of a Content Set

Version 1 (< Tangerine v3.13.0)

In the root directory of a v1 Content Set, you will find the following: - ./app-config.json_example (required) - ./forms.json (required)

Version 2 (> Tangerine v3.13.0)

Starting in Tangerine v3.13.0, the second iteration of Content Sets was launched. In the root directory of a v2 Content Set, you will find the following:

  • ./client/ (required): The folder containing content that will be deployed to Tablets.
  • ./client/app-config.defaults.json (required): Defaults to use for app-config.json. For example, a Case Module enabled group would have a "homeUrl" property with a value of "case-home".
  • ./client/forms.json (required)
  • ./editor/ (required): A folder containing assets pertanent to how Editor behaves.
  • ./editor/index.html (required): The file loaded when displaying the Dashboard in a group's Editor.
  • ./README.md (suggested)
  • ./docs/ (suggested)

Version 2.1 (>= Tangerine 3.15.0)

Starting in Tangerine v3.13.0, content sets gained a package.json and build step. The package.json specifies the required libs and scripts for the content set. If the content set uses custom scripts, these scripts are compiled by the included webpack. Note that .gitignore ignores the compiled code - client/custom-scripts.js - to avoid conflicts.

To install the 2.1 content set - instead of npm install, run npm run install-server.

Be sure to update any cron jobs to include the new build commands if they are using content set 2.1:

git pull npm rn install-server npm build

If using Content set 2, this build process is not necessary. However, it is an advantage for cs2 users to upgrade to be able to pin the webpack version in packagejson and also to not have the huge custom-scripts file.

Creating a new Content Set

Importing a Content Set into Tangerine

New group set: - git clone tangerine starter repo - Modify content as needed - Push to Git - On server instance, setup GH deploy key by navigating to your Repository on Github and click on Settings -> Deploy keys -> Add deploy key and paste your Docker instances /root/.ssh/id_rsa.pub in the key contents, enable "Allow write access" and save. Run docker exec tangerine create-group "New Group A" https://github.com/id/tangerine-content.git using the cli - Add crontab entry that uses the non-pub key to do the pull, npm run install-server, npm run build.

\ No newline at end of file diff --git a/editor/custom-apps/index.html b/editor/custom-apps/index.html new file mode 100644 index 0000000000..250c15bc5a --- /dev/null +++ b/editor/custom-apps/index.html @@ -0,0 +1,13 @@ + Custom Apps - Tangerine Documentation

Custom Apps

Creating a Custom App

cd tangerine/content-sets
+cp -r custom-app my-app
+cp ../translations/* my-app/client/
+cd my-app
+git init
+git add .
+git commit -m "First."
+git branch -M main
+git remote add origin <your apps origin> 
+git push -u origin main
+npm install
+npm start
+
\ No newline at end of file diff --git a/editor/debug-case-templates.png b/editor/debug-case-templates.png new file mode 100644 index 0000000000..b56a3ef955 Binary files /dev/null and b/editor/debug-case-templates.png differ diff --git a/editor/form-developers-cookbook/allowed-date-range-based-on-today.gif b/editor/form-developers-cookbook/allowed-date-range-based-on-today.gif new file mode 100644 index 0000000000..2352a134c0 Binary files /dev/null and b/editor/form-developers-cookbook/allowed-date-range-based-on-today.gif differ diff --git a/editor/form-developers-cookbook/dynamic-location-levels.gif b/editor/form-developers-cookbook/dynamic-location-levels.gif new file mode 100644 index 0000000000..1d4a4850cf Binary files /dev/null and b/editor/form-developers-cookbook/dynamic-location-levels.gif differ diff --git a/editor/form-developers-cookbook/dynamically-change-text-color.gif b/editor/form-developers-cookbook/dynamically-change-text-color.gif new file mode 100644 index 0000000000..591783cea3 Binary files /dev/null and b/editor/form-developers-cookbook/dynamically-change-text-color.gif differ diff --git a/editor/form-developers-cookbook/dynamically-prevent-next.gif b/editor/form-developers-cookbook/dynamically-prevent-next.gif new file mode 100644 index 0000000000..384ac9d391 Binary files /dev/null and b/editor/form-developers-cookbook/dynamically-prevent-next.gif differ diff --git a/editor/form-developers-cookbook/flag-discrepancy-or-warning-and-hide.gif b/editor/form-developers-cookbook/flag-discrepancy-or-warning-and-hide.gif new file mode 100644 index 0000000000..97cee48753 Binary files /dev/null and b/editor/form-developers-cookbook/flag-discrepancy-or-warning-and-hide.gif differ diff --git a/editor/form-developers-cookbook/index.html b/editor/form-developers-cookbook/index.html new file mode 100644 index 0000000000..3b262131d8 --- /dev/null +++ b/editor/form-developers-cookbook/index.html @@ -0,0 +1 @@ + The Tangerine Form Developers' Cookbook - Tangerine Documentation

The Tangerine Form Developers' Cookbook

Examples of various recipes for Tangerine Forms collected throughout the years. To create your own example, remix the example on glitch.com.

Skip a question based on input in another question

In the following example we ask an additional question about tangerines if the user indicates that they do like tangerines.

Run example - Open Editor - View Code

skip-question-based-on-input

Skip sections based on input

In the following example, wether or not you answer yes or no to the question, you will end up on a different item.

Run example - Open Editor - View Code

skip-sections-based-on-input

Valid by number of decimal points

In the following example, we validate user input by number of decimal points.

Run example - Open Editor - View Code

valid-by-number-of-decimal-points

Valid if greater or less than other input

Run example - Open Editor - View Code

valid-if-greater-or-less-than-other-input

Allowed date range based on today

Run example - Open Editor - View Code

allowed-date-range-based-on-today

Flag choice as discrepancy and/or warning and show or hide content depending

Run example - Open Editor - View Code

flag-discrepancy-or-warning-and-hide

Indicate a mutually exclusive option in a checkboxes group such as "None of the above"

In the following example when you make a selection of a fruit and then choose one of the mutually exclusive options, your prior selections will be deselected.

Run example - Open Editor - View Code

Jan-03-2020 13-48-05

Capture and show local date and time

Sometimes we want to show the user the local date and time to ensure their time settings are correct.

Run example - Open Editor - View Code

tangerine-form-editors-cookbook--capture-local-date-and-time

Show a timer in an item

Let's say you want to show a timer of how long someone has been on a single item. This calculates the time since item open and displays number of seconds since then in a tangy-box.

Run example - Open Editor - View Code

stop watch

Capture the time between two items

Sometimes we want to know how much time passed between two points in a form. This example captures, the start_time variable on the first item, then end_time on the last item. Lastly it calculates the length of time.

Run example - Open Editor - View Code

timed items

Hard checks vs. soft checks

A "hard check" using "valid if" will not allow you to proceed. However a "soft check" using "warn if" will allow you to proceed after confirming.

Run example - Open Editor - View Code

soft-checks-vs-hard-checks

Set selected value in radio buttons

In the following example we set the value of a <tangy-radio-buttons>.

Run example - Open Editor - View Code

set-value-of-tangy-radio-buttons

Dynamically prevent proceeding to next section

In the following example hide the next button given the value of some user input.

Run example - Open Editor - View Code

dynamically-prevent-next

Proactive input validation

In the following example we validate an input after focusing on the next input. This approach is more proactive than running the validation logic when clicking next or submit.

Run example - Open Editor - View Code

proactive-input-validation

Content Box with Tabs

In the following example we display content in a set of tabs.

Run example - Open Editor - View Code

tangy-form-tabs

Dynamic Changing of Text Color

In the following example we change the color of text depending on a user's selection.

Run example - Open Editor - View Code

dynamically-change-text-color

Use skip-if to reference variable inside tangy-inputs-group

In the following example a skip-if refers to an other variable local to the group itself is in. The trick is using backticks around the variable name (not quotes) you are referencing and prepending the variable name you are referencing with ${context.split('.')[0]}.${context.split('.')[1]}..

Run example - Open Editor - View Code

skip-if-inside-tangy-inputs-groups

Use valid-if to reference variable inside tangy-inputs-group

In the following example a valid-if refers to an other variable local to the group itself is in. The trick is using backticks around the variable name (not quotes) you are referencing and prepending the variable name you are referencing with ${input.name.split('.')[0]}.${input.name.split('.')[1]}.. Watch out for the gotcha of not using input.name instead of context like we do in a skip-if.

Run example - Open Editor - View Code

skip-if-inside-tangy-inputs-groups

Dynamic Location Level

In the following example we empower the Data Collector to select which Location Level at which they will provide their answer. This example can also be used in a more advanced way to base the level of location required for entry given some other set of inputs.

Run example - Open Editor - View Code

dynamic-location-levels

Prevent user from proceeding during asynchronous logic

Sometimes in a form the logic calls for running some code that is asynchronous such as database saves and HTTP calls. As this logic runs, we would like to prevent the user from proceeding in the form. This is a job for a <tangy-gate>. Tangy Gate is an input that by default will not allow a user to proceed in a form. The gate can only be "opened" by some form logic that set's that Tangy Gate's variable name's value to true. This gives your logic in your forms an opportunity to run asynchronously, blocking the user from proceeding, then when async code is done your code sets the the gate to open.

Run example - Open Editor - View Code

dynamic-location-levels

\ No newline at end of file diff --git a/editor/form-developers-cookbook/proactive-input-validation.gif b/editor/form-developers-cookbook/proactive-input-validation.gif new file mode 100644 index 0000000000..3eb39dc0a1 Binary files /dev/null and b/editor/form-developers-cookbook/proactive-input-validation.gif differ diff --git a/editor/form-developers-cookbook/set-value-of-tangy-radio-buttons.png b/editor/form-developers-cookbook/set-value-of-tangy-radio-buttons.png new file mode 100644 index 0000000000..3b0eb24976 Binary files /dev/null and b/editor/form-developers-cookbook/set-value-of-tangy-radio-buttons.png differ diff --git a/editor/form-developers-cookbook/skip-if-inside-tangy-inputs-groups.gif b/editor/form-developers-cookbook/skip-if-inside-tangy-inputs-groups.gif new file mode 100644 index 0000000000..685c54c13d Binary files /dev/null and b/editor/form-developers-cookbook/skip-if-inside-tangy-inputs-groups.gif differ diff --git a/editor/form-developers-cookbook/skip-question-based-on-input.gif b/editor/form-developers-cookbook/skip-question-based-on-input.gif new file mode 100644 index 0000000000..c05aa37111 Binary files /dev/null and b/editor/form-developers-cookbook/skip-question-based-on-input.gif differ diff --git a/editor/form-developers-cookbook/skip-sections-based-on-input.gif b/editor/form-developers-cookbook/skip-sections-based-on-input.gif new file mode 100644 index 0000000000..09af6278ee Binary files /dev/null and b/editor/form-developers-cookbook/skip-sections-based-on-input.gif differ diff --git a/editor/form-developers-cookbook/soft-checks-vs-hard-checks.gif b/editor/form-developers-cookbook/soft-checks-vs-hard-checks.gif new file mode 100644 index 0000000000..7dce136a0e Binary files /dev/null and b/editor/form-developers-cookbook/soft-checks-vs-hard-checks.gif differ diff --git a/editor/form-developers-cookbook/tangerine-form-editors-cookbook--capture-local-date-and-time.png b/editor/form-developers-cookbook/tangerine-form-editors-cookbook--capture-local-date-and-time.png new file mode 100644 index 0000000000..d9d7437901 Binary files /dev/null and b/editor/form-developers-cookbook/tangerine-form-editors-cookbook--capture-local-date-and-time.png differ diff --git a/editor/form-developers-cookbook/tangerine-form-editors-cookbook--stop-watch.gif b/editor/form-developers-cookbook/tangerine-form-editors-cookbook--stop-watch.gif new file mode 100644 index 0000000000..6ac3546aba Binary files /dev/null and b/editor/form-developers-cookbook/tangerine-form-editors-cookbook--stop-watch.gif differ diff --git a/editor/form-developers-cookbook/tangerine-form-editors-cookbook--timed-items.gif b/editor/form-developers-cookbook/tangerine-form-editors-cookbook--timed-items.gif new file mode 100644 index 0000000000..3e196d1b6e Binary files /dev/null and b/editor/form-developers-cookbook/tangerine-form-editors-cookbook--timed-items.gif differ diff --git a/editor/form-developers-cookbook/tangy-form-tabs.gif b/editor/form-developers-cookbook/tangy-form-tabs.gif new file mode 100644 index 0000000000..e1608c2de5 Binary files /dev/null and b/editor/form-developers-cookbook/tangy-form-tabs.gif differ diff --git a/editor/form-developers-cookbook/tangy-gate-example.gif b/editor/form-developers-cookbook/tangy-gate-example.gif new file mode 100644 index 0000000000..2eb5c8f044 Binary files /dev/null and b/editor/form-developers-cookbook/tangy-gate-example.gif differ diff --git a/editor/form-developers-cookbook/valid-by-number-of-decimal-points.gif b/editor/form-developers-cookbook/valid-by-number-of-decimal-points.gif new file mode 100644 index 0000000000..c885ea212e Binary files /dev/null and b/editor/form-developers-cookbook/valid-by-number-of-decimal-points.gif differ diff --git a/editor/form-developers-cookbook/valid-if-greater-or-less-than-other-input.gif b/editor/form-developers-cookbook/valid-if-greater-or-less-than-other-input.gif new file mode 100644 index 0000000000..b98dcf5677 Binary files /dev/null and b/editor/form-developers-cookbook/valid-if-greater-or-less-than-other-input.gif differ diff --git a/editor/form-developers-cookbook/valid-if-inside-tangy-inputs-groups.gif b/editor/form-developers-cookbook/valid-if-inside-tangy-inputs-groups.gif new file mode 100644 index 0000000000..fd5f3fdeb6 Binary files /dev/null and b/editor/form-developers-cookbook/valid-if-inside-tangy-inputs-groups.gif differ diff --git a/editor/form-versions/index.html b/editor/form-versions/index.html new file mode 100644 index 0000000000..cb01dfd75c --- /dev/null +++ b/editor/form-versions/index.html @@ -0,0 +1,73 @@ + Form Versions - Tangerine Documentation

Form Versions

Throughout the lifetime of a form, many versions of a form may be deployed. When reviewing form responses collected on a past version of a form, it's important to open that form response using the version of the form it was collected on. When filling out a form response, it helps to think of the form response as a clear plastic sheet that you are writing on over the paper copy of the form. If the questions on that underlying physical form are removed, moved, or new questions are added, the clear plastic sheet you filled out previous form responses on no longer overlays correctly on the updated paper copy of that form. The consequence of not using Form Versions on a form that changes over time is that when reviewing past data, if 1a question was removed in a future version of a form, it will appear that data collected in the past are now missing that data. There are other scenarios where a form version should be created which we will cover in later sections, but first a simple example.

Example

First Release

forms.json:

[
+  {
+    "id" : "form-x",
+    "title" : "Form X",
+    "src" : "./assets/form-x/form.html",
+  }
+]
+

./assets/form-x/form.html:

<tangy-form id="form-x" title="Form X">
+  <tangy-input label="Question A" name="a"></tangy-input>
+  <tangy-input label="Question B" name="b"></tangy-input>
+</tangy-form>
+

Second Release

{
+  "id" : "form-x",
+  "title" : "Form X",
+  "src" : "./assets/form-x/form.html",
+  "formVersionId": "2",
+  "formVersions": [
+    {
+      "id": "1",
+      "src" : "./assets/form-x/1.html"
+    },
+    {
+      "id": "2",
+      "src" : "./assets/form-x/2.html"
+    }
+  ]
+}
+

./assets/form-x/form.html:

<tangy-form id="form-x" title="Form X">
+  <tangy-input label="Question A" name="a"></tangy-input>
+</tangy-form>
+

./assets/form-x/1.html:

<tangy-form id="form-x" title="Form X">
+  <tangy-input label="Question A" name="a"></tangy-input>
+  <tangy-input label="Question B" name="b"></tangy-input>
+</tangy-form>
+

./assets/form-x/2.html:

<tangy-form id="form-x" title="Form X">
+  <tangy-input label="Question A" name="a"></tangy-input>
+</tangy-form>
+

Third Release

{
+  "id" : "form-x",
+  "title" : "Form X",
+  "src" : "./assets/form-x/form.html",
+  "formVersionId": "3",
+  "formVersions": [
+    {
+      "id": "1",
+      "src" : "./assets/form-x/1.html"
+    },
+    {
+      "id": "2",
+      "src" : "./assets/form-x/2.html"
+    },
+    {
+      "id": "3",
+      "src" : "./assets/form-x/3.html"
+    }
+  ]
+}
+

./assets/form-x/form.html:

<tangy-form id="form-x" title="Form X">
+  <tangy-input label="Question A" name="a"></tangy-input>
+  <tangy-input label="Question C" name="c"></tangy-input>
+</tangy-form>
+

./assets/form-x/1.html:

<tangy-form id="form-x" title="Form X">
+  <tangy-input label="Question A" name="a"></tangy-input>
+  <tangy-input label="Question B" name="b"></tangy-input>
+</tangy-form>
+

./assets/form-x/2.html:

<tangy-form id="form-x" title="Form X">
+  <tangy-input label="Question A" name="a"></tangy-input>
+</tangy-form>
+

./assets/form-x/3.html:

<tangy-form id="form-x" title="Form X">
+  <tangy-input label="Question A" name="a"></tangy-input>
+  <tangy-input label="Question C" name="c"></tangy-input>
+</tangy-form>
+

When should I create a new Form Version?

Situations when a new Form Version should be created include:

  1. New question
  2. Removed question
  3. Options for a question added or removed.
  4. New page
  5. Removed page
  6. Reordered pages

Situations when a new Form Version can be skipped: 1. Label of a question has changed. 2. Variable marked as required.

Future Tooling proposals

Proposals for tools to help in managing form versions:

  • Linter idea - cli that checks if 2 form versions share the same path - makes sure there are no duplicate revision src paths. Check for dupes in formVersion.id and formVersion.src.
  • start new dir in tangerine dir called cli - this would be the first subcommand of a new cli. This is different from the server cli. tangerine-preview is another command that could be integrated into this new cli. Example - generate-new-form creates the scaffolding for a new form and could implement/facilitate the revisions feature.
  • Version incrementor - used for releases

Proposed Testing version support:

What are the different use-cases that the software must implement to fully support versions? The following list will list each case and the correct source for the form:

  • Using the tangerine-preview app and must use the most recent version: formInfo.src
  • Viewing a record created in a legacy group with no formVersionId and no formVersions: formInfo.src
  • viewing a record created in a legacy group with no formVersionId but does have formVersions using the legacyOriginal flag: legacyVersion.src
  • viewing a record created in a legacy group with no formVersionId but does have formVersions without legacyOriginal flag: lawd have mercy! formInfo.src
  • viewing a record created in a new group using formVersionId and has formVersions: formVersion.src
  • viewing a record created in a new group using formVersionId and does not have formVersions : formInfo.src
\ No newline at end of file diff --git a/editor/getting-started-editor/add-sections/index.html b/editor/getting-started-editor/add-sections/index.html new file mode 100644 index 0000000000..f299f86200 --- /dev/null +++ b/editor/getting-started-editor/add-sections/index.html @@ -0,0 +1 @@ + Adding Sections and Question to a Form - Tangerine Documentation

Adding Sections and Question to a Form

To add a new section to your instrument, hit "ADD SECTION".

The interface allows a drag-and-drop feature which enables reordering of the sections already created. The order in which the sections are listed, is the same as the sequence of screens that will be shown on the tablet when the tablet user is filling in the form.

Section Editor

Upon adding a new section, or selecting to "EDIT" your instrument section, you will see the section editor screen below.

If this is a new section, you might give it a new name from the section header. Click the pen icon on the right of the blue bar and overwrite the "title". Or any other of the configuration options. Then hit SUBMIT to save your edits.

Section Options

Each one of the sections has some options that you can control:

Show this section in the summary at the end -- mark only if this section is the last one, and if you have coded some summary/feedback otherwise leave unchecked.

Hide the back button -- checking this option will remove the Back button from the section when rendered on the tablet.

Hide the next button -- hides the Next button on a section. Sometimes advancing the page may depend on the selection of an item, just like it is on some EF inputs. Generally, you keep this unchecked.

right-to-left orientation -- switches the position of the Back and Next buttons for RTL languages.

Hide navigation labels -- Hides the label from the Back and Next buttons so that it becomes an arrow.

Hide navigation icons -- Hides the arrow from the back and Next buttons. If both this and the above are checked you will only see an orange button without labels and text.

Threshold: Number of incorrect answers before disabling remaining questions -- This option is used in conjunction with radio button questions only. Set it to the number of consecutive incorrect replies before the test is discontinued. You must mark an option in the radio button group as Correct for this to work. Only one correct option per question can be defined.

To add an item to your instrument section, click

This opens the item type selection interface.

These elements are subdivided into groups of item types (e.g., inputs, location, lists, misc):

Inputs

INPUT-DATE: This item type renders a calendar widget on the tablet

INPUT-TEXT: This item type is a standard numbers and letters field

INPUT-TIME: This item type displays a clock hour selection on the tablet

INPUT-NUMBER: This item type opens up the number keyboard on the tablet and doesn't allow any other non-number characters to be inserted here

Location

GPS: This item type automatically collects GPS coordinates of the tablet

LOCATION: The location item type requires a list of locations, e.g. school names by district and region to be imported to the Tangerine editor. Check out the location list section

Lists

CHECKBOX: This item type allows for multiple answers to be selected from a list of options

CHECKBOX GROUP: Allows for multiple answer options to be selected from a group of options

DROPDOWN (select): This item type allows for a single answer selection for longer lists

RADIO BUTTONS: This item type only allows for a single answer selection from a list of answer options

Miscellaneous

IMAGE: the image items allows you to select an image already uploaded by the media library and present it to the user on a particular section

SIGNATURE: this input type allows you to capture a signature by the assessor.

HTML CONTENT CONTAINER: This item type allows for flexible integration of headers, help text, or transition messages that do not require any user input or response.

QR CODE SCANNER: This item allows scanning of a QR and Data Matrix codes. Tangerine will capture and save the target info (e.g. URL).

EF TOUCH: This item type is to assess children's executive functions, including working memory, inhibitory control, and cognitive flexibility (requires RTI manual support to upload your images and sounds).

TIMED GRID: This item type facilitates timed assessment approaches, e.g., to assess letter sound knowledge, oral reading fluency or math operations.

UNTIMED GRID: This item type facilitates assessment approaches that are not timed, but require many items, e.g. oral counting, untimed reading comprehension tasks, etc.

CONSENT: This item is a special function for participant consent. If the participants responds that no consent is given, the form will be closed and data saved accordingly.

Depending on the element chosen, an interface for providing more detail on the item being rendered/created is presented in the Item Editor.

\ No newline at end of file diff --git a/editor/getting-started-editor/create-new-form/index.html b/editor/getting-started-editor/create-new-form/index.html new file mode 100644 index 0000000000..6969020e71 --- /dev/null +++ b/editor/getting-started-editor/create-new-form/index.html @@ -0,0 +1 @@ + Creating a New Instrument/Form - Tangerine Documentation

Creating a New Instrument/Form

Prerequisite: Existing group or create a new Group

After creating a group or opening an existing one, you will be presented with various options. The first action that you may wish to take is to create a new Form

From the main menu options, select Author and then click Forms. This will bring up a listing of all forms for this group.

You will notice that there is also a User Profile form. This form represents the profile each user has to fill in on the tablet, after they create their user login details. All information that you require in the user profile is attached to each record in the CSV export file. The user profile represents your assessor's information.

Click the plus icon to create a new form. A new form with a default name is created and a default section is placed into this form. To change the form's name, type in the new text and click Save.

Form Actions

Beside each of the forms you will see some action available. From here you can Edit, Print, Copy, Delete, or Archive a form. We recommend that you archive your forms instead of removing them. This will ensure that you can export the data of a form that is no longer in use.

Click on the pen icon to modify the form. Each form contains a number of sections that represent your form's pages

Click on the print icon to open a new printable menu where you can select two of the print details. This printable view we often use to quality assure (QA) the instrument or to get a list of variables and their definition. You can also use the print screen to save paper copy of your form.

Click on the copy icon to create a copy of the current instrument. You can copy an instrument to a different group or to the current one.

Click on the trash icon to delete this form.

Click on the archive icon to archive an assessment. All archived forms are moved to the bottom of the page

If a form is archived, click the un-archive button to activate a form. Only active forms are displayed in tablet listing of forms to the assessor.

\ No newline at end of file diff --git a/editor/getting-started-editor/create-new-group/index.html b/editor/getting-started-editor/create-new-group/index.html new file mode 100644 index 0000000000..0cae40d830 --- /dev/null +++ b/editor/getting-started-editor/create-new-group/index.html @@ -0,0 +1 @@ + Creating a New Group - Tangerine Documentation

Creating a New Group

Upon logging into your Tangerine instance, you will see a screen listing your Tangerine groups. You might think of groups as discrete data collection efforts that might contain several instruments or forms. If you have, e.g., a baseline data collection and an endline data collection for the same project, you might make these two different groups. When packaging your instruments into the apk (.apk is the application installation file format for Android devices) for installation on an Android device, Tangerine packages all instruments in a group. Thus, you should set up groups and categorize instruments accordingly.

On the main group listing page, click "+" button in the bottom right corner to create a new group

A group can be configured to display a drop-down to display available content-sets for which to configure the new group.

Enter a name for your group and click Submit

WARNING: If you are using the free service you are not able to create new groups.

If you are a Tangerine subscriber, or run Tangerine on your own server, the user1 account can be configured to be the only account with permissions to create new groups. If this is desired, please send a request for this configuration to support@tangerinehelp.zendesk.com Any Admin user can create a new group unless configured for the user1 account only

\ No newline at end of file diff --git a/editor/getting-started-editor/downloading-your-data/index.html b/editor/getting-started-editor/downloading-your-data/index.html new file mode 100644 index 0000000000..ae06e779c3 --- /dev/null +++ b/editor/getting-started-editor/downloading-your-data/index.html @@ -0,0 +1 @@ + Downloading your data (Download CSV) - Tangerine Documentation

Downloading your data (Download CSV)

All collected and synced data is available for you to download in a CSV format from the server. Each file downloaded contains some metadata output plus all of you variables. To find out how your variables map to the values in the csv file, please inspect your meta data export first (accessible from the form printing action under the Author tab)

To access the csv generation and download screen go to your group and then select Data. Depending on the permissions you have for your group, you will see Dashboard, Download Data/Export, and Uploads tabs available for selection. Under the Download Data tab there are 3 options:

Request spreadsheet - here you can create a download link for your csv of bundle multiple export files in a zip Spreadsheet Requests - here you will find a historical of generated requests that contain the exported data for the selected forms, at the time of the request. Note that we delete this links after one month, meaning that here you will see active links for only the past month Manage Spreadsheet templates - this links allows you to name and create a template with only selected columns for your csv export

Let's see how to generate a spreadsheet. Click the Request Spreadsheet button. On the next page you will see a header filter, where you can select the month and year for the export, or leave as is to export all data. You will also see a filter for "Exclude PII" - marking this option allows you to exclude personal information from the export (Note that only variables that have been configured as PII will be excluded). If you want you can also give an optional description to your Request.

Under the form selection area, you will see that you can select multiple form results to be exported or all forms. If you have created a data template you can slo select it from the drop down list, otherwise leave as "All data"

In the below example, we have 3 forms selected and no filters for month/year. Click the Submit Request button to start the export generation. Note that while the generation of the file is in progress you can mouse away and do other actions in Tangerine. We will see how to get to a previously generated request further down.

While the files are being generated you will see their status updated.Below I see that there are 3 forms in the queue, the first one is starting, and the other two are queued. You can also see. on the top right the "download all" button is greyed out

Once the files are generated and the request completed you can download them. On the below screenshot we see:

We can download each of the files individually by clicking Download file link on the right side of each form name We can download all files in a zip but clicking the Download all button on the top right.

Now let's go back to the Data tab. This time under the Data Download option click the Spreadsheet Request button. This will bring you to all historically generated spreadsheet requests. On the below screenshot I see that there are 12 pages of previous/historical requests. On the most recent page, which is the one that loads by default, I see that the top file is with status Available, meaning that I can download it and the rest of the requests on this page have already been removed, since they are older than 1 month. The status for those is File removed and there is no link. From this screen, you can hit the download button beside the request to get the zip file containing all form results. Or, you can click the More info link to go to the same screen that we saw earlier during the file generation and download the files individually.

Warning

To make use of the Exclude PII function, all of your personally identifiable inputs must be marked like such on the input edit page, or when inserting a new input/question on the insert new input page. All data must be collected prior to this configuration

To correctly interpret your data you need to know each variable and the corresponding values for the answer option. Please use the form's metadata print function under the form listing (go to Author->Forms). Access the metadata by clicking the print icon next to the form name and then select metadata.

Each CSV file includes all data from the form responses. We can think of these data as data and metadata. Each line of the data file represents one data entry for your instrument. The values displayed under each variable correspond to the values you have assigned to each response option when designing your instrument.

For each variable in Tangerine you will see a column or columns in the csv file. Some outputs like checkboxes and grids spread across multiple columns. For checkboxes you will see a column named using a this pattern "VariableName_Value". This means that for each option value pair for the checkbox group there will be a column . Similarly grids provide columns named ""GridVariableName_ItemPosition" meaning that for each item in the grid you will see a column with the item position.

If the CSV generation was successful, the following screen will present the group name, Form id , Start time and progress of the CSV download. Click Download to download the file.

Once the CSV has been downloaded, you can find it in your Downloads folder.

Metadata included in CSV

To correctly interpret your data you need to know each variable and the corresponding values for the answer option. Please use the form's metadata print function under the form listing (go to Author->Forms). Access the metadata by clicking the print icon next to the form name and then select metadata.

Each CSV file includes all data from the form responses. We can think of these data as data and metadata. Each line of the data file represents one data entry for your instrument. The values displayed under each variable correspond to the values you have assigned to each response option when designing your instrument.

For each variable in Tangerine you will see a column or columns in the csv file. Some outputs like checkboxes and grids spread across multiple columns. For checkboxes you will see a column named using a this pattern "VariableName_Value". This means that for each option value pair for the checkbox group there will be a column . Similarly grids provide columns named ""GridVariableName_ItemPosition" meaning that for each item in the grid you will see a column with the item position.

Each csv output from data collected by Tangerine has a set of metadata variables that are automatically output. Here is the current list:

Variable Name Meaning
_id 32-digit uuid identifying the unique form taken
formId text name of the form
startUnixtime unix timestamp that the form was first opened
endUnixtime unix timestamp that the form was completed
lastSaveUnixtime unix timestamp that the form was last opened and saved
buildId 32-digit uuid identifying the app version
buildChannel type of build: will be either build or production
deviceId 32-digit uuid identifying the registered device
groupId identifyer of the Tangerine group to which this device is registered
complete TRUE or FALSE, whether the form is complete
tangerineModifiedByUserId 3 2-digit uuid identifying the registered user who filled the form
caseId Only for Case module configuration,32-digit uuid identifying the Case the form is associated with
eventId Only for Case module configuration, 32-digit uuid identifying the Event within the Case that the form is associated with
eventFormId Only for Case module configuration, 32-digit uuid identifying the Form within the Event within the Case that the form is associated with
participantId Only for Case module configuration, 32-digit uuid identifying the participant for whom the form was filled
GridVar.duration For a grid variable GridVar, this indicates the time limit/duration for the grid
GridVar.time_remaining For a grid variable GridVar, this indicates the time remaining on the grid
GridVar.gridAutoStopped For a grid variable GridVar, this indicates if the grid stopped using the auto stop rule
GridVar.autoStop For a grid variable GridVar, this indicates the number of items that trigger the auto stop
GridVar.item_at_time For a grid variable GridVar, this indicates the item at the Xth second
GridVar. time_intermediate_captured For a grid variable GridVar, indicates the time time of the intermediate capture.
GridVar.number_of_items_correct For a grid variable GridVar, indicates the number of correct items on the grid
GridVar.number_of_items_attempted For a grid variable GridVar, indicates the number of items attempted on the grid
GridVar.items_per_minute For a grid variable GridVar, indicates the number of items per minute read by the child.
SectionId_firstOpenTime For a section with ID "sectionId" this it eh time stamp when it was first opened.
sr_classId For Teach module configuration, the Id of the class for this student
sr_studentId For Teach module configuration, the Id of the student

User Profile Metadata

The information for the user logged in to the Tangerine app is also included in the CSV outputs. This includes the metadata below. The location metadata is based on the location defined in the location list. The table below includes three location levels as an example.

Variable Name Meaning
user-profile._id 32-digit uuid identifying the registered user who filled the form
user-profile.item-1_firstOpenTime unix timestamp of when the form was opened
user-profile.item-1.first_name user's first name
user-profile.item-1.last_name user's last name
user-profile.item-1.gender user's gender
user-profile.item-1.phone user's phone number
user-profile.item-1.location.Region user's assigned Region
user-profile.item-1.location.District user's assigned District
user-profile.item-1.location.Village user's assigned Village

UNIX Timestamps conversion

To convert any of the unix timestamp inputs to readable dates, use this formula in Excel in a new column:
=A2/(60*60*24000) +"1/1/1970" Replace the A column with the corresponding column containing the timestamp. Now select the entire column and format it as Date or date + time. This will give you the human readable date and time.

What can I do with Tangerine’s timestamps?

Check precise duration of an assessment or subtests. If you notice your overall assessment time (per assessor or per group) is lengthy, you may wish to use the timestamps to provide data on which subtests are the most time consuming. Confirm when data was collected. If you have suspicions that your data collector may have manually changed the values on your Date/Time screen against your instructions, you can check for inconsistencies between the Date/Time subtest data and the timestamps, as these cannot be altered by the user unless s/he also alters the date and time settings on the device.

Downlaoding data video

\ No newline at end of file diff --git a/editor/getting-started-editor/edit-form/index.html b/editor/getting-started-editor/edit-form/index.html new file mode 100644 index 0000000000..04083cef32 --- /dev/null +++ b/editor/getting-started-editor/edit-form/index.html @@ -0,0 +1 @@ + Editing the Form - Tangerine Documentation

Editing the Form

Once you have selected to edit your new or existing instrument/form, you will see a screen like the one below. This view lists the different sections of your instrument. Each section can contain one or more items.

On the tablet, items in any one section can be seen on a single screen. The user moves through items by "scrolling" down the screen. The user moves through sections by hitting "Next" or "Back" on the tablet.

The form editor provides the interface for adding and editing instrument sections and items. This interface provides controls that make the following actions possible:

EDIT HTML - Clicking this button shows you the HTML code behind the form. Please edit the HTML with care.

PREVIEW -- This control enables you to have a preview of your form in the current state

SAVE -- This control allows you to save the form in its current state

ADVANCED -- This Control enables you to access the on-open logic and on-change logic. This logic is used for skipping an entire instrument section.

ADD SECTION -- This allows you to add a section of items to your instrument.

For each section there are two actions available.

COPY SECTION -- This icon copies the section and autmatically renames all variables.

EDIT -- This icon opens the interface to edit an instrument section (e.g., add items)

DELETE -- This icon deletes this instrument section

In this view, you can drag sections to reorder them.

\ No newline at end of file diff --git a/editor/getting-started-editor/editor-overview/index.html b/editor/getting-started-editor/editor-overview/index.html new file mode 100644 index 0000000000..447b788442 --- /dev/null +++ b/editor/getting-started-editor/editor-overview/index.html @@ -0,0 +1 @@ + Overview - Tangerine Documentation

Overview

Tangerine contains an extremely extensible foundational framework that allows the form developer to highly customize the core functioanlity, actions, and progression. The following section provides guidance and common examples for extending Tangerine.

What is a Tangerine form?

Tangerine Preview

\ No newline at end of file diff --git a/editor/getting-started-editor/input-types/index.html b/editor/getting-started-editor/input-types/index.html new file mode 100644 index 0000000000..4a2eab7097 --- /dev/null +++ b/editor/getting-started-editor/input-types/index.html @@ -0,0 +1 @@ + Different Input Types - Tangerine Documentation

Different Input Types

Item Editor

The item editor screen is similar for many of the item types. It usually contains the following elements:

Variable name: This name has to be unique for any instrument/form, as this will be used for the CSV data output as column header with each observation/child assessed/interview being a row. Avoid special characters and spaces, use lowercase only (e.g., "age").

Label: This will be the item label/name that will be displayed to the user (e.g. "How old are you?")

Question number: If you input a number in here, you will see that the entire questions is moved to the right and the question number stands out when looking at the page. Use this if you are looking for a visual effect like this.

Hint Text: This field allows you to add text that acts as a hint for the user (e.g., "Enter child's age or year of birth, if known")

Toggle (On/Off) settings per question

Required: Selecting this checkbox marks the element as a required field. This ensures that users will enter a value before proceeding to other instrument sections or finalizing the instrument/form.

Disabled: Selecting this checkbox marks the element as inactive. The item is visible to the user on the tablet, but its value cannot be changed.

Hidden: Selecting this checkbox makes the element inactive AND invisible on the tablet.

PII - Personally Identifiable Information. This setting marks the input as PII which lets you remove this field from the CSV export file when this option is selected. Easily share data by de-identifying it whn using this option.

You will have access to two more tabs allowing you to add Skip Logic and Validation to your questions.

Conditional Display tab

Skip if - Use this field to define logic for the input to be omitted if the condition is true.

Show if - Use this field to define logic for the input to be displayed when the condition is true.

Find examples of skip logic here

Validation tab

Warn if - fail the user submission with a warning only once. This is to alert the user that perhaps they are entering something out of the defined boundaries. The warn if logic will allow the user to proceed if they click the Next/Submit button a second time.

Warning Text - The warning text to be displayed to the user when the condition is triggered.

Valid if - define logic to contain a valid definition of the input

Error text - the text to be displayed when the above condition is not met.

Find examples of validation logic here

GPS Item

Use the GPS item to record the location (longitude & latitude) of the user while filling in the instrument/form.

We suggest placing a GPS item always in its own section. Do not combine with other items.

When selecting to add an item of the GPS type, Tangerine presents the below item editor screen.

The following might be a way to configure this item:

Variable name: Enter "gps".

Hint Text: Leave blank

Hit "SUBMIT" to see the below item added to the section editor.

On the tablet this item will look like this:

Location

This item type offers a dropdown listing of predefined location information such as, e.g., region, district, and school name. Before you add this item to your form, you need to upload a location list and configure Tangerine:

To see a video of how to do this Watch the video

First, decide what levels you would like to show and prepare a CSV file that contains your locations and ids. Each column header will present a location level (e.g. column A header might be region; column B header might be district, etc.). Make sure each level/column header contains only a single word and no spaces.

Second, define the location levels for Tangerine. Click Configure/Location Lists and add the desired levels using the '+' sign

Click "Create a New Location Level".

Enter the name of the "highest" location level under "Label" (e.g. region). Repeat this process for all other location levels, however, for each "child" level, select which is the parent level. E.g. in the case of district, the "Parent Level" would be "region", and so forth. Hit "Submit" to save.

Warning

You cannot delete location levels. Be careful and deliberate as you define them for your group. If you made a mistake or need to make changes, contact the Tangerine helpdesk.

Next, click the Import tab and select "Import CSV". Double check that your CSV file contains only those columns that you have defined as levels and spelled exactly the same!

Download a sample location list file with IDs:(https://drive.google.com/file/d/1y3X0aMJKRYx51--jPC_3OUkMpmkhyEn-/view)

Once you selected the CSV, Tangerine will ask you to map the location levels you already defined to the column headers found in your csv file.

Click on the small arrows to select the matching column header. For each ID field, select "Map a column to a level and select "AutoGeneratedID" for the ID as shown in the example below.

Then click "Process CSV" as shown in the screen above. Once processing is completed, you will receive a notification like this:

Once you have successfully uploaded a location list and prepared Tangerine, you can add the location input item to your form. The following might be a way to configure this item, once you completed the above steps.

If you think that your location list may change significantly, and you'd like to re-upload it at some point thus not implementing any changes manually, consider adding manual IDs to your location file. In the instruction above, you saw how to add the Autogenerated ID that Tangerine inserts. These IDs, however, are not persisted when you re-upload your location file. In such cases, where you know that you'd rather re-upload the entire file, we recommend that you insert an ID column and you preserve those IDs across versions of your location list. By doing that you ensure that any matching on location IDs (and not on Location labels) will be persisted.

To upload a location file with IDs, first create those IDS in the Excel file. Then, on the Map location field instead of selecting Autogenerated ID, select the column representing the ID for the corresponding level.

Warning

We highly recommend that you create a location list export before importing any new data. Click the Export tab and export your current location list. ALways work with this export file to make sure your list is up-to-date

Warning

Upon upload you are wiping out the location list. All previous results collected will be missing the labels for those location and will contain only the old IDs; all data on the tablets under the Visits tab will show the ID rather than the label. This is why we highly recommend altering the location list manually or maintaining the IDs across different version of the location list in your Excel file.

Check out this Excel file to see a location list with IDs that you can import in Tangerine. The formula for generating the IDs can be copied to your own file: Download a sample location list file with IDs: (https://drive.google.com/file/d/1y3X0aMJKRYx51--jPC_3OUkMpmkhyEn-/view)

Variable name: Enter "location".

Hint Text: Leave blank

Show levels (ex. county,subcounty): Enter "province,district,school"

Show meta-data: Leave blank

Hit "SUBMIT" to see the below item added to the section editor.

On the tablet this item will look like this:

Warning

Without a location list, no location will be displayed, and the item will be seen as "loading".

Checkbox group (Checkbox, Radio Buttons, or Dropdowns)

This item type lets you define a checkbox item that lets a user pick one or more options.

The following might be a way to configure this item:

Variable name: Enter "books".

Label: Enter "What kind of books do you like to read?"

Hint Text: Enter "Tick all that apply"

Value (answer option): Enter the data value for the first answer option, e.g., "0"

Label (answer option): Enter the label for the first answer option, e.g., "None"

Hit "ADD ANOTHER" to add additional answer options, e.g.:

Value (answer option): "1"

Label (answer option): "Storybooks (fiction)"

Hit "ADD ANOTHER" to add additional answer options, e.g.:

Value (answer option): "2"

Label (answer option): "Books about real things (non-fiction)

When done adding all answer options, hit "SUBMIT".

Warning

The item type "CHECKBOX" only adds a single checkbox to the form, with the item label being the answer option label.

On the tablet this single checkbox item will look like this:

Radio Buttons

Radio buttons are an item type used for items that allow for only one answer. The configuration for radio buttons is the same as for checkbox group with one exception. You will see that radio button options have a check mark to indicate which answer is correct. This is used in conjunction with the Threshold defined in the section header.

When you have a threshold defined as 4, and for each question there is only one question option defined as correct, Tangerine will discontinue (hide the questions) after 4 consecutive replies are given as not correct. You can use this in EGMA tasks or in any other scenario where this makes sense

On the tablet the radio button item will look like this:

Dropdown is an item type used for items that allow for only one answer to be picked from a dropdown list of items. This item type is convenient when there are many options to choose from. The configuration for a dropdown item is the same as for checkbox group.

On the tablet the dropdown item will look like this:

Timed Grid

This item type facilitates timed assessment approaches, e.g., to assess letter sound knowledge, oral reading fluency or math operations. The following might be a way to configure this item:

Variable name: Enter "letter_sound".

Number of columns: Enter the number of columns by which you'd like to organize the items. Enter, e.g. "4" (Tip: choose less columns for larger items, like words or operation problems)

Hint Text: Leave blank

Auto Stop: The autostop field defines the number of consecutive incorrect items, starting from the first one, after which the test stops automatically. For example, with an autostop value of 10, if a child has the first 10 items all incorrect, the test will stop. If a child has the first 4 items correct and then the following 10 items incorrect, the test will not autostop.

Mark entire rows: This option allows the user to mark and entire row of items as incorrect (e.g. if a child skipped an entire row of sounds in a letter sound assessment)

Duration in seconds: Enter the time allowed to complete this assessment, e.g. "60" for 60 seconds or one minute.

Options (each option separated by a space): Enter all grid items here. Separate each item by a space from the next; if you have extra spaces please remove them!

When done adding all answer options, hit "SUBMIT".

Warning

For these kids of assessments there are usually instructions preceding the assessment items. Insert those instructions as a "HTML CONTENT CONTAINER" item first, as shown below, followed by the "TIMED GRID" in the same section. We recommend to only feature the instructions (HTML Content) and Timed Grid in any one section of your instrument/form

On the tablet the timed grid item will look like this:

HTML Content Container

This item type allows for flexible integration of headers, help text, or transition messages that do not require any user input or response. You can treat this container as a variable and hide or show different instructional text upon the selection of different options.

The following might be a way to configure this item:

Variable name: Enter "Assessor instructions".

Mark entire rows: This option allows the user to mark an entire row of items as incorrect (e.g. if a child skipped an entire row of sounds in a letter sound assessment)

Rows 1-X: Insert assessor instructions, use html tags to insert line breaks or formatting (e.g.
for a line break; text for bolding a piece of text, etc.).

When done adding all answer options, hit "SUBMIT".

On the tablet this HTML container item will look like this:

Copying Items

If you have an element and/or content which is the same as a previous element (e.g., radio buttons) that you would like to insert into your instrument quickly, without having to click "INSERT HERE" again, there is a COPY feature that you can use to do this. First, enter your original content (e.g., variable name, labels, and values) and then click SUBMIT. Once the first step is complete, next you click on the icon. Doing so automatically creates a duplicate of all your original content, except the variable name, which you will need to edit, if desired. In the image below, you can see that all duplicates are auto-populated with the name "widget," followed by an underscore, and a mix of letters and numbers (always different from the previous copy). If you would like to, you can edit all the content of the copy to fit your needs.

\ No newline at end of file diff --git a/editor/getting-started-editor/media/add_GPS_element.png b/editor/getting-started-editor/media/add_GPS_element.png new file mode 100644 index 0000000000..951d0f87c7 Binary files /dev/null and b/editor/getting-started-editor/media/add_GPS_element.png differ diff --git a/editor/getting-started-editor/media/add_checkbox.png b/editor/getting-started-editor/media/add_checkbox.png new file mode 100644 index 0000000000..6311b3110f Binary files /dev/null and b/editor/getting-started-editor/media/add_checkbox.png differ diff --git a/editor/getting-started-editor/media/add_input.png b/editor/getting-started-editor/media/add_input.png new file mode 100644 index 0000000000..9dc8545026 Binary files /dev/null and b/editor/getting-started-editor/media/add_input.png differ diff --git a/editor/getting-started-editor/media/add_section.png b/editor/getting-started-editor/media/add_section.png new file mode 100644 index 0000000000..e071f66d81 Binary files /dev/null and b/editor/getting-started-editor/media/add_section.png differ diff --git a/editor/getting-started-editor/media/advanced_editor.png b/editor/getting-started-editor/media/advanced_editor.png new file mode 100644 index 0000000000..634a43998b Binary files /dev/null and b/editor/getting-started-editor/media/advanced_editor.png differ diff --git a/editor/getting-started-editor/media/archiveImage.png b/editor/getting-started-editor/media/archiveImage.png new file mode 100644 index 0000000000..f7511e975b Binary files /dev/null and b/editor/getting-started-editor/media/archiveImage.png differ diff --git a/editor/getting-started-editor/media/autogenerated_id.png b/editor/getting-started-editor/media/autogenerated_id.png new file mode 100644 index 0000000000..895185db2f Binary files /dev/null and b/editor/getting-started-editor/media/autogenerated_id.png differ diff --git a/editor/getting-started-editor/media/checkboxGroup.png b/editor/getting-started-editor/media/checkboxGroup.png new file mode 100644 index 0000000000..71ae03316a Binary files /dev/null and b/editor/getting-started-editor/media/checkboxGroup.png differ diff --git a/editor/getting-started-editor/media/content-sets-dropdown.png b/editor/getting-started-editor/media/content-sets-dropdown.png new file mode 100644 index 0000000000..8586a73319 Binary files /dev/null and b/editor/getting-started-editor/media/content-sets-dropdown.png differ diff --git a/editor/getting-started-editor/media/copyButton.png b/editor/getting-started-editor/media/copyButton.png new file mode 100644 index 0000000000..4187e41e06 Binary files /dev/null and b/editor/getting-started-editor/media/copyButton.png differ diff --git a/editor/getting-started-editor/media/copyImage.png b/editor/getting-started-editor/media/copyImage.png new file mode 100644 index 0000000000..2c4d6cae2c Binary files /dev/null and b/editor/getting-started-editor/media/copyImage.png differ diff --git a/editor/getting-started-editor/media/copying_items.png b/editor/getting-started-editor/media/copying_items.png new file mode 100644 index 0000000000..9fb1111c98 Binary files /dev/null and b/editor/getting-started-editor/media/copying_items.png differ diff --git a/editor/getting-started-editor/media/createForm.gif b/editor/getting-started-editor/media/createForm.gif new file mode 100644 index 0000000000..cbf6be8d0e Binary files /dev/null and b/editor/getting-started-editor/media/createForm.gif differ diff --git a/editor/getting-started-editor/media/deleteButton.png b/editor/getting-started-editor/media/deleteButton.png new file mode 100644 index 0000000000..ea5e717a23 Binary files /dev/null and b/editor/getting-started-editor/media/deleteButton.png differ diff --git a/editor/getting-started-editor/media/downloadCsv.png b/editor/getting-started-editor/media/downloadCsv.png new file mode 100644 index 0000000000..c447ddd8b0 Binary files /dev/null and b/editor/getting-started-editor/media/downloadCsv.png differ diff --git a/editor/getting-started-editor/media/downloadCsvFile.png b/editor/getting-started-editor/media/downloadCsvFile.png new file mode 100644 index 0000000000..8b16ac45ff Binary files /dev/null and b/editor/getting-started-editor/media/downloadCsvFile.png differ diff --git a/editor/getting-started-editor/media/downloadGeneration.png b/editor/getting-started-editor/media/downloadGeneration.png new file mode 100644 index 0000000000..1dd3ff8ce1 Binary files /dev/null and b/editor/getting-started-editor/media/downloadGeneration.png differ diff --git a/editor/getting-started-editor/media/downloadLIsting.png b/editor/getting-started-editor/media/downloadLIsting.png new file mode 100644 index 0000000000..76f533dc0e Binary files /dev/null and b/editor/getting-started-editor/media/downloadLIsting.png differ diff --git a/editor/getting-started-editor/media/downloadSelection.png b/editor/getting-started-editor/media/downloadSelection.png new file mode 100644 index 0000000000..6a83c298a9 Binary files /dev/null and b/editor/getting-started-editor/media/downloadSelection.png differ diff --git a/editor/getting-started-editor/media/dropdown_menu.png b/editor/getting-started-editor/media/dropdown_menu.png new file mode 100644 index 0000000000..02baffad1c Binary files /dev/null and b/editor/getting-started-editor/media/dropdown_menu.png differ diff --git a/editor/getting-started-editor/media/editButton.png b/editor/getting-started-editor/media/editButton.png new file mode 100644 index 0000000000..55ff374139 Binary files /dev/null and b/editor/getting-started-editor/media/editButton.png differ diff --git a/editor/getting-started-editor/media/editForm.gif b/editor/getting-started-editor/media/editForm.gif new file mode 100644 index 0000000000..c77182a838 Binary files /dev/null and b/editor/getting-started-editor/media/editForm.gif differ diff --git a/editor/getting-started-editor/media/formActions.gif b/editor/getting-started-editor/media/formActions.gif new file mode 100644 index 0000000000..eb6d110b5d Binary files /dev/null and b/editor/getting-started-editor/media/formActions.gif differ diff --git a/editor/getting-started-editor/media/formActions.png b/editor/getting-started-editor/media/formActions.png new file mode 100644 index 0000000000..45907117ee Binary files /dev/null and b/editor/getting-started-editor/media/formActions.png differ diff --git a/editor/getting-started-editor/media/gps_location_entry.png b/editor/getting-started-editor/media/gps_location_entry.png new file mode 100644 index 0000000000..42bff3cc5b Binary files /dev/null and b/editor/getting-started-editor/media/gps_location_entry.png differ diff --git a/editor/getting-started-editor/media/gps_output.png b/editor/getting-started-editor/media/gps_output.png new file mode 100644 index 0000000000..bf89b42eed Binary files /dev/null and b/editor/getting-started-editor/media/gps_output.png differ diff --git a/editor/getting-started-editor/media/groupCreation.png b/editor/getting-started-editor/media/groupCreation.png new file mode 100644 index 0000000000..95207b9618 Binary files /dev/null and b/editor/getting-started-editor/media/groupCreation.png differ diff --git a/editor/getting-started-editor/media/how-it-works.png b/editor/getting-started-editor/media/how-it-works.png new file mode 100644 index 0000000000..582f7bcbcf Binary files /dev/null and b/editor/getting-started-editor/media/how-it-works.png differ diff --git a/editor/getting-started-editor/media/htmlContainer.png b/editor/getting-started-editor/media/htmlContainer.png new file mode 100644 index 0000000000..70e51bdd25 Binary files /dev/null and b/editor/getting-started-editor/media/htmlContainer.png differ diff --git a/editor/getting-started-editor/media/htmlContainerView.png b/editor/getting-started-editor/media/htmlContainerView.png new file mode 100644 index 0000000000..2e2e81db0b Binary files /dev/null and b/editor/getting-started-editor/media/htmlContainerView.png differ diff --git a/editor/getting-started-editor/media/image85.png b/editor/getting-started-editor/media/image85.png new file mode 100644 index 0000000000..9046f2d22d Binary files /dev/null and b/editor/getting-started-editor/media/image85.png differ diff --git a/editor/getting-started-editor/media/inputVar.png b/editor/getting-started-editor/media/inputVar.png new file mode 100644 index 0000000000..a7ba6b6fa4 Binary files /dev/null and b/editor/getting-started-editor/media/inputVar.png differ diff --git a/editor/getting-started-editor/media/insertButton.png b/editor/getting-started-editor/media/insertButton.png new file mode 100644 index 0000000000..152719f815 Binary files /dev/null and b/editor/getting-started-editor/media/insertButton.png differ diff --git a/editor/getting-started-editor/media/interpretMetadata.png b/editor/getting-started-editor/media/interpretMetadata.png new file mode 100644 index 0000000000..c2155120da Binary files /dev/null and b/editor/getting-started-editor/media/interpretMetadata.png differ diff --git a/editor/getting-started-editor/media/itemInterface.png b/editor/getting-started-editor/media/itemInterface.png new file mode 100644 index 0000000000..95620da9e8 Binary files /dev/null and b/editor/getting-started-editor/media/itemInterface.png differ diff --git a/editor/getting-started-editor/media/locationLevel.png b/editor/getting-started-editor/media/locationLevel.png new file mode 100644 index 0000000000..6e829a3473 Binary files /dev/null and b/editor/getting-started-editor/media/locationLevel.png differ diff --git a/editor/getting-started-editor/media/location_element.png b/editor/getting-started-editor/media/location_element.png new file mode 100644 index 0000000000..7cc500c29c Binary files /dev/null and b/editor/getting-started-editor/media/location_element.png differ diff --git a/editor/getting-started-editor/media/location_level_create.png b/editor/getting-started-editor/media/location_level_create.png new file mode 100644 index 0000000000..3f2cf0beea Binary files /dev/null and b/editor/getting-started-editor/media/location_level_create.png differ diff --git a/editor/getting-started-editor/media/meta01.png b/editor/getting-started-editor/media/meta01.png new file mode 100644 index 0000000000..c2155120da Binary files /dev/null and b/editor/getting-started-editor/media/meta01.png differ diff --git a/editor/getting-started-editor/media/newGroupname.png b/editor/getting-started-editor/media/newGroupname.png new file mode 100644 index 0000000000..5ea27408b1 Binary files /dev/null and b/editor/getting-started-editor/media/newGroupname.png differ diff --git a/editor/getting-started-editor/media/on_open.png b/editor/getting-started-editor/media/on_open.png new file mode 100644 index 0000000000..f4fdd7e4aa Binary files /dev/null and b/editor/getting-started-editor/media/on_open.png differ diff --git a/editor/getting-started-editor/media/plusButton.png b/editor/getting-started-editor/media/plusButton.png new file mode 100644 index 0000000000..956e5e2897 Binary files /dev/null and b/editor/getting-started-editor/media/plusButton.png differ diff --git a/editor/getting-started-editor/media/plusButton_small.png b/editor/getting-started-editor/media/plusButton_small.png new file mode 100644 index 0000000000..956e5e2897 Binary files /dev/null and b/editor/getting-started-editor/media/plusButton_small.png differ diff --git a/editor/getting-started-editor/media/prior_valid.png b/editor/getting-started-editor/media/prior_valid.png new file mode 100644 index 0000000000..f1c970dcb1 Binary files /dev/null and b/editor/getting-started-editor/media/prior_valid.png differ diff --git a/editor/getting-started-editor/media/process_csv.png b/editor/getting-started-editor/media/process_csv.png new file mode 100644 index 0000000000..f924aa2485 Binary files /dev/null and b/editor/getting-started-editor/media/process_csv.png differ diff --git a/editor/getting-started-editor/media/radio_button_complete.png b/editor/getting-started-editor/media/radio_button_complete.png new file mode 100644 index 0000000000..cb77b29889 Binary files /dev/null and b/editor/getting-started-editor/media/radio_button_complete.png differ diff --git a/editor/getting-started-editor/media/radio_button_create.png b/editor/getting-started-editor/media/radio_button_create.png new file mode 100644 index 0000000000..f88a68e64d Binary files /dev/null and b/editor/getting-started-editor/media/radio_button_create.png differ diff --git a/editor/getting-started-editor/media/saveImage.png b/editor/getting-started-editor/media/saveImage.png new file mode 100644 index 0000000000..6cebfda6d9 Binary files /dev/null and b/editor/getting-started-editor/media/saveImage.png differ diff --git a/editor/getting-started-editor/media/sectionEditor.gif b/editor/getting-started-editor/media/sectionEditor.gif new file mode 100644 index 0000000000..0c870a477c Binary files /dev/null and b/editor/getting-started-editor/media/sectionEditor.gif differ diff --git a/editor/getting-started-editor/media/sectionEditor.png b/editor/getting-started-editor/media/sectionEditor.png new file mode 100644 index 0000000000..8ca3d87f50 Binary files /dev/null and b/editor/getting-started-editor/media/sectionEditor.png differ diff --git a/editor/getting-started-editor/media/section_detail_editor.png b/editor/getting-started-editor/media/section_detail_editor.png new file mode 100644 index 0000000000..37eb71b851 Binary files /dev/null and b/editor/getting-started-editor/media/section_detail_editor.png differ diff --git a/editor/getting-started-editor/media/section_details.png b/editor/getting-started-editor/media/section_details.png new file mode 100644 index 0000000000..15004e83fc Binary files /dev/null and b/editor/getting-started-editor/media/section_details.png differ diff --git a/editor/getting-started-editor/media/section_editor.png b/editor/getting-started-editor/media/section_editor.png new file mode 100644 index 0000000000..83bfcb5555 Binary files /dev/null and b/editor/getting-started-editor/media/section_editor.png differ diff --git a/editor/getting-started-editor/media/show_if.png b/editor/getting-started-editor/media/show_if.png new file mode 100644 index 0000000000..cf83078ae3 Binary files /dev/null and b/editor/getting-started-editor/media/show_if.png differ diff --git a/editor/getting-started-editor/media/showif.png b/editor/getting-started-editor/media/showif.png new file mode 100644 index 0000000000..cf83078ae3 Binary files /dev/null and b/editor/getting-started-editor/media/showif.png differ diff --git a/editor/getting-started-editor/media/single_checkbox.png b/editor/getting-started-editor/media/single_checkbox.png new file mode 100644 index 0000000000..7a39ec8273 Binary files /dev/null and b/editor/getting-started-editor/media/single_checkbox.png differ diff --git a/editor/getting-started-editor/media/skip01.png b/editor/getting-started-editor/media/skip01.png new file mode 100644 index 0000000000..dfa7b67203 Binary files /dev/null and b/editor/getting-started-editor/media/skip01.png differ diff --git a/editor/getting-started-editor/media/skip02.png b/editor/getting-started-editor/media/skip02.png new file mode 100644 index 0000000000..799367e663 Binary files /dev/null and b/editor/getting-started-editor/media/skip02.png differ diff --git a/editor/getting-started-editor/media/skip03.png b/editor/getting-started-editor/media/skip03.png new file mode 100644 index 0000000000..b26b65e518 Binary files /dev/null and b/editor/getting-started-editor/media/skip03.png differ diff --git a/editor/getting-started-editor/media/skip04.png b/editor/getting-started-editor/media/skip04.png new file mode 100644 index 0000000000..97f5c0dd38 Binary files /dev/null and b/editor/getting-started-editor/media/skip04.png differ diff --git a/editor/getting-started-editor/media/skip05.png b/editor/getting-started-editor/media/skip05.png new file mode 100644 index 0000000000..34373f7d26 Binary files /dev/null and b/editor/getting-started-editor/media/skip05.png differ diff --git a/editor/getting-started-editor/media/submit.png b/editor/getting-started-editor/media/submit.png new file mode 100644 index 0000000000..7657b586d4 Binary files /dev/null and b/editor/getting-started-editor/media/submit.png differ diff --git a/editor/getting-started-editor/media/syntax_hilite.png b/editor/getting-started-editor/media/syntax_hilite.png new file mode 100644 index 0000000000..4b47f9d362 Binary files /dev/null and b/editor/getting-started-editor/media/syntax_hilite.png differ diff --git a/editor/getting-started-editor/media/timedGrid.png b/editor/getting-started-editor/media/timedGrid.png new file mode 100644 index 0000000000..7c477441be Binary files /dev/null and b/editor/getting-started-editor/media/timedGrid.png differ diff --git a/editor/getting-started-editor/media/timed_grid_complete.png b/editor/getting-started-editor/media/timed_grid_complete.png new file mode 100644 index 0000000000..07005f259f Binary files /dev/null and b/editor/getting-started-editor/media/timed_grid_complete.png differ diff --git a/editor/getting-started-editor/media/validation01.png b/editor/getting-started-editor/media/validation01.png new file mode 100644 index 0000000000..aa67122b37 Binary files /dev/null and b/editor/getting-started-editor/media/validation01.png differ diff --git a/editor/getting-started-editor/media/validation02.png b/editor/getting-started-editor/media/validation02.png new file mode 100644 index 0000000000..deafb8533d Binary files /dev/null and b/editor/getting-started-editor/media/validation02.png differ diff --git a/editor/getting-started-editor/media/validation03.png b/editor/getting-started-editor/media/validation03.png new file mode 100644 index 0000000000..b53ccff3af Binary files /dev/null and b/editor/getting-started-editor/media/validation03.png differ diff --git a/editor/getting-started-editor/skip-logic/index.html b/editor/getting-started-editor/skip-logic/index.html new file mode 100644 index 0000000000..0f8dd6bc69 --- /dev/null +++ b/editor/getting-started-editor/skip-logic/index.html @@ -0,0 +1,21 @@ + Skip Logic - Tangerine Documentation

Skip Logic

Every instrument/form, section, and individual item provides an interface for adding logic, e.g. skip logic, that controls the interactivity and presentation of the instrument, section, or item.

There are two types of skip logic that can be applied:

  • On form level - used to skip an entire section and implement logic that is applicable to the entire form
  • On section/page level
    • Most common case: You can implement those in the item's 'Skip If' field, or
    • Used for more complex conditions Implement the skip in the section's on-change logic

The functions that we use for skip logic are:

  • getValue('name') - to check the value of input 'name'
    • Use this for Text, Number, Dates, Time, Radio buttons, or Drop down lists
  • getValue('name').includes('value') - to check if 'value' is in the selected items of 'name'
    • Use this call to check if a value is in the list of selected values of a checkbox group input.
  • grid specific functions - look at the end of this page for more information.

Join skip logic conditions using the && (AND) and || (OR) operators getValue('repeatedgrade') == '1' && getValue('age') >= 1

Negate a condition using the ! (NOT) operator getValue('repeatedgrade') != '1' Or !getValue('grades_taught').includes('1')

Logic at Question/ Input level

When we tap the Conditional Display tab, we see that here we can enter logic to hide or show a question based on previous input on that or other previous sections.

In the above screenshot we see that there is a "Skip if" input a "Show if" input. The skip if condition will skip the question if the condition is true. The show if condition will show the question if the condition is true. For example:

To skip a question when the value of a previous question is not equal to 777, use the skip if condition To show a question when the value of a previous question is equal to 777, use the show if condition You can see how the two possible ways of skipping are opposites of one another. We can use either logic for each scenario but sometimes it is easier to think in a positive condition and other times it is easier to think in a negative condition.

In all skip logic conditions, except for those based on grids (timed input), we use the getValue() function. To write the conditions from above in terms of skip logic we need:

The value(s) to be used in the condition The variable name Here is how the above condition looks in skip logic:

To skip a question when the value of question with variable homework is not equal 777 To show a question when the value of question with variable homework is equal 777

This logic can be used to compare the values of Text, Number, Email, Radio Buttons, Dropdown select input types.

Here is the actual skip logic.

Skip if: getValue('homework') != '777' Show if: getValue('homework') == '777' You can see how similar the logic is. The == sign above means "equal" and the != sign means "not equal"

You can enter only one Show-if or Skip-if condition per question. You can also use the && (AND) and || (OR) logical operators to combine conditions. We will not look into that here.

The above use of the function applies to all input types except for Checkbox group, Timed Grid, Untimed Grid, and Location

To build logic based on the answers of a Checkbox group question, we use the getValue function but in a different way. Here we check if one of the selected options is the desired one. To skip a question when ONE of the values of a Checkbox question with variable homework is not equal 777 To show a question when ONE of the values of a Checkbox question with variable homework is equal 777 Here is how this condition will look

Skip if: !getValue('homework').includes('777') Show if: getValue('homework').includes('777') Note how above we are asking if one of the selected options is 777 or if it isn't 777 - this is done with the ! (NOT) operator in front of the function.

Take a look at how these examples look in Tangerine:

For radio button question similar to below, where we want to show up the Other Specify input only when Other is selected:

Use this logic in the homework_other question

For a checkbox group question similar to the one below

Use this logic to show to show a question in the stu_language_other

Warning

The skip logic commands used in Tangerine are case-sensitive and space-sensitive. You must type precisely the name of the variables which you want to reference.

Warning

Use single straight quotation marks to demarcate variables names ', do NOT use single slanted quotation marks ' or double quotation marks ".

Logic at instrument/form level

At the instrument/form level, accessing this logic editor is via advanced settings in the section editor.

Click on ADVANCED to see the screen below with "on-open" and "on-change" entries.

As outlined earlier, at the item level, such logic can be added in the "Show if" field in the item editor.

On-open and on-change

As the name suggest, on-open logic is only executed when the form is opened whereas on-change logic is always executed whenever a change happens in the whole form. When selecting on-open logic either at the instrument/form level or in the section editor, the following screen appears. The interface allows JavaScript logic to be incorporated into the instrument.

(Skip) Logic Examples

You want to skip an entire section:

Navigate to and select the "on-change" at the instrument/form level. This logic will not work if you insert it in a section (it must be defined on form level)

In this example, the section gets skipped based on responses from a previous item, e.g., if the respondent answered negatively to a previous question "Do you have children?". Note that the sectionID is provided in Tangerine in the section details as shown below. Form level skip logic is used to present or hide an entire section page to the user. This is very useful when managing a workflow and you need to display some sections but hide others according to the selected option for a question. For example, you can show a certain section only for grade 1 and hide it if grade 2 is selected.

if(getValue('children') == '1')
+{sectionEnable('item_1')}
+else
+{sectionDisable('item_1')}
+

You want to hide a set of items based on responses to an item in a previous section:

Navigate to and select the "on-open" at the section level.

In this example several items in this section are hidden based on the participant's response to the item about the child's schooling experience in a previous section.

if(getValue('school') == '1')
+{itemShow('grade')
+itemShow('repeatedgrade')
+itemShow('dropout')}
+else
+{itemHide('grade')
+itemHide('repeatedgrade')
+itemHide('dropout')}
+

You want to hide a set of items based on responses to two items in a previous section:

Navigate to and select the "on-open" at the section level.

In this example the item "teachers_name" should only be shown if the participant's previous response to "teacher_available" was yes = 1 AND if the participant' previous response to "class_selected" was "1".

if(getValue(' teacher_available') === '1' && getValue('class_selected') === '1' )
+{itemShow('teachers_name')}
+else
+{itemHide('teachers_name')}
+

The Logic interface offer syntax highlighting. This is handy when you have errors in your code. Below is an example of an error and sample message.

Logic at section level

At the section level, the logic editor can be accessed by editing the Section Details clicking the pen icon on the right of the blue bar (where one can also rename the section).

Skip logic with grid specific functions

You may be in the situation where you are required to perform a skip based on some results from a grid. We provide four functions that you can use in your skip logic to show or hide questions or sections based on the results of a grid.

Showing a question based on the number of attempted items on a grid

If you'd like to hide a question when the number of attempted items on a particular grid is over a certain threshold you can make use of the 'numberOfItemsAttempted(input)' function. If your grid variable is 'letter_sound' and the question you want to skip is 'Q_1' then in the question Q_1 I can insert the below skip logic(under Show If) to show it only when the number of attempted items on the grid is greater than 10

numberOfItemsAttempted(inputs.letter_sound) > 10
+

Showing a question based on the number of correct items of a grid

Sometimes it may be the case where you want to show a question only if there are a certain N items on the grid answered correctly. In those cases, we make use of the 'numberOfCorrectItems(input)' function. If your grid variable is 'letter_sound' and the question you want to skip is 'Q_1' then in the question Q_1 I can insert the below skip logic(under Show If) to show this question only when the number of correct items on the grid is greater than 0

numberOfCorrectItems(inputs.letter_sound) > 0
+

Show a question only if the grid did not auto stop

If you have set the autostop value of a grid with variable name 'letter_sound' and you want to show a question only when the grid did not discontinue due to a triggered auto stop, then you can insert the below logic into the question's Show If field:

typeof inputs.letter_sound != 'undefined' && inputs.letter_sound.gridAutoStopped
+

The use of the '!' gives us the opposite of the result returned by the function. If the grid stopped the result will be true. When we use the '!' in front of the function, it means that, when the grid did not stop we want a positive answer hence show the question.

Show a question based on the words per minute read on a grid

It may happen that you need to show a question only to advanced students. In those cases, we make use of the function 'itemsPerMinute(input)' This function returns the number of items per minute read by the student. We can use it, just as before, in the Show If input field of a question, like so:

itemsPerMinute(inputs.letter_sound) > 35
+

This call will force a question to be displayed only when the rate of reading was higher than 35 workds per minute.

NOTE: All of the above functions can also be used to show or a hide an entire section page.

\ No newline at end of file diff --git a/editor/getting-started-editor/validation/index.html b/editor/getting-started-editor/validation/index.html new file mode 100644 index 0000000000..5555325bb0 --- /dev/null +++ b/editor/getting-started-editor/validation/index.html @@ -0,0 +1,10 @@ + Validation - Tangerine Documentation

Validation

Tangerine provides the option to check the validity of an input field. Navigate to the "Valid if" field in the Item Editor.

When we tap the Validation tab, we see that here we can enter logic to validate a question based on previous or current input. For the number input type we can also directly enter min and max values. For all validation we also have the option to specify a default error message.

Inputs that are common for all validation screens are:

Warn-if: this input allows you to define logic to issue a warning if the condition is true. A warning validation means that clicking Next or Submit will fail the first time the user clicks it but will allow the user to continue on the second trial

Warning Text: this is the text displayed to the user when a warning condition is triggered

Valid if: this input allows you to define logic to issue an error if the condition is true. A valid if logic means that clicking Next or Submit will fail when the condition is met and the user is forced to correct the input

Error text: this is the text displayed to the user when a validation error is triggered

All warning and validation logic can make use of the getValue function but also you can access the current input's value by referring to input.value. In many cases we also use the parseInt function to convert the text input to a number.

In the below example you see an input defined which will trigger a validation if the value is great then 5, Between 5 and 7 it is only a warning message, meaning that the user can proceed if they click Next/Submit again but if the value is greater then 7 the user has to correct the input.

The easiest validation is for number type of inputs. There we can directly specify the minimum and maximum values for the number that we can accept.

I will add an age input of type number. Then on the Validation tab i have defined min and max values, as well as error text.

I want to add one more validation to my stu_number variable, making sure it is exactly 6 characters long. For this I will use input.value.length == 6

Here is how this looks in student number input.

Question/ Input Validation Examples

If, for example, the value entered into an "INPUT-NUMBER" field should have 9 or more characters, enter the following into "Valid if" for this item:

input.value.length > 9
+

Tangerine also allow you to compare the value entered in the current item to a value entered for another, earlier item. This might be the case, e.g. for attendance when observing a classroom. That is, when recording attendance, the number of children present should not exceed the number of children enrolled. Assume that a relevant variable name of the earlier item was "boys_enrolled" and the current items is about the boys present, this might be the validation logic to enter under "Valid if" for boys_present.

parseInt(input.value) <= inputs.boys_enrolled.value
+

If you want to validate that a number input is in between a particular range but also allow a 'No Reply' answer, use the below validation rule:

input.value >= 0 
+&& input.value <= 10 
+|| input.value == 999
+

Here we make sure that the user can only enter numbers between 0 and 10 but also 999 as a reply to this question.

You can now use the Mutually Exclusive option on the checkbox edit page to achieve the same functionality.

Deprecated

If you have a list of checkboxes with the option No (value 0), NA (value 888), and some other options, and you'd like to make sure that the assessor cannot select the options No or the option NA along with other available options you need to implement a rule like the one below. The variable name in the below example is TQ1

(!getValue('TQ1').includes('888') && !getValue('TQ1').includes('0')) 
+|| (getValue('TQ1').includes('888') 
+|| getValue('TQ1').includes('0')) 
+&& getValue('TQ1').length == 1
+
\ No newline at end of file diff --git a/editor/index.html b/editor/index.html new file mode 100644 index 0000000000..3882ff0e98 --- /dev/null +++ b/editor/index.html @@ -0,0 +1 @@ + Editor Quick Links - Tangerine Documentation
\ No newline at end of file diff --git a/editor/password-policy/index.html b/editor/password-policy/index.html new file mode 100644 index 0000000000..c7e1ac48ba --- /dev/null +++ b/editor/password-policy/index.html @@ -0,0 +1 @@ + Password Policy - Tangerine Documentation

Password Policy

The default password policy is in config.defaults.sh. Although you can change it, it is recommended that you have a strong password policy.

Relevant variables: - T_PASSWORD_POLICY - The policy, coded in the form of a regular expression. - T_PASSWORD_RECIPE - Description of the policy, to enable user to create a password that will pass the policy.

Ideally a password policy should include the following specifications: - 8 characters or more - at least one upper case letters - at least one lower case letter - at least one special character - at least one numeral

Each group can have a unique password policy. When a group is created, the default policy and recipe from config.sh are copied over to the passwordPolicy and passwordRecipe variables in app-config.json.

For some groups, it may be more useful to have a simpler password policy on client than on editor. Here is an example:

  • "passwordPolicy": "(?=.\d)(?=.[a-z])(?=.*[A-Z]).{8,}",
  • "passwordRecipe": "Must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters",

Editor on the server uses the T_PASSWORD_POLICY and T_PASSWORD_RECIPE variables.

Tips

If the server's shell has problems interpreting any of the special characters when loading T_PASSWORD_POLICY from config.sh, you may need to add an escape \ before the special character.

The site https://www.regextester.com/ has been very helpful in testing out password policies.

\ No newline at end of file diff --git a/editor/project_managment/case-archive/assets/tangerine-archive-button.png b/editor/project_managment/case-archive/assets/tangerine-archive-button.png new file mode 100644 index 0000000000..3b7d57bd8b Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-archive-button.png differ diff --git a/editor/project_managment/case-archive/assets/tangerine-case-archive-dropdown.png b/editor/project_managment/case-archive/assets/tangerine-case-archive-dropdown.png new file mode 100644 index 0000000000..e4e9a88631 Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-case-archive-dropdown.png differ diff --git a/editor/project_managment/case-archive/assets/tangerine-case-archive-warning.png b/editor/project_managment/case-archive/assets/tangerine-case-archive-warning.png new file mode 100644 index 0000000000..9851616252 Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-case-archive-warning.png differ diff --git a/editor/project_managment/case-archive/assets/tangerine-case-archive.png b/editor/project_managment/case-archive/assets/tangerine-case-archive.png new file mode 100644 index 0000000000..cd7c23d195 Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-case-archive.png differ diff --git a/editor/project_managment/case-archive/assets/tangerine-case-archived-reopen.png b/editor/project_managment/case-archive/assets/tangerine-case-archived-reopen.png new file mode 100644 index 0000000000..318dc28750 Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-case-archived-reopen.png differ diff --git a/editor/project_managment/case-archive/assets/tangerine-case-show-archived.png b/editor/project_managment/case-archive/assets/tangerine-case-show-archived.png new file mode 100644 index 0000000000..9fb3bac47f Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-case-show-archived.png differ diff --git a/editor/project_managment/case-archive/assets/tangerine-case-unarchive-dropdown.png b/editor/project_managment/case-archive/assets/tangerine-case-unarchive-dropdown.png new file mode 100644 index 0000000000..6fad307b9a Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-case-unarchive-dropdown.png differ diff --git a/editor/project_managment/case-archive/assets/tangerine-configure-security.png b/editor/project_managment/case-archive/assets/tangerine-configure-security.png new file mode 100644 index 0000000000..2558820772 Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-configure-security.png differ diff --git a/editor/project_managment/case-archive/assets/tangerine-configure.png b/editor/project_managment/case-archive/assets/tangerine-configure.png new file mode 100644 index 0000000000..0579297bf2 Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-configure.png differ diff --git a/editor/project_managment/case-archive/assets/tangerine-event-archive-warning.png b/editor/project_managment/case-archive/assets/tangerine-event-archive-warning.png new file mode 100644 index 0000000000..f7a4d9a52c Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-event-archive-warning.png differ diff --git a/editor/project_managment/case-archive/assets/tangerine-event-archive.png b/editor/project_managment/case-archive/assets/tangerine-event-archive.png new file mode 100644 index 0000000000..ebba4c6a4c Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-event-archive.png differ diff --git a/editor/project_managment/case-archive/assets/tangerine-event-show-archived.png b/editor/project_managment/case-archive/assets/tangerine-event-show-archived.png new file mode 100644 index 0000000000..d14f850e2e Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-event-show-archived.png differ diff --git a/editor/project_managment/case-archive/assets/tangerine-event-unarchive-warning.png b/editor/project_managment/case-archive/assets/tangerine-event-unarchive-warning.png new file mode 100644 index 0000000000..4c87213e05 Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-event-unarchive-warning.png differ diff --git a/editor/project_managment/case-archive/assets/tangerine-event-unarchive.png b/editor/project_managment/case-archive/assets/tangerine-event-unarchive.png new file mode 100644 index 0000000000..0558b95d58 Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-event-unarchive.png differ diff --git a/editor/project_managment/case-archive/assets/tangerine-kabob.png b/editor/project_managment/case-archive/assets/tangerine-kabob.png new file mode 100644 index 0000000000..0d1f3f8c6e Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-kabob.png differ diff --git a/editor/project_managment/case-archive/assets/tangerine-security-checkbox.png b/editor/project_managment/case-archive/assets/tangerine-security-checkbox.png new file mode 100644 index 0000000000..1aa97a3ed0 Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-security-checkbox.png differ diff --git a/editor/project_managment/case-archive/assets/tangerine-security-permissions.png b/editor/project_managment/case-archive/assets/tangerine-security-permissions.png new file mode 100644 index 0000000000..7844054a60 Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-security-permissions.png differ diff --git a/editor/project_managment/case-archive/assets/tangerine-unarchive-button.png b/editor/project_managment/case-archive/assets/tangerine-unarchive-button.png new file mode 100644 index 0000000000..c3e414cf5e Binary files /dev/null and b/editor/project_managment/case-archive/assets/tangerine-unarchive-button.png differ diff --git a/editor/project_managment/case-archive/case-archive/index.html b/editor/project_managment/case-archive/case-archive/index.html new file mode 100644 index 0000000000..a69b0e69da --- /dev/null +++ b/editor/project_managment/case-archive/case-archive/index.html @@ -0,0 +1 @@ + Case, Event and Form Archive and Unarchive - Tangerine Documentation

Case, Event and Form Archive and Unarchive

Introduction

We have released an update to Tangerine which allows for the archiving and un-archiving of both events, and forms within events. This is an extension of the already existing functionality by which an entire case can be archived. The purpose of this is to empower data management teams using Tangerine to "clean up" messy cases where extraneous data has been added to a case in error, or by a conflict situation. The purpose of this document is to summarize both the configuration to enable this, and to demonstrate the use of these functions. This functionality will only apply to the web-based version of Tangerine, and will not be available on tablets.

Enabling access to archive functions:

Under "Users and Roles", either a new role needs to be created, or additional rights will need to be granted to an existing role to provide access to the archive and unarchive functionality. To access this, login to Tangerine, and go to configure for the specific group that you are working with.

By default, the admin role will NOT have this functionality. You can either update the role, or create a new role as needed. Note that the USER1 account will have already have access to this functionality. We suggest that all users of the web interface be given their own accounts for better tracking of activity within Tangerine, and that you do not normally use the USER1 account. Under roles, you can either modify the admin account to add this access, or create a new role with access, and assign that role to users. Note that a single user can be assigned to more than one role. By clicking on the configure icon () next to the trash can icon, you can edit an existing role (such as the admin role above).

You will see that there are 4 separate permissions related to archiving. You can actively apply these as needed for your project team. There are 2 archive permissions to add (can_archive_events, and can_archive_forms), and two additional unarchive permissions (can_unarchive_events, and can_unarchive_forms). These are in addition to the can_archive_cases and can_unarchive_cases permissions which previously existed. For each site, there may be reasons to manage these each independently.

Archiving and unarchiving of forms and events:

Once the correct roles have been applied to a user, they will be able to use the archiving/un-archiving functionality.

In the upper right corner, there is a kabob menu ( ) that can access the archive and delete functions at the case level. Note that we suggest NEVER using the delete function, as it is much harder to restore a deleted case.

Select the "Archive" option, and then click "OK" to confirm the archiving of a case.

If you need to review an archived case, you can select the "View Archived Cases" checkbox on the cases screen.

When you re-open an archived case, it shows with an indication that it is archived, and the menu will provide you with the option to un-archive it.

Selecting Unarchive will restore the case and all forms back to a normal (unarchived) status.

Additionally, this functionality will cascade down to the event and form level. If you have a duplicate event, or an event that was added in error. You can archive the event, and all the forms within the event by clicking on the "archive" icon ( ) to the right of the event.

Click the icon and then confirm that you wish to archive the event by clicking "OK."

If you need to visualize an archived event, you can check the checkbox for "Show Archived Events." The archived event will show as greyed out, with an "Unarchive" icon next to it ( ). If you select an archived event, the forms within it will also be archived, but similarly are viewable by checking the "Show Archived Forms" box. The forms will be greyed out and show the unarchive button to the right.

Clicking on the unarchive button for an event will prompt you to confirm if you want to unarchive the event.

Unarchiving an event will cascade down, and automatically unarchive any associated forms for that event. If you need to unarchive an event, and archive SOME forms within that event, you can unarchive, and then go into the event to archive individual forms within that event as needed.

\ No newline at end of file diff --git a/editor/project_managment/case-module/case-data-model/index.html b/editor/project_managment/case-module/case-data-model/index.html new file mode 100644 index 0000000000..b6fef651dc --- /dev/null +++ b/editor/project_managment/case-module/case-data-model/index.html @@ -0,0 +1,39 @@ + Case Management Data Model - Tangerine Documentation

Case Management Data Model

Case Entities and Relationships:

Entities: Participant, Case, CaseEvent, EventForm, FormResponse

Relationships:

  • A Case is related to many Participants.
  • A Case is related to many CaseEvents.
  • A CaseEvent is related to many EventForms.
  • An EventForm is related to one FormResponse.
  • An EventForm is related to one Participant.

Then there are definition Entities that are not in the data:

Entities: ParticipantDefinition, CaseDefinition, CaseEventDefinition, EventFormDefinition, FormDefinition

How the Case Entities and Relationships are expressed in Tangerine

A typical Tangerine Case will feature: - one document (type = case) that has all of the Case-related meta-data mentioned below, and - multiple documents with forms data. These forms are linked by formResponseId in the case's eventForms array.

There is not a 1-to-1 mapping between Tangerine entities and data persisted to the server. Records are saved in Tangerine as a TangyFormResponse doc, identified by "collection": "TangyFormResponse" in the Couchdb document.

A TangyFormResponse is a very generic container for data; it does not by default manage any of its relationships. Most of the Case-related entities are saved in a single TangyFormResponse as "type": "case" and explicitly manages these relationships inside the eventForms array:

{
+  "_id": "8744ff38-4c3e-487d-814d-ddcb916a41d5",
+  "collection": "TangyFormResponse",
+  "type": "case",
+  "eventForms": [
+    {
+      "id": "c7b6ee21-793a-11ea-9144-710703689c79",
+      "complete": true,
+      "caseId": "c7b23330-793a-11ea-9144-710703689c79",
+      "participantId": "",
+      "caseEventId": "c7b6ee20-793a-11ea-9144-710703689c79",
+      "eventFormDefinitionId": "enrollment-screening-form",
+      "formResponseId": "c7b6ee22-793a-11ea-9144-710703689c79"
+    },
+    {
+      "id": "c7b6ee23-793a-11ea-9144-710703689c79",
+      "complete": true,
+      "caseId": "c7b23330-793a-11ea-9144-710703689c79",
+      "participantId": "8a46e841-d80c-4038-857c-7ae43c1d42cf",
+      "caseEventId": "c7b6ee20-793a-11ea-9144-710703689c79",
+      "eventFormDefinitionId": "mnh-sociodemographic-form",
+      "formResponseId": "c7b6ee24-793a-11ea-9144-710703689c79"
+    }
+  ]
+}
+
Any other documents related to a case save only form data and a small amount of meta-data.

How relationships are mapped in an EventForm

Let's first look at the Case hierarchy: A Case has a collection of CaseEvents.

A CaseEvent has a collection of EventForms, which manage the relationship between : - the CaseEvent (stored as _id in the CaseEvent and caseId in the CaseEvent eventForms array ) - Participant (stored in the CaseEvent's particiaptns array and also linked via participantId in the CaseEvent's eventForms's array) - CaseEvent (stored as caseEventId in the CaseEvent's events array) - TangyFormResponse (stored as formResponseId and available externally in a separate document)

class EventForm {
+  id:string;
+  participantId:string
+  complete:boolean = false
+  caseId:string; 
+  caseEventId:string;
+  eventFormDefinitionId:string;
+  formResponseId:string;
+  data?:any;
+  constructor() {
+
+  }
+}
+

The formResponseId links to a TangyFormResponse, which contains the data filled out in a form.

\ No newline at end of file diff --git a/editor/project_managment/case-module/case-management-group/index.html b/editor/project_managment/case-module/case-management-group/index.html new file mode 100644 index 0000000000..40a010f0bb --- /dev/null +++ b/editor/project_managment/case-module/case-management-group/index.html @@ -0,0 +1,66 @@ + Case Management Group - Tangerine Documentation

Case Management Group

Case Management allows us to define Case Definitions for different purposes such as following a participant in a drug trial over the course of many events, where each event may require many forms to be filled out. In order to create and find cases, you will need to configure the "case-home" as the "homeUrl" value in app-config.json.

Configuring Cases

Case Management allows us to define Case Definitions for different purposes such as following a participant in a drug trial over the course of many events, where each event may require many forms to be filled out.

To configure cases, there are four files to modify.

First add a reference to the new Case Definition in the case-definitions.json. Here is an example of a case-definitions.json file that references two Case Definitions.

File: case-definitions.json

[
+  {
+    "id": "case-definition-1",
+    "name": "Case Definition 1",
+    "src": "./assets/case-definition-1.json"
+  },
+  {
+    "id": "case-definition-2",
+    "name": "Case Definition 2",
+    "src": "./assets/case-definition-2.json"
+  }
+]
+

Then create the corresponding Case Definition file...

File: case-definition-1.json

{
+  "id": "case-definition-1",
+  "formId": "case-definition-1-manifest",
+  "name": "Case Definition 1",
+  "description": "Description...",
+  "startFormOnOpen": {
+    "eventId": "event-definition-1",
+    "eventFormId": "event-form-1"
+  },
+  "eventDefinitions": [
+   {
+      "id": "event-definition-1",
+      "name": "Event Definition 1",
+      "description": "Description...",
+      "repeatable": false,
+      "required": true,
+      "eventFormDefinitions": [
+        {
+          "id": "event-form-definition-1",
+          "formId": "form-1",
+          "name": "Form 1",
+          "required": true,
+          "repeatable": false
+        }
+      ]
+    }
+  ]
+}
+

Case Definition Templates

As a Data Collector uses the Client App, they navigate a Case's hierarchy of Events and Forms. Almost every piece of information they see can be overriden to display custom variables and logic by using the Case Definition's templates. This section describes the templates available and what variables are available. Note that all templates are evaluated as Javascript Template Literals. There are many good tutorials online about how to use Javascipt Template Literals, here are a couple of Javascript Template Literals examples that we reference often for things like doing conditionals and loops.

Schedule

case schedule templates

templateScheduleListItemIcon default:

"templateScheduleListItemIcon": "${caseEvent.status === 'CASE_EVENT_STATUS_COMPLETED' ? 'event_note' : 'event_available'}"
+

templateScheduleListItemPrimary default:

"templateScheduleListItemPrimary": "<span>${caseEventDefinition.name}</span> in Case ${caseService.case._id.substr(0,5)}"
+

templateScheduleListItemSecondary default:

"templateScheduleListItemSecondary": "<span>${caseInstance.label}</span>"
+

Variables available: - caseService: CaseService - caseDefinition: CaseDefinition - caseEventDefinition: CaseEventDefinition - caseInstance: Case - caseEvent: CaseEvent

Debugging Case Definition Templates

debug case templates

The case references a Form in the formId property of the Case Definition. Make sure there is a form with that corresponding Form ID listed in forms.json with additional configuration for search.

File: forms.json

[
+  {
+     "id" : "case-definition-1-manifest",
+     "type" : "case",
+     "title" : "Case Definition 1 Manifest",
+     "description" : "Description...",
+     "listed" : true,
+     "src" : "./assets/case-definition-1-manifest/form.html",
+     "searchSettings" : {
+        "primaryTemplate" : "Participant ID: ${searchDoc.variables.participant_id}",
+        "shouldIndex" : true,
+        "secondaryTemplate" : "Enrollment Date: ${searchDoc.variables.enrollment_date}, Case ID: ${searchDoc._id}",
+        "variablesToIndex" : [
+           "participant_id",
+           "enrollment_date"
+        ]
+     }
+  }
+]
+

Configuring two-way sync

Because you may need to share cases across devices, configuring two-way sync may be necessary. See the Two-way Sync Documentation for more details. Note that you sync Form Responses, and it's the IDs of that you'll want to sync in the "formId" of the Case Definition in order to sync cases.

Configuring the Schedule

One of the two tabs that Data Collectors see when they log into Tangerine is a "Schedule" tab. This schedule will show Case Event's on days where they are have an estimated day, scheduled day, and/or occurred on day. You can set these three dates on an event using the following APIs.

caseService.setEventEstimatedDay(idOfEvent, timeInUnixMilliseconds)
+caseService.setEventOccurredOn(idOfEvent, timeInUnixMilliseconds)
+caseService.setEventScheduledDay(idOfEvent, timeInUnixMilliseconds)
+
\ No newline at end of file diff --git a/editor/project_managment/case-module/case-module-cookbook/index.html b/editor/project_managment/case-module/case-module-cookbook/index.html new file mode 100644 index 0000000000..6099d582aa --- /dev/null +++ b/editor/project_managment/case-module/case-module-cookbook/index.html @@ -0,0 +1,11 @@ + Case Module Cookbook - Tangerine Documentation

Case Module Cookbook

In the following example, from an on-change hook or on-open, we can look up the corresponding participant for the current form, then look the age variable that has been previously set on that participant.

const currentEventId = window.location.hash.split('/')[5]
+const currentFormId = window.location.hash.split('/')[6]
+const participantId = caseService
+  .case
+  .events
+  .find(event => event.id === currentEventId)
+  .eventForms
+  .find(eventForm => eventForm.id === currentFormId)
+  .participantId
+const age = caseService.getParticipantData(participantId, 'age')
+

\ No newline at end of file diff --git a/editor/project_managment/class/index.html b/editor/project_managment/class/index.html new file mode 100644 index 0000000000..6f12946720 --- /dev/null +++ b/editor/project_managment/class/index.html @@ -0,0 +1,10 @@ + Class module - Tangerine Documentation

Class module

Setup

Add class to the T_MODULES property in config.sh. Create a new group via editor; this takes advantage of the class module which sets important class-related properties and file. If you /must/ use the create-group command, change homeUrl to dashboard and set uploadUnlockedFormReponses = true.

Feedback

Feedback for each form item (subtask) can be entered using the Settings editor.

Feedback is displayed if available on the Student grouping report. The following fields are available: - ${feedback.example} - ${feedback.skill} - ${feedback.assignment}

The following code can be used to format feedback on the Student Grouping report:

<div class='feedback-assignment'>${feedback.assignment}</div>
+
<div class='feedback-example'>${feedback.example}.</div>
+

Note that the use of these formatting commands are optional.

Here is a sample feedback message that uses this formatting:

These students are doing really well. Consider framing your feedback to these student as follows: <div class='feedback-example'>${feedback.example}.</div>
+Reflect on these students results: why do you think did these students were particularly successful in ${feedback.skill}.
+Was there a specific ${feedback.skill} strategy or activity you used? Did they already know this content?
+Is there another strategy or activity they could do to extend their ${feedback.skill} skills?
+Consider giving these students supplementary story: <div class='feedback-assignment'>${feedback.assignment}</div> to read
+and make 3-5 inferential questions for them to answer. You may also consider engaging these students as peer mentors to
+others as these other students do additional practice.
+

Scoring

There are 3 options for scoring in Class: - Using a TANGY-TIMED grid - Using a hidden formId+_score field to store the calculated score value when the form is submitted using the on-change javascript - Using a score calculated at report run-time.

The dashboard.service populaceTransformedResult function loops through the inputs; for each item type, it calculates the value, score, and max.

It also keeps a running tally of the sum of all max values (totalMax).

Here are the default rules for each input type: * TANGY-INPUT: * value: value field * score: value field * max: max field * TANGY-RADIO-BUTTONS: * value: loops through the options and uses the value from the non-empty option * score: value * max: Use value of the highest option. * TANGY-CHECKBOXES: * value: loops through the options and uses the value from the non-empty option * score: value * max: Use value of the highest option.

For a TANGY-TIMED input, once the value and score have been calculated for each item and populated into an answeredQuestions array, we loop through this array and calculate aggregates for the tangy-form-item.

  • TANGY-TIMED:
  • value:
  • score: totalCorrect

For tangy form items that use a _score field: Calculate the totalAnswers by subtracting 1 from the item.inputs.length (to account for the _score field) Use score for totalCorrect and totalAnswers for maxValueAnswer, unless the max value was assigned earlier.

Finally, there is support for calculating the score at report-time by looping through answeredQuestions and summing the score and max values.

\ No newline at end of file diff --git a/editor/project_managment/configuration/index.html b/editor/project_managment/configuration/index.html new file mode 100644 index 0000000000..335e5ee7d8 --- /dev/null +++ b/editor/project_managment/configuration/index.html @@ -0,0 +1 @@ + Configuration - Tangerine Documentation

Configuration

App Configuration

app-config.json should have the following properties defined.

  • homeUrl:string The default route to load when no route is specified. Think of this as the root url
  • securityPolicy:string[]. This is an array of all the combinations of the security policies to be enforced in the app. NOTE: noPassword and password are mutually exclusive. Only one should be provided and not both.
    • password
    • noPassword
  • associateUserProfileMode: This is the mode that determines where a user profiles comes from after a user has created an account on a device. Note, a "User" is tied together across devices by a single "User Profile". The account on the device is simply a security mechanism for using the profiles.
  • remote: Selecting this will result in a user being promted to enter a "code" after they register an account. This code is the last 6 characters of their User Profile ID. Typically a Group Admin would create a User Profile doc on the server and then send this code to the person associated with the User Profile. When the user enters this code on the tablet, the tablet will reach out over the Internet and download the corresponding User Profile and any content associated with that User Profile. Because all content is downloaded for that user, it can also be used as a way to fully restore a user's data on a new or recovered tablet. However note that this data is mode is not compatible with using CouchDB sync settings on any form definitions' sync settings.
  • local-new: This option allows users who register an Account on a tablet to create a new User Profile. This is also the default if no option is selected.
  • local-exists: This option is useful when using devices are set up using the "Centrally Managed Device" setup which would result in a facility's User Profiles already being on that device. When this option is selected, a drop down of unclaimed user profiles appears when accounts are being registered.
\ No newline at end of file diff --git a/editor/project_managment/editor-guide/index.html b/editor/project_managment/editor-guide/index.html new file mode 100644 index 0000000000..2194e9446b --- /dev/null +++ b/editor/project_managment/editor-guide/index.html @@ -0,0 +1 @@ + Releasing updates to existing forms - Tangerine Documentation

Releasing updates to existing forms

Gotchas

  • If you remove an input from an item or move that input to another item, when a user resumes a form response that was created with the prior version, content for that input will appear to have dissappeared.
  • If you add remove an item from a form, when users resume form responses created on with the prior version, it will appear they have lost data since the item has been removed.
\ No newline at end of file diff --git a/editor/project_managment/project_admin/index.html b/editor/project_managment/project_admin/index.html new file mode 100644 index 0000000000..44f9ded71f --- /dev/null +++ b/editor/project_managment/project_admin/index.html @@ -0,0 +1 @@ + Project Managment Overview - Tangerine Documentation

Project Managment Overview

  • How do you manage a project in Tangerine?
  • What is a group?
  • Why do you need a group?
  • Creating a group
  • What is a role in Tangerine?
  • Creating roles
  • How to create new users and add them to a group?
  • How do you manage users?

Mobile Device Use

  • What devices will Tangerine work with?
  • How do you manage devices?
  • How to manage Tangerine Updates?
  • Android Installation
  • Web Browser Installation
  • Tangerine Installation Decision Tree
  • Registration and Login
  • Administering Instruments
  • Resuming Instruments
  • Syncing Data
  • Location Data Management
  • Filter location data based on the user’s profile location
  • Import a location list
  • Location list sample file with IDs
\ No newline at end of file diff --git a/editor/reserved-words/index.html b/editor/reserved-words/index.html new file mode 100644 index 0000000000..dab2e14d9e --- /dev/null +++ b/editor/reserved-words/index.html @@ -0,0 +1 @@ + Reserved words in Tangerine - Tangerine Documentation

Reserved words in Tangerine

The following words should not be used as field names in Tangerine because they will cause clashes with mysql export and other features: - buildChannel - buildId - caseDefinitionId - caseId - caseRoleId - caseEventId - collection - complete - dbRevision - deviceId - estimate - eventformid - formID_sanitized - groupId - inactive - participantId - required - startDate - startUnixtime - type - uploadDatetime

\ No newline at end of file diff --git a/editor/translations/index.html b/editor/translations/index.html new file mode 100644 index 0000000000..b6b0a76543 --- /dev/null +++ b/editor/translations/index.html @@ -0,0 +1,9 @@ + Translations - Tangerine Documentation

Translations

There are two types of translations in Tangerine, Application Translations and Content Translations. Applications Translations are translations on Tangerine User Interface such as the "next" button on a form, or the "Sync" menu item in the top level tablet menu. Content Translations are the translations on forms such as the "label" and "hint text" of a question. The method of providing translations are different for the two.

Content Translations

Translations for specific languages are embedded in content, thus portable and specific to that content. The <t-lang> component (https://github.com/ICTatRTI/translation-web-component) is used to detect the language assigned to the HTML doc. In the following example, the label on the hello input will be "Hello" if English is set as the language, "Bonjour" if French is selected as the language.

    <tangy-input 
+        name="hello"
+        label="
+            <t-lang en>Hello</t-lang>
+            <t-lang fr>Bonjour</t-lang>
+        "
+    >
+    </tangy-input>
+

Application Translations

By default, when you create a new Group in Tangerine, a set of default Application Translations are provided. Currently that includes English, French, Jordanian, Khmer, and Russian. When deploying, these languages are selectable on a per tablet basis under the Tangerine Settings menu.

If you would like to add or modify translations for your group, currently we would recommend setting up your group with a Github Integration to allow editing of the content of your group's content. In your group's content folder you will find two types of files, the list of translations in translations.json, and then a file per translation such as translation.fr.json for French, translations.ru.json for Russian, etc. By adding to, or removing, or modifying entries in translations.json, this will modify what translations are available for a tablet user to select in settings.

See the default translations.json file here and find the other default translation files here.

\ No newline at end of file diff --git a/getting-started/index.html b/getting-started/index.html new file mode 100644 index 0000000000..dc777e8d1d --- /dev/null +++ b/getting-started/index.html @@ -0,0 +1 @@ + Getting Started - Tangerine Documentation

Getting Started

The starting point for Tangerine is dependent on what your role with the product will be. Are you a Project Manager looking to deploy Tangerine to help you manage data collection in your next project? Are you a Systems Administrator working on the back-end to make sure that Tangerine is operational on your company's technology? Or are you a forms developer, taking the questions for the survey and creating the digital forms in Tangerine, or are you a in the field data collector actually running the surveys? Depending on which of these roles fits will define where you should start. If you fall into multiple roles, try to follow this documentation path System Administrator -> Project Manager -> Forms Editor -> Data Collector. Use the tables below to determine your starting point.

Since Tangerine is an open-source platform, you are welcome to develop off of our code if you wish. Go to the Tangerine Community to see the code or navigate to our Developer Guide.

Moodle Courses

Try one of our moodle course available at Moodle courses

Roles

Role Function Skill Set User Guide
System Administrator Install and implement the technical side of Tangerine. AWS, SSL, SSH, running script in Terminal, Server/DB Management System Administrator Guide
Project Manager Manages the technical aspects of the project. What and how data should be collected, defining how the project works within the Tangerine platform, and deploying the project in the field. Project Management, Data Collection Methodology
Forms Editor Converts surveys into a digitial form using the Tangerine Form Editor and/or Javascript. Web Form Editor, Javascript (For Advanced Functions only) Form Editor Guide
Data Collector Field Level Data collector, administers surveys, directly handles devices loaded with Tangerine Survey and Data Collection Data Collector Guide

Overview of How Tangerine Works

Case to Group to Form - How they interact with each other

Flow Chart of Interactions

\ No newline at end of file diff --git a/guides/index.html b/guides/index.html new file mode 100644 index 0000000000..cfda0bd86a --- /dev/null +++ b/guides/index.html @@ -0,0 +1 @@ + Guides - Tangerine Documentation
\ No newline at end of file diff --git a/how-it-works.png b/how-it-works.png new file mode 100644 index 0000000000..582f7bcbcf Binary files /dev/null and b/how-it-works.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000000..d3a3ba2644 --- /dev/null +++ b/index.html @@ -0,0 +1 @@ + Home - Tangerine Documentation

What is Tangerine?

Tangerine is an open source software that helps with mobile data collection in low resource environments. Tangerine helps with skills measurement, continous and summative assessments, classroom observations, and health surveys and studies. Tangerine is flexible and customizable, allowing you to adapt the product to your specific project needs. The documentation in this site should guide you in making Tangerine work for your needs.

Find the source for this in the Tangerine docs folder on Github.

Moodle Courses

Try one of our moodle course available at Moodle courses

\ No newline at end of file diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000..0ae4777296 Binary files /dev/null and b/logo.png differ diff --git a/navigation/index.html b/navigation/index.html new file mode 100644 index 0000000000..1633d0afb0 --- /dev/null +++ b/navigation/index.html @@ -0,0 +1,20 @@ + Other Resources - Tangerine Documentation
\ No newline at end of file diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 0000000000..5133e113a6 --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"What is Tangerine?","text":"

Tangerine is an open source software that helps with mobile data collection in low resource environments. Tangerine helps with skills measurement, continous and summative assessments, classroom observations, and health surveys and studies. Tangerine is flexible and customizable, allowing you to adapt the product to your specific project needs. The documentation in this site should guide you in making Tangerine work for your needs.

Find the source for this in the Tangerine docs folder on Github.

"},{"location":"#moodle-courses","title":"Moodle Courses","text":"

Try one of our moodle course available at Moodle courses

"},{"location":"CONTRIBUTING/","title":"How to Contribute Documentation","text":"

Documentation in Tangerine is managed using the same process as all code contributions. In short, all changes should be completed within a feature-branch or fork of Tangerine and submitted as a pull request to the \"next\" branch.

"},{"location":"CONTRIBUTING/#documentation-overview","title":"Documentation Overview","text":"

Tangerine documentation is written using Markdown as the standard source. Documentation is compiled using MkDocs and is available within GitHub Pages. Links are as follows:

  • GitHub Pages: https://docs.tangerinecentral.org/
"},{"location":"CONTRIBUTING/#documentation-standards","title":"Documentation Standards","text":"

All documentation must be created and published using Markdown (.md) files and must reside in the docs/ folder or a subdirectory of the docs folder.

"},{"location":"CONTRIBUTING/#adding-your-document-to-the-navigation","title":"Adding your Document to the Navigation","text":"

Please follow the instructions on the MkDocs Documentation for adding pages to the navigation. The mkdocs.yml file can be found at the root level of the Tangerine repository.

...\nnav:\n    - Home: index.md\n    - About: about.md\n...\n
"},{"location":"CONTRIBUTING/#setting-up-your-environment-for-local-documentation-development","title":"Setting up your Environment for Local Documentation Development","text":"

Since Tangerine documentation is written in Markdown it's not necessary to have a full local development environment setup to add or modify documentation. That said, if you're making significant changes you may desire to have the ability to build the documentation locally. If you are on Mac OS, you will first need to install python 3. This tutorial worked great for RJ. Make sure to follow the \"What to do\" section, not the others. Then in the top level tangerine directory, run the following commands to install dependencies. If any of the commands fail, try running the failed command again (that worked for R.J.).

pip install mkdocs\npip install mkdocs-material\npip install mkdocs-git-revision-date-localized-plugin\npip install mkdocs-awesome-pages-plugin\npip install mkdocs-minify-plugin\n

Now you have everything installed, get started viewing content by running the following in the tangerine root directory (not the tangerine/docs/ directory!)...

mkdocs serve\n
"},{"location":"CONTRIBUTING/#contribution-guide","title":"Contribution Guide","text":"

TODO: Replace this video with an updated version to reflect the new process

"},{"location":"about/","title":"About Tangerine","text":"

Tangerine is electronic data collection software designed for use on Android mobile devices. Its primary use is to enable offline data capture in low-resource areas.

Tangerine was first developed to capture student responses in in oral early grade reading and mathematics skills assessments, specifically Early Grade Reading Assessment (EGRA) and Early Grade Mathematics Assessment (EGMA). As well as capture interview responses from students, teachers and principals on home and school context information. Tangerine's capabilities have been expanded for data capture and management for rural health intervention projects.

Using Tangerine improves data quality and the efficiency of data collection and analysis by simplifying the preparation and implementation of field work, reducing measurement and data entry errors, and eliminating manual data entry from paper forms.

Tangerine was developed in 2011 by RTI International with its own internal research funds, and made available to the public through a GNU General Public License. RTI redesigned Tangerine and developed a new codebase using latest technologies in 2018 with funding support from Google.org. As an open source software platform Tangerine's source code is available for anyone who wishes to install and use Tangerine on their own web server. Tangerine's source code and related documentation is available on Github, a commonly used repository for open source software. To learn more and have a look under the hood, check out Tangerine's Code Repositories on Github.

"},{"location":"about/#how-it-works","title":"How it works","text":""},{"location":"getting-started/","title":"Getting Started","text":"

The starting point for Tangerine is dependent on what your role with the product will be. Are you a Project Manager looking to deploy Tangerine to help you manage data collection in your next project? Are you a Systems Administrator working on the back-end to make sure that Tangerine is operational on your company's technology? Or are you a forms developer, taking the questions for the survey and creating the digital forms in Tangerine, or are you a in the field data collector actually running the surveys? Depending on which of these roles fits will define where you should start. If you fall into multiple roles, try to follow this documentation path System Administrator -> Project Manager -> Forms Editor -> Data Collector. Use the tables below to determine your starting point.

Since Tangerine is an open-source platform, you are welcome to develop off of our code if you wish. Go to the Tangerine Community to see the code or navigate to our Developer Guide.

"},{"location":"getting-started/#moodle-courses","title":"Moodle Courses","text":"

Try one of our moodle course available at Moodle courses

"},{"location":"getting-started/#roles","title":"Roles","text":"Role Function Skill Set User Guide System Administrator Install and implement the technical side of Tangerine. AWS, SSL, SSH, running script in Terminal, Server/DB Management System Administrator Guide Project Manager Manages the technical aspects of the project. What and how data should be collected, defining how the project works within the Tangerine platform, and deploying the project in the field. Project Management, Data Collection Methodology Forms Editor Converts surveys into a digitial form using the Tangerine Form Editor and/or Javascript. Web Form Editor, Javascript (For Advanced Functions only) Form Editor Guide Data Collector Field Level Data collector, administers surveys, directly handles devices loaded with Tangerine Survey and Data Collection Data Collector Guide"},{"location":"getting-started/#overview-of-how-tangerine-works","title":"Overview of How Tangerine Works","text":""},{"location":"getting-started/#case-to-group-to-form-how-they-interact-with-each-other","title":"Case to Group to Form - How they interact with each other","text":""},{"location":"getting-started/#flow-chart-of-interactions","title":"Flow Chart of Interactions","text":""},{"location":"guides/","title":"Guides","text":"

Welcome to the Tangerine Documentation site. Find the source for this in the Tangerine docs folder on Github.

  • Editor Guide
  • Data Collector Guide
  • System Administrator Guide
  • Developer Guide
"},{"location":"guides/#moodle-courses","title":"Moodle Courses","text":"

Try one of our moodle course available at Moodle courses

"},{"location":"navigation/","title":"Other Resources","text":"
  • How to contribute documentation
  • Issue Queue
  • Code
  • Releases
  • Technical Chat
  • Community Forum
  • Tangerine Central
"},{"location":"whats-new/","title":"What's new","text":""},{"location":"whats-new/#v3311","title":"v3.31.1","text":"

General Updates

  • Allow mysql outputs of TANGY-TIMED and TANGY-UNTIMED-GRID data

Administration

  • The reporting-cache-clear script will honor the environmnt variable T_ONLY_PROCESS_THESE_GROUPS to limit the groups processed
  • Set T_ONLY_PROCESS_THESE_GROUPS to a comma-separated list of group names to limit the groups cleared and then processed by the script

Fixes - Fixes for editing of Form Responses in the server web UI * Edits of Attendence, Behavior, and Scoring are currently prohibited in the server web UI * Verified and Archived Form Responses must be Unverified and Unarchived before editing is available - Teacher Dashboard Scoring: Fix issues with custom scoring - Fix output of Case Participants to mysql - Fix online survey release

Libs and Dependencies - Bump version of tangy-form to v4.54.4 * Fix check for 'readOnly' input metadata * Fix undefined access of input without tagName * Fix missing function parens

Server upgrade instructions

See the Server Upgrade Insturctions.

Special Instructions for this release: N/A

"},{"location":"whats-new/#v3310","title":"v3.31.0","text":"

New Features

  • Audio and Visual Feedback: A new Prompt Box widget available in form authoring allows a form designer to add audio and visual feedback connected to Radio Block widgets. This feature provides a toolset for creating self-guided assessments. See the example in tangy-forms. #3473

  • Client Login Screen Custom HTML: A new app-config.json setting, customLoginMarkup, allows for custom HTML to be added to the login screen. This feature is useful for adding custom branding or additional information to the login screen. As an example:

    \"customLoginMarkup\": \"<div style='text-align: center;'><img src='assets/media/logo.png' alt='logo' style='max-width: 100%;'></div>\"\n

  • Improved Data Management:

  • Data Managers now have access to a full workflow to review, edit, and verify data in the Tangerine web server. The Data Manager can click on a record and enter a new screen that allows them to perform actions align with a data collection supervision process.
  • Searching has been improved to allow seaqrching for a specific ID in the imported data. This feature is useful for finding specific records synced to the server when reviewing or editing completed form responses. #3681

Fixes - Client Search Service: exclude archived cases from recent activity - Media library cannot upload photos #3583 - User Profile Import: The process of importing an existing device user now allows for retries and an asynchronous process to download existing records. This fixes an issue cause by timeouts when trying to import a user with a large number of records. #3696 - When T_ONLY_PROCESS_THESE_GROUPS has a list of one or more groups, running reporting-cache-clear will only process the groups in the list

Tangerine Teach

  • Add toggle in Attendence Check for 'late'. A teacher can click through the status of 'present', 'late', or 'absent' for each student.
  • Use studentRegistrationFields to control showing name and surname of student in the student dashboard

Libs and Dependencies - Bump version of tangy-form to v4.31.1 and tangy-form-editor to v7.18.0 for the new Prompt Box widget - Bump version of tangy-form to v4.45.1 for disabling of tangy-gps in server edits

Server upgrade instructions

See the Server Upgrade Insturctions.

Special Instructions for this release:

Once the Tangerine and CouchDB are running, run the upgrade script for v3.31.0:

docker exec -it tangerine /tangerine/server/src/upgrade/v3.31.0.js

"},{"location":"whats-new/#v3302","title":"v3.30.2","text":"

New Features

  • Customizable 'About' Page on client #3677 -- Form developers can create or update a form with the id 'about'. There is an example form in the Content Sets -- The form will appear in the 'About' page on the client

General Updates - Password Visibility -- the login and register screen on the client shows an 'eye' icon used to hide or show passwords - Re-organization of the client app menu - Reintroduce registrationRequiresServerUser app config setting to make managing central user more flexible - use registrationRequiresServerUser to require an import code when registering users on the client - use centrallyManagedUserProfile to require an import code AND only allow changes to the user profile on the server - use hideProfile to hide the manage user profile page from on the client

Teach Module Updates - Behavior screen show a link instead of a checkbox to access the Behavior form - Hint text added to attendance, behavior, and scoring tables - Improved save messaging for attendance and scoring - In Attendance Reports: - add start and end dates to view a custom date range report - Fix the names not displaying in the tables

Fixes - Get Media Uploads working in Editor #3583 - CSV Generation broken with 'doLocalWorkaround is undefined' error

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. \ndf -h\n# If there is not more than 12 GB plus the size of the data folder, create more space before proceeding. \n# Good candidates to remove are: data back-up folders and older versions of the Tangerine image\n# rm -rf ../data-backup-<date>\n# docker rmi tangerine/tangerine:<version>\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout -b v3.30.1 v3.30.1\n./start.sh v3.30.2\n# Run the update to copy the new About page to all groups on your site.\ndocker exec -it tangerine /tangerine/server/src/upgrade/v3.30.2.js\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:<previous_version>\n
"},{"location":"whats-new/#v3301","title":"v3.30.1","text":"

New Features - Multiple Location Lists can be configured using the Tangerine server web interface -- Create and manage location lists for use in Tangerine forms -- The default location list is used for device and device user assignment. - The app-config.json teachProperties has new properties, \"unitDates\" and \"studentRegistrationFields\":

\"unitDates\": [{\"name\": \"Unidad 1\",\"start\": \"2023-02-15\", \"end\": \"2023-04-23\"}, {\"name\": \"Unidad 2\",\"start\": \"2023-04-24\", \"end\": \"2023-06-30\"}], \n\"studentRegistrationFields\": [\"student_name\", \"student_surname\", \"phone\", \"classId\"]\n
The unitDates property is used to configure the dates for each unit in the Class module. The studentRegistrationFields property is used to configure the fields from the Student Registration form to be saved in the class attendance, behavior, and score register and CSV's. - The app-config.json teachProperties has a new property, \"showAttendanceCalendar\", which enables the Attendance Calendar in the Class module when set to true. - Intl/locale support in Class: The class module currently supports the es-gt locale. Add additional locales in class/module.ts:
import { registerLocaleData } from '@angular/common';\nimport localeEsGt from '@angular/common/locales/es-GT';\nregisterLocaleData(localeEsGt);\n
- The \"Request spreadsheets\" CSV output form now has three new forms to view if useAttendanceFeature is set to true in app-config.json: Attendance, Behavior, and Score

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. \ndf -h\n# If there is not more than 12 GB plus the size of the data folder, create more space before proceeding. \n# Good candidates to remove are: data back-up folders and older versions of the Tangerine image\n# rm -rf ../data-backup-<date>\n# docker rmi tangerine/tangerine:<version>\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout -b v3.30.1 v3.30.1\n./start.sh v3.30.1\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:<previous_version>\n
"},{"location":"whats-new/#v3300","title":"v3.30.0","text":"

New Features

  • The 'teach' content-set now supports an optional 'Attendance' feature, enabled by adding \"useAttendanceFeature\": true and \"homeUrl\": \"attendance-dashboard\" to app-config.json. It also has a new Class/Attendance menu which enables collection of those values per student, and an 'Attendance' report.
  • The Attendance records generate _id's based on the grade, curriculum, user, and date and time of the record, so that they can be sorted chronologically. See dashboard.service generateSearchableId for details.
  • Class now supports eventFormRedirect to redirect to different url after submit: on-submit=\"window.eventFormRedirect =/attendance-check\"
  • New app-config.json configuration for teach properties: ```js \"teachProperties\": { \"units\": [\"Unidad 1\", \"Unidad 2\"], \"attendancePrimaryThreshold\": 80, \"attendanceSecondaryThreshold\": 70, \"scoringPrimaryThreshold\": 70, \"scoringSecondaryThreshold\": 60, \"behaviorPrimaryThreshold\": 90, \"behaviorSecondaryThreshold\": 80, \"useAttendanceFeature\": true } The PrimaryThreshold and SecondaryThreshold values are used to determine the color of the cell in the reports.

  • Updated docker-tangerine-base-image to v3.8.0, which adds the cordova-plugin-x-socialsharing plugin and enables sharing to WhatsApp.

Fixes - Fixed PWA assets (sound,video) only work when online #1905

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade. \ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout -b v3.30.0 v3.30.0\n./start.sh v3.30.0\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.29.1\n
"},{"location":"whats-new/#v3291","title":"v3.29.1","text":"

Fixes

  • Fix undefined referencein markQualifyingEventsAsComplete

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. \ndf -h\n# If there is not more than 12 GB plus the size of the data folder, create more space before proceeding. \n# Good candidates to remove are: data back-up folders and older versions of the Tangerine image\n# rm -rf ../data-backup-<date>\n# docker rmi tangerine/tangerine:<version>\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout -b v3.29.1 v3.29.1\n./start.sh v3.29.1\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:<previous_version>\n
"},{"location":"whats-new/#v3290","title":"v3.29.0","text":"

New Features - Case, Event and Form Archive and Unarchive

We have released an update to Tangerine which allows for the archiving and un-archiving of both events, and forms within events. This is an extension of the already existing functionality by which an entire case can be archived. The purpose of this is to empower data management teams using Tangerine to \"clean up\" messy cases where extraneous data has been added to a case in error, or by a conflict situation. The purpose of this document is to summarize both the configuration to enable this, and to demonstrate the use of these functions. This functionality will only apply to the web-based version of Tangerine, and will not be available on tablets.

Package Updates - Updated tangy-form to v4.40.0

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. \ndf -h\n# If there is not more than 12 GB plus the size of the data folder, create more space before proceeding. \n# Good candidates to remove are: data back-up folders and older versions of the Tangerine image\n# rm -rf ../data-backup-<date>\n# docker rmi tangerine/tangerine:<version>\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout -b v3.29.0 v3.29.0\n./start.sh v3.29.0\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:<previous_version>\n
"},{"location":"whats-new/#v328","title":"v3.28","text":"
  • This became v4
"},{"location":"whats-new/#v3278","title":"v3.27.8","text":"

New Features - New server configuration setting for output value of optionally not answered questions - The value set in the config variable T_REPORTING_MARK_OPTIONAL_NO_ANSWER_WITH in config.sh will be the value of questions that are optional and not answered by the respondent. - The default value is \"SKIPPED\" for consistency with previous outputs - CSV outputs now include the metadata variables startDateTime and endDateTime auto-calculated from the startUnixTime and endUnixTime variables - Additional parameter for the csv data set generation process to ignore user-profile and reports from the output csv files

Fixes - Copy all media directories from the client form directories to ensure assets are available in online surveys - Allows form developers to publish images and sounds in online surveys - Fix the language dropdown in online surveys - Outputs will no longer try to process outputs for TANGY-TEMPLTE inputs

Breaking Changes - Removes build dependencies for legacy python mysql output module - For those using the legacy module, see the documentation move to the new mysql-js module

Package Updates - Lock @ts-stack/markdown to 1.4.0 to prevent breaking of builds

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade. \ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout -b v3.27.8 v3.27.8\n./start.sh v3.27.8\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.27.7\n
"},{"location":"whats-new/#v3277","title":"v3.27.7","text":"

Fixes - Enable mysql-js module outputs for online-survey app data

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout -b v3.27.7 v3.27.7\n./start.sh v3.27.7\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.27.6\n
"},{"location":"whats-new/#v3276","title":"v3.27.6","text":"

Fixes - Address issues using the CaseService createCaseEvent API in on-submit logic by making the function synchronous

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.27.6\n./start.sh v3.27.6\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.27.5\n
"},{"location":"whats-new/#v3275","title":"v3.27.5","text":"

Fixes - CSV Generation: Fix permissions on generate csv batch script

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.27.5\n./start.sh v3.27.5\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.27.4\n
"},{"location":"whats-new/#v3274","title":"v3.27.4","text":"

Fixes - Synchronization: Update Reduce Batch Size button to apply during normal sync for pull and push

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.27.4\n./start.sh v3.27.4\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.27.3\n
"},{"location":"whats-new/#v3273","title":"v3.27.3","text":"

Fixes - Fix running the reporting-cache-clear command on the mysql-js module - Extend the particpantID key to 80 chars to handle long keys for T_MYSQL_MULTI_PARTICIPANT_SCHEMA - For those using mysql-js: This change requires running reporting-cache-clear to take effect. - Fix missing groupId in user-profile PR: #3494 - This bugfix added groupId to the user-profile. - In mysql-js, it also throws an error when groupId is missing. Relevant commit. This is different from earlier behavior, which lets the document pass without an error. All docs should have a groupId.

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.27.2\n./start.sh v3.27.2\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.27.2\n
"},{"location":"whats-new/#v3272","title":"v3.27.2","text":"

Fixes - Tangerine on Android APK ignore requestFullscreen() #3539 - This fix above also adds a new app-config.json property - exitClicks - enables admin to set number of clicks to exit kioskMode. - Fixed: Tangy-radio button and tangy keyboard do not render on Online survey #3551

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.27.2\n./start.sh v3.27.2\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.27.1\n
"},{"location":"whats-new/#v3271","title":"v3.27.1","text":"

Fixes - Limit debugging logs in csv generation to prevent exec from hitting max_buffer issue - Add protection to use of onEventOpen and onEventClose API

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.27.1\n./start.sh v3.27.1\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.27.0\n
"},{"location":"whats-new/#v3270","title":"v3.27.0","text":"

NEW Features - Update triggers for Case API hooks

The Case Service API has a set of functions that are triggered on events. An implementer of Tangerine using the Case module can add these triggers to their case definition json in order to hook into these actions. The variable name to add is the trigger, e.g. onCaseOpen and the value is valid javascript that will run when the trigger is fired. The following changes were made to the hooks:

-- onCaseClose hook will fire when the case is closed by the user by clicking the 'X' in the upper-left corner -- onCaseOpen hook will fire when the case is opened (no change) -- onEventOpen has been changed to fire when a Case Event is clicked on the Case Summary page -- onEventClose has been changed to fire when the Event page is closed -- onEventCreate has been added and will fire when the user creates a new event using the dropdown in the Case Summary page -- onEventFormOpen has been added and will fire when the user opens a form from the Event page -- onEventFormClose has been added and will fire when the user closes a form

Fixes - Check for custom search js when creating user dbs (Tangerine Preview)

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.27.0\n./start.sh v3.27.0\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.26.2\n
"},{"location":"whats-new/#v3262","title":"v3.26.2","text":"

DEPRECATION NOTICE AND UPCOMING MODULE DELETION

The mysql module is deprecated; it will be removed soon from this source code in the v3.28.0 release. We have been using the mysql-js in production for a few months and it is more performant and reliable than the output of the mysql module. We recommend switching to the mysql-js module. See the MySQL-JS Module doc for upgrade and configuration information.

New Features - New group content-set dropdown: PR 3275 - https://github.com/Tangerine-Community/Tangerine/pull/3275 - enables a content-set dropdown and is already in main. Modify the template (content-sets-example.json) and rename to content-sets.json to enable the dropdown.

Fixes - Created Feedback dialog to resolve layout issue on mobile devices PR: #3533 - Fix for Class listing breaks if you archive all classes in Teach; unable to add new classes. Issue: #3491 - Fix for Mysql tables not populating; ER_TOO_BIG_ROWSIZE error in Tangerine logs. Issue: #3488 - Changed location of mysql-js config file to point to the mysql-js directory. Also increased memory parameters in conf.d/config-file.cnf. - If you are using the mysql container and are having errors with very large forms, the new settings in ./server/src/mysql-js/conf.d/config-file.js should help. You will need to completely rebuild the mysql database. See the \"Resetting MySQL databases\" section in the MySQL-JS Module docs. - Important: If you already have a mysql instance running and don't want to rebuild the mysql database, delete the innodb-page-size=64K line from ./server/src/mysql-js/conf.d/config-file.js; otherwise, your mysql instance will not start. - Fix for CSV Download fails with larger forms. Issue: #3483

Backports

The following feature was backported from v3.24.6 patch release:

  • T_UPLOAD_WITHOUT_UPDATING_REV : A new config.sh setting for use in high-load instances using sync-protocol-1. *** Using this setting COULD CAUSE DATA LOSS. *** This setting uses a different function to process uploads that does not do a GET before the PUT in order to upload a document. Please note that if there is a conflict it try to POST the doc which will create a new id and copy the _id to originalId. If that fails, it will log the error and not upload the document to the server, but still send an 'OK' status to client. The failure would result in data loss.

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.26.2\n./start.sh v3.26.2\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.26.1\n
"},{"location":"whats-new/#v3261","title":"v3.26.1","text":"

NEW Features - New configuration parameter: T_LIMIT_NUMBER_OF_CHANGES - Number of change docs from the Couchdb changes feed queried by reporting-worker (i.e. use as the limit parameter). Default: 200. - Added volume mapping for translations dir in start script. - A new mysql-js module replaces the old mysql module. Documentation is here. The new mysql-js module is faster and more accurate than the old mysql module. It no longer uses an intermediate \"group-uuid-mysql\" couchdb; instead, it reads from the _changes feed and writes directly to a MySql database. To use the new module, add mysql-js to the T_MODULES list of modules and configure the following settings: - T_MYSQL_CONTAINER_NAME=\"mysql\" # Either the name of the mysql Docker container or the hostname of a mysql server or AWS RDS Mysql instance. - T_MYSQL_USER=\"admin\" # Username for mysql credentials - T_MYSQL_PASSWORD=\"password\" # Password for mysql credentials - T_USE_MYSQL_CONTAINER=\"true\" # If using a Docker container, set to true. This will automatically start a mysql container when using a Tangerine launch script.

Fixes - Student subtest report incorrect for custom logic inputs #3464 - Init paid-worker file when server restarted. - Fix bug in start.sh script for --link option - Rename T_REBUILD_MYSQL_DBS to T_ONLY_PROCESS_THESE_GROUPS. Configure T_REBUILD_MYSQL_DBS to list group databases to be skipped when processing data through modules such as mysql and csv.

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.26.1\n./start.sh v3.26.1\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.26.0\n
"},{"location":"whats-new/#v3260","title":"v3.26.0","text":"

NEW Features

  • MySQL JS Module: -- Track and output changes through the CouchDB Changes Feed -- Connect to a MySQL Server of your choice via a url and credentials
  • Add app-config flag to force confirmation of each form response created on the client
  • Update to tangy-form and tangy-form-editor which enables configuration of automatic scoring in Editor for groups using Class. Issue: #1021
  • Documented a list of Reserved words in Tangerine
  • Bump docker-tangerine-base-image to v3.7.4 (enables RECORD_AUDIO permission for APK's), tangy-form to 4.38.3, tangy-form-editor to 7.15.4.

Fixes

  • Add protection when using Case APIs that load other cases than the currently active case
  • feat(custom-scoring): If customScore exists, use it [#3450](https://github.com/Tangerine-Community/Tangerine/pull/3450
  • fix(record-audio): Request audio permissions #3451

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.26.0\n./start.sh v3.26.0\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.26.0\n
"},{"location":"whats-new/#v3251","title":"v3.25.1","text":"

Fixes

  • Fix logic in has merge change permissions

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.25.1\n./start.sh v3.25.1\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.25.1\n
"},{"location":"whats-new/#v3250","title":"v3.25.0","text":"

NEW Features

  • Improvements to Issues on the Client and Server 3413 -- Add app-config flag to allow client users to Commit changes to Issues -- Add user-role permissions to select which events or forms Issue changes can be commited on the client -- Pull form responses changed in Issues on the server down to the client
  • Add parameter to CSV Dataset Generation that allows exclusion of archived form definitions

Fixes

  • Apply isIssueContext correctly on the client

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.25.0\n./start.sh v3.25.0\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.25.0\n
"},{"location":"whats-new/#v3246","title":"v3.24.6","text":"

NEW Features

  • T_UPLOAD_WITHOUT_UPDATING_REV : A new config.sh setting for use in high-load instances using sync-protocol-1. *** Using this setting COULD CAUSE DATA LOSS. *** This setting uses a different function to process uploads that does not do a GET before the PUT in order to upload a document. Please note that if there is a conflict it will copy the _id to originalId and POST the doc, which will create a new id. If that fails, it will log the error and not upload the document to the server, but still send an 'OK' status to client. The failure would result in data loss.
"},{"location":"whats-new/#v3244","title":"v3.24.4","text":"

NEW Features

  • Ability to add scoring from the interface (24 hours) #1021

Fixes

  • User is forced to stay on form until submission [#3215] - changed current-form-id to incomplete-response-id
  • Bumped tangy-form to 4.37.0 and tangy-form-editor to 7.14.11.

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.24.4\n./start.sh v3.24.4\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.24.3-final\n
"},{"location":"whats-new/#v3243-final","title":"v3.24.3-final","text":"

Please note that this release is tagged v3.24.3-final, not v3.24.3. This is a deviation from our usual format; we will resume the previous format in the next release.

New Feature - To force user to stay on form until submission, set `\"forceCompleteForms\":true' in the group app-config.json. Issue: #3215

Fixes - Separate Archived and Active forms in Request Spreadsheet screen #3222 - When incomplete results upload is enabled on a group, do not save empty record when using sync-protocol 1. #3360 - Enable editing the \"No\" confirmation alert for tangy-consent #3025 - Fix Sync error caused by async directory error when creating media directories #3374 - Exclude client-uploads folder from APK and PWA releases #3371 - Remove ordering of inputs when creating spreadsheets #3252 - Bumped tangy-form-editor to v7.14.8 to add Video Capture input warning text #3376 - Bumped tangy-form to 4.36.3. - Updated the online-survey-app routing to route to a specific form, and also adds an optional routing option. PR:#3387

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.24.3-final\n./start.sh v3.24.3-final\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.24.2\n
"},{"location":"whats-new/#v3242","title":"v3.24.2","text":"

Fixes - Hide Case Events form the Schedule View that are 'inactive' - Add the 'endUnixTimestamp' to mysql outputs generated by the python module - Remove unnecessary and expensive query for conflicts during synchronization on the client PR: #3365

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.24.2\n./start.sh v3.24.2\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.24.1\n
"},{"location":"whats-new/#v3241","title":"v3.24.1","text":"

New Features

  • Feature: New version of mysql module, called mysql-js, which is coded in javascript instead of python. This module exports records much faster than previous version. It should also use much less memory and provide more flexibility in terms of column data types and (eventually) support of different types of databases. Issue: #3047
  • Feature: Enable upload of files created by the tangy-photo-capture and tangy-video-capture inputs. PR: #3354 Note: In order to cause minimal negative impact upon current projects, the default behavior will be to save image files created by the tangy-photo-capture input to the database, instead of saving to a file and uploading. That being said, it is preferable to save as a file and upload. To over-ride this default, set the new mediaFileStorageLocation property to 'file' in the group's app-config.json. The default is 'database'. If this property is not defined, it will save to the database. New groups will be created with mediaFileStorageLocation set to 'file'. Videos created using the tangy-video-capture input will always be uploaded to the server due to their large file size.

Fixes - The default password policy (T_PASSWORD_POLICY in config.sh) has been improved to support most special characters and the T_PASSWORD_RECIPE description has been updated to list the permitted special characters. Issue: https://github.com/Tangerine-Community/Tangerine/issues/3299

Example:

(\\` ~ ! @ # $ % ^ & * ( ) \\ - _ = + < > , . ; : \\ | [ ] { } )\n
  • Enable forms without location to be viewed in visits listing. PR: #3347
  • Fix results with cycle sequences that do not generate a CSV file. Issue: #3249 PR: 3345
  • Enable grids to be hidden based on skip logic #1391
  • Add confirmation to consent form if 'No' selected before the form is closed #3025. Activate this feature using the new property: confirm-no=\"true\".
  • Fix app config doNotOptimize logic PR: #3358
  • Those using the doNotOptimize flag must reverse the logic in the appConfig.sh file when updating to this version

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.24.1\n./start.sh v3.24.1\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.24.0\n
"},{"location":"whats-new/#v3240","title":"v3.24.0","text":"

New Features

  • App user can reduce the batch size as a workaround when experiencing 'out-of-memory' errors with Rewind Sync
  • On occasion Rewind Sync will fail to complete due to 'out-of-memory' issues. The Advanced Sync section of the Sync page now shows a checkbox that when checked will reduce the batch sizes used to perform the Rewind Sync. As noted in the UI, the Rewind Sync will take longer to process however in most cases it will be able to complete the process. The batch size reduction will be reverted once the Rewind Sync is complete or the user unchecks the box.
  • System Admin can scan a QR code to download an APK or PWA release
  • When deploying Tangerine, it can be a long process to download and install the APK or PWA on multiple devices. To improve the deployment process, the Release tables now show a QR code that when scanned will download the release directly to a new device without the need to type in the URL.
  • Improvements to Sync: In case of false positives on push, keep pushing until nothing is pushed
  • Client Case Service API:
  • Case Event and Event Form (De)activation 3334
    • activateCaseEvent: Marks a Case Event as 'active' and shows it in the Case Event list
    • deactivateCaseEvent: Marks a Case Event as 'inactive' and hides it in the Case Event list
    • activateEventForm: Marks an Event Form as 'active' and shows it in the Event Form list
    • deactivateEventForm: Marks an Event Form as 'inactive' and hides it in the Event Form list
  • Editor Case Service API:
  • Add useful APIs used in the client Case Service API to the editor Case Service API 3325
  • Option to sync a case before viewing it 3237
  • Support for showing photo and signatures in Issues

Fixes

  • Fix after update messaging and async issues

Translations - Include Vietnamese translations

Deprecations - Comparison Sync has been removed from this release to reduce confusion reported by Tangerine users. The Rewind Sync functionality out-performs Comparison Sync and is recommended for use when needed on all deployments.

"},{"location":"whats-new/#v3231","title":"v3.23.1","text":"

Fixes

  • Fixed bug in sync on PWA's. Also, do note that video file upload using the new tangy-form input <tangy-video-capture> only works for APKs Issue: [#3338] https://github.com/Tangerine-Community/Tangerine/issues/3338
  • Fixed tangy-form-editor to 7.14.2 to fix bug with input widget for tangy-keyboard-input (postfix field).

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.23.1\n./start.sh v3.23.1\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.23.0\n
"},{"location":"whats-new/#v3230","title":"v3.23.0","text":"

New Features

  • Enabled video upload feature which uses the new tangy-form input <tangy-video-capture>. There is a new video file upload section in the sync feature for both sync-protocol 1 and 2. implementation details in this PR: 3327. Issue: #3212 The new tangy-form input <tangy-video-capture> takes the following properties:
  • frontCamera: Boolean. Whether to use the front camera or the back camera. Default is true.
  • noVideoConstraints: Boolean. Whether to force use of front or back camera. If true, chooses the first available source. Default is true.
  • codec: String. The codec to use. Default is 'video/webm;codecs=vp9,opus' - AKA webm vp9. It is possible the device may not support all of these codecs. Other potential codecs include video/webm;codecs=vp8,opus and video/webm;codecs=h264,opus.
  • videoWidth: Number. The width of the video. Default is 1280 and videoHeight: Number. The height of the video. Default is 720.
  • Bump tangy-form lib to 4.34.3, tangy-form-editor to 7.14.1.

Fixes

  • Add postfix property to tangy-keyboard-input. Also add highlight to value entered. Issue: 3321
"},{"location":"whats-new/#v3224","title":"v3.22.4","text":"

New Features

  • Feature: Tangerine CLI for dropping mysql tables and resetting mysql .ini files. PR: #3281 Usage: docker exec tangerine module-cache-clear mysql

Fixes

  • Error when mysql module creates a table with duplicate participantId PR: #3279
  • New languages - Bengali, Dari, Hindi, Pashto, Portuguese, Updated Russian, Swahili, Urdu #3263
  • Filter archived case events out of Schedule View #3267
  • Many fixes to Teach:
  • Add Current Date to Teach Subtest Report #3273
  • Remove some appended Teach CSV columns #3271
  • Fix student subtest report failing by transforming data only for related curriculum #3272
  • Student subtask report is failing with error #3270
  • CSV file contains tangy-input metadata and displaces all inputs #3227
  • Fix summary upload #3265
  • Records should be one doc per Student per Curriculum per Class. Not per Student per Curriculum per Class per Item. #3264
  • Provide Bengali number translation in Student Grouping Report #3255
  • Bengali numbers are not being replaced in Class Grouping report #3228

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.22.4\n./start.sh v3.22.4\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.22.3\n
"},{"location":"whats-new/#v3223","title":"v3.22.3","text":"

Fixes

  • Fix all Tangy Templates are missing when reviewing completed form responses.

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.22.3\n./start.sh v3.22.3\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.22.2\n
"},{"location":"whats-new/#v3222","title":"v3.22.2","text":"

Fixes

  • Download All button on Spreadsheet Request info page does not download #3232
  • Spreadsheet Requests page does not load for new groups #3233
  • Fix use of window.eventFormRedirect #3211
  • Spreadsheet Request will fail to generate Download All zip if one form has specific characters in the title #3217

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.22.2\n./start.sh v3.22.2\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.22.1\n
"},{"location":"whats-new/#v3221","title":"v3.22.1","text":"

Fixes

  • Fix: Tangy Template elements all say \"false\" if using environment variables like caseService and T #3203
  • Make issue diffs less crash prone #3200
  • Fix: Case fails to open after selecting Case in search behind a \"load more\" button #3194
  • Fix: Unable to scroll to last item in search list if there is not more button #3195
  • Fix: After typing a search, \"load more\" button appears with no search results for a few seconds #3196
  • On a Spreadsheet Request page, style the download all button's icon as white.
  • Unify and fix the exclude pii label on spreadsheet requests.

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server. \ndocker logs --since=60m tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.22.1\n./start.sh v3.22.1\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.22.0\n
"},{"location":"whats-new/#v3220","title":"v3.22.0","text":"

Fixes

  • Device User should not be able to register Device Account without username #3162
  • CSV Datasets are not filtering by month and year when a 'Month' and 'Year' is selected #3181
  • Make Loc and t available on Editor's window object for consistency with Client environment #3161
  • Fix messaging during data optimization and reduce number of view optimized that are never used #3165
  • Prevent menu items from jumping around on Deploy page #3169
  • Fix bug causing document updates to get skipped over in sync after a Comparison Sync #3179

Deprecate single csv download in favor of Spreadsheet Requests

See screenshots here.

  • Change terminology referring to \"CSV\" to more commonly recognized \"Spreadsheet\" term.
  • \"CSV Datasets\" term changed to \"Spreadsheet Requests\".
  • Fix \"CSV Datasets are not filtering by month and year when a 'Month' and 'Year' is selected #3181\"
  • Request Spreadsheets page: Submit button now hovers and is sticky to bottom of page; \"*\" in Month/Year selection clarified as \"All months\"/\"All years\"; \"Description\" no longer required and given own line for better formatting; other formatting cleanup.
  • Data page: Removed deprecated CSV Download button; updated language; added \"Request Spreadsheets\" button for quick access to making a request for spreadsheets.
  • Spreadsheet Request Info page: Now dynamically updates as Spreadsheets are rendered with row counts and status; removed unnecessary filename to download all, instead it's a \"download all\" button; new types of status including \"File removed\", \"Stopped\", \"Available\", and \"In progress\"; Month and Year values of \"*\" now clarified as \"All months\" and \"All years\"; loading screen improvements; title of page now the date the spreadsheets were requested on.
  • Spreadsheet Requests page: Updated language; fixed total Spreadsheet Requests calculation in pagination; if status of Spreadsheet Request is \"Available\" the status shows in green; if the status of the Spreadsheet Request is \"In progress\" a spinner is shown where the Download button will be; labels of \"More Info\" and \"Download\" added to corresponding buttons; loading overlay now shown on initial load and when changing pages.
  • Spreadsheet Templates page: Updated terminology from CSV Templates to Spreadsheet Templates.

New Features

  • Show recent activity as default search results #3171
  • Make a cached version of the Device information available to form logic on T.device.device #3183
  • Group Administrator configures Device Account password policy #3172
  • On search UI: limit initial results to 10 for fast load, add a \"Load More\" button for pagination, and style improvements #3164

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.22.0\n./start.sh v3.22.0\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.21.0\n
"},{"location":"whats-new/#v3210","title":"v3.21.0","text":"

Developers:Good to Know

  • The master branch has been moved to the main branch. No development will happen on the master branch, which has been deleted. Also, please note the updates to the Release Workflow

Fixes

  • Prevent unnecessary CaseService saves by comparing hashes #3155
  • Prevent loss of case changes when leaving incomplete form by always saving the case #3156
  • Prevent on-submit of a form running in one Case from being able to run in another case by navigating quickly to another Case. We inject T and case (caseService) variables into Tangy Form (formPlayer) from EventFormComponent. This will add instanceFrom: 'EventFormComponent' to the caseService (and also assigns ['instanceFrom'] = 'EventComponent' in EventComponent). Note that if you have any use of window.T or window.caseService, you will need to make them T and caseService to take advantage of this fix. Commit: 716bc5e9
  • Bump tangy-form to v4.29.1 and tangy-form-editor to v7.10.2 Commit: a3f785310

New Features

  • Add support for running an SSL frontend. Issue: #3147
  • Make CORS settings configurable by T_CORS_ALLOWED_ORIGINS Commit: 1f448f7e
  • Add ability to generate CSV datasets for all groups and all forms. This feature provides the new generate-csv-datasets command and csvDataSets route. #3149
  • Add support for Tangy Form's useShrinker flag, implemented as AppConfig.saveLessFormData. This is an experimental mode in Tangy Form that only captures the properties of inputs that have changed from their original state in the form. This should lead to smaller formResponses and quicker sync data transfers. Commit: 35a05c2b, Tangy-form pull: Add support for shrinking form responses #209

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.21.0\n./start.sh v3.21.0\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.20.4\n
"},{"location":"whats-new/#v3204","title":"v3.20.4","text":"

Fixes

  • Fixes resuming an unfinished Event Form. Commit bf97492

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.20.4\n./start.sh v3.20.4\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.20.3\n
"},{"location":"whats-new/#v3203","title":"v3.20.3","text":"

Fixes

  • Editing Timed Grids on Forms: Capture at item and Duration are compared as strings leading to unexpected validation scenarios #3130
  • Fix CORs usage in Tangerine APIs when outside applications are using credentials. #3132
  • When Tangerine creates CouchDB users for Sync, DB Administration, and Reporting, restrict that users access to the databases for the group they are assigned. This is a tightening of security to support use cases where users of groups on the same server should be restricted from accessing other groups data on the same server when Sync Protocol 2 and Database Administrator features are being used. #3118
  • Data Manager views in CSV which cycle sequence was used in each form response #3128.
  • Fix access denied message when using Tangerine APIs #3133
  • Make status translateable on Tangerine Teach Task Report. #3089
  • When editing Timed Grids on Forms, \"Capture at Time\" and \"Duration\" are compared as strings leading to unexpected validation scenarios. #3130
  • Fix database export when using Sync Protocol 1 by using the correct database names #3120

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.20.3\n./start.sh v3.20.3\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.20.2\n
"},{"location":"whats-new/#v3202","title":"v3.20.2","text":"

Fixes

  • Improve listing of items in the Data menu. Issue: #3125
  • Fix issue where Group User on server with permission to access database would not have access. Commit: a10162d9
  • Add Amharic translation.
  • Fix issue when backup has never run, the Clean backups command in Maintenance on client fails, and the process alert does not go away. This PR also copies over a fix for clearing all progress messages from Editor. PR: #3098
  • Fix bad url for Print Content feature in Editor/Author. PR: #3099
  • Clicking on unavailable form in Case should not open it. Issue: #3063
  • The csv and mysql outputs must carry over the 'archived' property from the group db. PR: #3104
  • Bump tangy-form to v4.28.2 and tangy-form-editor to v7.9.5. Includes fix for tangy-input-groups change logic Issue: #2728
  • Users should enter dataset description when creating a dataset in Editor PR: #3078
  • Avoid crashes when properties on the markup are accessed before being available to the component #3080
  • Replace special chars with underscore in CSV output. PR: #3003
  • Refresh global reference to T.case when using a case so most importantly the correct context is set PR: #3108
  • Link to download data set downloads a JSON file with headers and group config doc. Issue: #3114
  • CSV template creation fails. Issue: #3115
  • Restart couchdb container on failure. PR: #3112
  • APK and PWA Updates fail with User not logged in (every time) #3111
  • Fix error when looping through input values for data dictionary. PR: #3124
  • Add config to allow output of multiple participants in MySQL. Consult the PR for implementation details. If you wish to enable this feature, add T_MYSQL_MULTI_PARTICIPANT_SCHEMA:true to the config.sh script. PR: #3110

Upgrade notice

If your project was already using the Data Conflicts tools that were installed manually, you must remove those in order to prevent a conflict with the Database Conflicts tool that is now automatically installed in Tangerine -> Deploy -> Database Conflicts. Reset the group-uuid/editor directory with the content-sets/case-module/editor components or the content-sets/case-module-starter/editor/index.html file.

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.20.2\n./start.sh v3.20.2\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.20.1\n# This will index all database views in all groups. It may take many hours if \n# the project has a lot of data.\nwedge pre-warm-views --target $T_COUCHDB_ENDPOINT\n
"},{"location":"whats-new/#v3201","title":"v3.20.1","text":"

Fixes

  • Fix Form Editor removes manually added on-resubmit logic in tangy-form #3017
  • Support old PWAs that did not check for all permissions when installed in order to get permanent storage #3084

New Features

  • Add Maintenance page to client to enable app administration tasks (clear out old backups and fix permissions) and disk space statistics. #3059

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.20.1\n./start.sh v3.20.1\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.20.0\n# This will index all database views in all groups. It may take many hours if \n# the project has a lot of data.\nwedge pre-warm-views --target $T_COUCHDB_ENDPOINT\n
"},{"location":"whats-new/#v3200","title":"v3.20.0","text":"

Fixes

  • Improve rendering of Device listing. PR: #2924 Note that you must run the update or else the Device view will fail.
  • Converted print form view as a single table #2927
  • Improvements to restoring a database from a backup PR: #2938
  • On server group's security page, fix link to adding roles and show loading screen when saving role.
  • Data outputs for CSV's now include the 'archived' property. #2988
  • This one goes out to the coders: Prevent CaseService singleton injection into Case related components #2948. This is an important change in how cases are handled - they are no longer singletons. If you are developing scripts for a form and there are problems accessing T.case, see the comments in #2948 for a solution.

New Features

  • New CSVs related to Cases now available for Case Participants, Case Events, and Case Event Forms. https://github.com/Tangerine-Community/Tangerine/pull/2908
  • Online Survey user is warned if they are using an unsupported web browser (Internet Explorer). https://github.com/Tangerine-Community/Tangerine/pull/3001
  • Data Manager generates CSV with specific columns using CSV Templates.
  • Data Manager restores Case Event stuck in Conflict Revision. Add the can_restore_conflict_event permission to the users' role(s) to enable. #2949
  • Enable Data Conflict Manager for groups. 2997 This is based on the couchdb-conflict-manager web component.
  • In Offline App, when submitting a form, opening a case, creating a case, etc., a new loading screen is shown. #3000
  • In Online Survey, new support for switching language without interrupting the survey. #2643
  • For PWA's, there is a new device permissions step in device setup to guarantee persistent storage #3002
  • The login screen may now have custom markup. #2979
  • Statistical files are now available in Stata .do format for corresponding forms #2971
  • The new usePouchDbLastSequenceTracking property in app-config.json and settings page enables the use of PouchDB's native last sequence tracking support when syncing. #2999
  • The new encryptionPlugin:'CryptoPouch' property in app-config.json enables testing of the CryptoPouch extension currently in development. #2998 Please note that this feature is not yet ready for deployment. There are now three different possible storage configurations for Tangerine:
  • \"encryptionPlugin\":\"CryptoPouch\" - Configures the app to use CryptoPouch, which encrypts documents in the app's indexedb for storage.
  • \"turnOffAppLevelEncryption\": true - Configures the app without encryption, using the app's indexedb for storage instead of sqlite/sqlCypher.
  • \"encryptionPlugin\":\"SqlCipher\" - or without any additional configuration (SqlCipher is the default configuration.) - Configures the app to use SqlCipher, which encrypts documents in an external sqlLite database for storage.
  • We have changed how we determine which storage engine is being used. In the past we exposed a window['turnOffAppLevelEncryption'] global variable based on the same flag in app-config.json; however, now we are determining in app-init.ts which engine is running and exposing either window['cryptoPouchRunning'] or window['sqlCipherRunning'] to indicate which engine is running. It is important to note that even the app is configured with encryptionPlugin:'CryptoPouch' in app-config.json, the app may have been installed without that setting and is actually running sqlCypher. This is why it is important to observe if either window['cryptoPouchRunning'] or window['sqlCipherRunning'] is set.

Backports/Good to Know

When we add new features or fix issues in patch releases of Tangerine, those code changes usually get added automatically to any new releases of Tangerine. To make sure users of new releases are aware of those changes, we will occasionally mention them in this section in case they have missed them in the Changelog for the corresponding earlier release. Please note that when you install or upgrade a new Tangerine release, please review the Changelog for any changes in minor or patch releases.

  • Server admin can configure regex-based password policy for Editor. Instructions in the PR: #2858 Issue: #2844
  • Show loading screen in more places that typically hang such as the Case loading screen, issue loading, issue commenting, and many other places when working with Issues on the sever. (demo: https://youtu.be/RkoUN41jqr4)
  • Enhancements to support for archiving cases:
  • Added ability to search archived cases. Issue: #2977 Important : Run docker exec -it tangerine /tangerine/server/src/upgrade/v3.19.3.js to enable searching archived cases.
  • Added archive/unarchive Case functionality and permission for \"can delete\" #2954
  • Added backup and restore feature for Tangerine databases using device encryption. Increase the appConfig.json parameter dbBackupSplitNumberFiles (default: 200) to speed up the backup/restore process if your database is large. You may also change that parameter in the Export Backup user interface. Updated docs: Restoring from a Backup PR: #2910
  • Updates to tangy-form lib to 4.25.18 (Changelog), which provides:
  • Support for changing a page content's language and number system without reloading the page.
  • A fix for photo-capture so that it de-activates the camera when going to the next page or leaving a form. Also a new feature for configuring compression
  • Implemented a new 'before-submit' event to tangy-form in order to listen to events before the 'submit' event is dispatched.
  • A fix for User defined Cycle Sequences.
  • Important If your site uses csvReplacementCharacters to support search and replace configuration for CSV output, which was released v3.18.2, you must change the configuration string. See issue #2804 for information about the new schema.
  • Feature: Editor User downloads CSVs for multiple forms as a set Issue: #2768 PR:#2777
  • Feature: Remove configurable characters from CSV output #2787.

Server upgrade instructions

Important upgrade: Please note that you must run update below (v3.20.0.js) to install the new listDevices view. If you don't the Devices listing will fail.

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.20.0\n./start.sh v3.20.0\n# Run the update to install the new listDevices view.\ndocker exec -it tangerine /tangerine/server/src/upgrade/v3.20.0.js\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.19.1\n# This will index all database views in all groups. It may take many hours if \n# the project has a lot of data.\nwedge pre-warm-views --target $T_COUCHDB_ENDPOINT\n
"},{"location":"whats-new/#v3193","title":"v3.19.3","text":"

Fixes

  • Fix issue where loading screen would not close after submitting a proposal on an Issue.
  • Fixes from v3.18.8 incorporated.
  • Fixes to how role based permission rules are applied on the schedule view.
  • Fix CaseService.rebaseIssue from failing due to accessing eventForms incorrectly.

New Features

  • Show loading screen in more places that typically hang such as the Case loading screen, issue loading, issue commenting, and many other places when working with Issues on the sever. (demo: https://youtu.be/RkoUN41jqr4)
  • Material design applied to loading indicator on the server.
  • New cancel button on loading indicator on the server. Will warn that this may cause data corruption and data loss. (demo: https://youtu.be/da9cxG5w8c0)
  • Added ability to search archived cases. Issue: #2977 Important : Run docker exec -it tangerine /tangerine/server/src/upgrade/v3.19.3.js to enable searching archived cases.

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.19.3\n./start.sh v3.19.3\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.19.2\n# Run the v3.19.3.js update to enable indexing of archived documents.\ndocker exec -it tangerine /tangerine/server/src/upgrade/v3.19.3.js\n# This will index all database views in all groups. It may take many hours if \n# the project has a lot of data.\nwedge pre-warm-views --target $T_COUCHDB_ENDPOINT\n
"},{"location":"whats-new/#v3192","title":"v3.19.2","text":"

Fixes

  • Added process indicator when archiving, un-archiving, or deleting a case. Issue: #2974
  • Add v3.19.2 update to recover if v3.19.0 search indexing failed

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.19.2\n./start.sh v3.19.2\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.19.1\n# Perform additional upgrades.\ndocker exec -it tangerine bash\n# This will index all database views in all groups. It may take many hours if \n# the project has a lot of data.\nwedge pre-warm-views --target $T_COUCHDB_ENDPOINT\n
"},{"location":"whats-new/#v3191","title":"v3.19.1","text":"

Fixes

  • Improved backup and restore file processing. Docs: Restoring from a Backup PR: #2910
  • Added archive/unarchive Case functionality and permission for \"can delete\" #2954

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.19.1\n./start.sh v3.19.1\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.19.0\n# Perform additional upgrades.\ndocker exec -it tangerine bash\n# This will index all database views in all groups. It may take many hours if \n# the project has a lot of data.\nwedge pre-warm-views --target $T_COUCHDB_ENDPOINT\n
"},{"location":"whats-new/#v3190","title":"v3.19.0","text":"

New Features

  1. Data Manager requests and downloads CSVs for multiple forms as a set. When logged into the server and in a group, you will now find a \"Download CSV Data Set\" menu item under \"Data\". From there you can view all of the CSV Data Sets you have generated in the past, the status of wether or not they have finished generating, a link to download them, and other meta data. Click the \"New Data Set\" button and you will be able to select any number of forms to generate CSVs for, data for all time or a specific month, and wether or not to exclude PII. This is especially useful for generating CSVs that take longer to generate than the automatic logout built into the server. You may request a CSV Data Set, log out, and then log back in later to check in on the status and download it. A Server Administrator can also configure cron with a generate-csv-data-set command to generate a data set on a daily, weekly, or monthly basis, handy for situations where you want CSVs to automatically generate on the weekend and then download them on Monday. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2768) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2777)
  2. Data Manager archives a Case to remove it from reporting output and Devices. This adds an \"archive\" button on Cases that flags all related Form Responses as archived and removes them from CSV output and Search on Devices. This uses the new T.case.archive() API which adds an 'archived' flag for those docs and saves a minimal version of the doc with enough data to be indexed on the server. Search on client and server CSV output are modified to filter archived docs. When viewing cases in Editor, displays \"Archived\" when viewing an archived case. When client syncs, it deletes any docs with the 'archived' flag and sets deletedArchivedDocs In the replicationStatus log. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2843) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2776)
  3. Devices Manager reconfigures claimed Device sync settings and selects multiple Sync Locations for a Device. Details: To change a Device's sync settings currently requires a reinstall of the app on the Device and setting up all the accounts again. This PR will allow system admins to change the sync settings for a Device which then triggers on next sync a Rewind Push, database delete, then a first pull with the new sync settings. Subsequent syncs then use the new sync settings. This PR also refactors the Create and Edit forms for Devices on the server so that multiple sync locations can be added. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2867) (PR: #2782)
  4. Device Manager estimates how large an initial sync will be given selected sync settings. When setting up sync settings for a Device, it is useful to know how many documents will need to be downloaded given which forms are configured for syncing down and the locations assigned. There is now a \"calculate down-sync size\" button at the bottom of Device edit/creation forms that when pressed will tally up the documents needing to be down synced given the device sync settings. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2845) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2818)
  5. Devices Manager monitors for Devices close to filling up disk space. Devices now report how much free space they have to the server after a sync. This can be monitored on the Deploy > Devices list. When a Device reports having less than 1GB free storage, a warning is shown on the Devices list. (Ticket: 2779) (PR: 2795)
  6. Server User views the version of Tangerine installed. Any user on the server can now view the version of Tangerine installed by going to Help menu in the left nav bar. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2846) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2794)
  7. Database Consumer accesses Tangerine MySQL databases via web browser. Users of Tangerine's MySQL database sometimes are not allowed to install tools such as MySQL Workbench on their work computers. This PR makes starting PHPmyAdmin (a mysql viewer) as a web service a configuration option in Tangerine so no one has to install software on their computer to access Tangerine MySQL. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2847) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2793)
  8. Data Collector creates Account on Device and associates with any User Profile in Group (ignoring Device assignment/sync settings). By default, when a Data Collector creates an Account on Device, they can only associate with User Profiles that are assigned to the same location as the Device's Assigned Location. Add \"disableDeviceUserFilteringByAssignment\":true to the app-config.json for the group and this restriction will be removed. Tablets will also sync all User Profiles, ignoring the Device's configured Sync Location(s). (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2848) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2792)
  9. Form Developer writes code that can access Case's related Location metadata without writing asynchronous code. When working synchronously in forms, we don't currently have access to the related Location Node data without loading the Location List async and using T.case.case.location to search the hierarchy for the node we want. This PR loads all related Location Nodes into memory at T.case.location when the context of a Case is set. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2849) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2791)
  10. Server Administrator configures substitutions for CSV output. This feature allows a Server Administrator to update the group's configuration in the app database to contains Regex string replacements for CSV output. This can be handy in situations where Data Analysts are having trouble parsing CSV data that contains line breaks and commas. An example configuration to remove line breaks and commas from data would be \"csvReplacementCharacters\": [{\"search\": \",\", \"replace\": \"|\"}, {\"search\": \"\\n\", \"replace\": \"___\"}]. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2787) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2788)
  11. Server Administrator configures Tangerine to not auto-commit in groups' data directories to preserver manually managed git content repositories. When using git to manage group content in a git flow like manner, the automatic commit can result in unnintentional commits. System Administrators can now turn off this auto-commit by configuring Tangerine's config.sh with T_AUTO_COMMIT=\"false\". If set to true also include the frequency T_AUTO_COMMIT_FREQUENCY=\"60000\" (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2614) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2748)
  12. Data Collector proposes change to a Form on a Case. The issues feature that has been available on the server is now optionally also available on Devices by \"allowCreationOfIssues\": true to client/app-config.json for the group you want this enabled. Most of the features of Issues you are familiar with from the server are there, except for merging proposals which is not allowed. Issues from Devices are uploaded to the server where proposals can be merged by a Data Manager. We also streamlined the Issue creation and proposal process by skipping the page to fill out an issue title/description, and then forward them directly to creating a proposal. To aid in issue titles/descriptions that make sense, Content Developers can now add templateIssueTitle and templateIssueDescription to Case Definition files. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2850) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2330) (Demo Video)
  13. Data Manager updates Issue Title and Issue Description Data Managers will now find a metadata tab on an Issue where they can update the Title, Description, and new \"Send to\" settings. (Issue: https://github.com/Tangerine-Community/Tangerine/issues/2851) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2330)
  14. Data Manager sends an Issue to all Devices in Sync Area or specific Device When create/configuring an Issue, Data Managers now have the option to send an Issue to a specific location in a Sync Area, or send it to a specific Device by Device ID. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2854) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2330)
  15. Forms Developer defines custom logic for Device's search of Cases and Forms In some cases there are situations where the standard variables for searching do not cover all things we want searched, or there is a compound field we want to be searched. Adding a client/custom-search.js file allows the Forms Developer to hook into the map function used to generate the search index. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2852) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2740)
  16. Data Manager views list of Issues related to Case When viewing a Case on the server, the first screen when opened will now show a list of related Issues. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2723) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2573)
  17. Form Developer uses API to override Device User based access to Event Forms on a per Case Event basis Currently we can configure in a Case Definition the operation permissions on all instances of an Event Form. This change allows a Form Developer to write logic that would control those permissions on a per Event Form basis by setting the same permissions property on the Event Form itself. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2624) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2660)
  18. Form Developer configures Devices to skip optimization of database views not in use on Device. Some projects relying heavily on a custom app will find they do not use all of the standard Tangerine database views, thus they can be skipped during the data optimization phase after a sync. In app-config.json, you can add a new doNotOptimize property with a value as an array of views to skip. To discover what views your app is indexing, see the console logs from a device during the optimization phase. You may discover some views you can add to doNotOptimize to speed up that optmization process. (Commit: https://github.com/Tangerine-Community/Tangerine/commit/4b8864470c1cad98e43152dd6bb3c91ee3e576a6)
  19. System Administrator batch imports all forms from a Tangerine v2 group into a Tangerine v3 group Tangerine v3 now has a script that will import all v2 group forms into a v3 group without having to do each form individually. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2857) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2584)

Fixes

  1. Issue created programatically in on-submit says we must rebase but no button to rebase #2785 Cases that have used the T.case.createIssue() API in forms to create Issues on the current form have recently found the resulting issues are broken. This is due to a change in when the Form Response is associated with the case (later than when T.case.createIssue() is called in a form's on-submit). To remedy this, we've added a new T.case.queueIssueForCreation(\"Some label\", \"Some comment\") API. If you are using T.case.createIssue(), immediately upgrade and replace its usage with T.case.queueIssueForCreation(). (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2785) (Example: https://github.com/Tangerine-Community/Tangerine/blob/next/content-sets/case-module/client/test-issues-created-programatically-on-client/form.html#L5)
  2. Using a simpler reverse sort for device status (PR: https://github.com/Tangerine-Community/Tangerine/pull/2775)
  3. Increase likelihood that migration of data to mysql will recover where it left off if server restarts. (PR: https://github.com/Tangerine-Community/Tangerine/pull/2773)
  4. From Case Definitions, the onCaseOpen and onCaseClose now also run in the server context. (PR: https://github.com/Tangerine-Community/Tangerine/pull/2696)
  5. \"openEvent is not defined\" when accessing a case in Editor (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2800)
  6. Synclog date/time header is incorrect and sort is broken (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2762)
  7. Synchronization UX Improvements - remove error state after retries when retry is successful (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2808) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2826)
  8. Fix missing 'form_' from id for v2 import (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2856) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2726)
  9. Minor tweak to tangerine-preview README (PR: https://github.com/Tangerine-Community/Tangerine/pull/2735)

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.19.0\n./start.sh v3.19.0\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.18.3\n# Perform additional upgrades.\ndocker exec -it tangerine bash\npush-all-groups-views\nupdate-down-sync-doc-count-by-location-id-index '*'\n# This will index all database views in all groups. It may take many hours if \n# the project has a lot of data.\nwedge pre-warm-views --target $T_COUCHDB_ENDPOINT\n
"},{"location":"whats-new/#v31810","title":"v3.18.10","text":"

Fixes

  • Backport: Make status translateable on Tangerine Teach Task Report. #3089
  • Backport: When editing Timed Grids on Forms, \"Capture at Time\" and \"Duration\" are compared as strings leading to unexpected validation scenarios. #3130
"},{"location":"whats-new/#v3189","title":"v3.18.9","text":"

Fixes

  • Backport: Restrict access to events by permissions when query by date on schedule view.
  • Fix issue where logging in as a different user shows the previously logged in users data (Multiuser/Tablet sharing https://github.com/Tangerine-Community/Tangerine/issues/2060)
  • Add additional translateables to Tangerine Teach components (Translatable feedback status text: https://github.com/Tangerine-Community/Tangerine/issues/2693) (Missing translatable strings: https://github.com/Tangerine-Community/Tangerine/issues/2987)
  • Allow class title to be anywhere on form #2994
"},{"location":"whats-new/#v3188","title":"v3.18.8","text":"
  • Add support for skipping indexes in form's cycle sequences.
  • Fix radio button scoring in Teach by only adding the final value of max to the totalMax variable. https://github.com/Tangerine-Community/Tangerine/issues/2947
  • On Tangerine Teach reports, fix calculating of \"percentile\", AKA percent correct grouping. https://github.com/Tangerine-Community/Tangerine/issues/2941
"},{"location":"whats-new/#v3187","title":"v3.18.7","text":"

Fixes

  • Back-ported some fixes to the backup and restore feature from the v3.19.1 branch.
  • Fixed issue with Teach where third subtask would not open correctly.

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.18.7\n./start.sh v3.18.7\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.18.6\n
"},{"location":"whats-new/#v3186","title":"v3.18.6","text":"

Updates

  • Updated tangy-form lib from 4.25.11 to 4.25.14 (Changelog), which provides:
  • A fix for photo-capture so that it de-activates the camera when going to the next page or leaving a form.
  • Implemented a new 'before-submit' event to tangy-form in order to listen to events before the 'submit' event is dispatched.
  • A fix for User defined Cycle Sequences.

Fixes

  • Remove incorrect exception classes for changes processing #2883 PR: #2883 Issue: #2882
  • Added backup and restore feature for Tangerine databases using device encryption. Increase the appConfig.json parameter dbBackupSplitNumberFiles (default: 200) to speed up the backup/restore process if your database is large. You may also change that parameter in the Export Backup user interface. Updated docs: Restoring from a Backup PR: #2910

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.18.6\n./start.sh v3.18.6\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.18.5\n
"},{"location":"whats-new/#v3185","title":"v3.18.5","text":"

Fixes

  • Server admin can configure regex-based password policy for Editor. Instructions in the PR: #2858 Issue: #2844
"},{"location":"whats-new/#v3184","title":"v3.18.4","text":"

Fixes

  • Backported a fix from the v3.19.0 branch for \"Save the lastSequence number after each change is processed in the tangerine-mysql connector\" Issue #2772
  • Address crashes when importing data using the mysql module #2820
cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.18.4\n./start.sh v3.18.4\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.18.3\n
"},{"location":"whats-new/#v3183","title":"v3.18.3","text":"

Fixes

  • Important If your site uses csvReplacementCharacters to support search and replace configuration for CSV output, which was released v3.18.2, you must change the configuration string. See issue #2804 for information about the new schema.
  • Backported a fix from the v3.19.0 branch for \"Issue created programmatically in on-submit says we must rebase but no button to rebase #2785\"
  • Description: Cases that have used the T.case.createIssue() API in forms to create Issues on the current form have recently found the resulting issues are broken. This is due to a change in when the Form Response is associated with the case (later than when T.case.createIssue() is called in a form's on-submit). To remedy this, we've added a new T.case.queueIssueForCreation(\"Some label\", \"Some comment\") API. If you are using T.case.createIssue(), immediately upgrade and replace its usage with T.case.queueIssueForCreation().
  • Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2785
  • Example: https://github.com/Tangerine-Community/Tangerine/blob/next/content-sets/case-module/client/test-issues-created-programatically-on-client/form.html#L5
"},{"location":"whats-new/#v3182","title":"v3.18.2","text":"
  • Feature: Editor User downloads CSVs for multiple forms as a set Issue: #2768 PR:#2777
  • Feature: Remove configurable characters from CSV output #2787.
  • Documentation updates for backup/restore and fixes to image paths
  • Fix default user profile so it doesn't assume use of roles or location
  • Disabled \"Print form backup\" in Editor
  • Improvements to display of \"Print metadata\" in Editor
  • Update and fix for Cycle Sequences to enable numbering of sequences starting from 1. PR's: #231, #269
  • Bump tangy-form to 4.25.11 and tangy-form-editor to 7.8.8.
"},{"location":"whats-new/#v3181","title":"v3.18.1","text":"
  • Fix backup when using os encryption and sync protocol 2 and cordova. (PR: #2767)
  • Fix creating of new Device Users when using Sync Protocol 2. (PR: #2769)
  • Fix default user profile form for Sync Protocol 1 users. We should not assume they are using roles or location.
"},{"location":"whats-new/#v3180","title":"v3.18.0","text":""},{"location":"whats-new/#new-features","title":"New Features","text":"
  • Enable configurable image capture in client #2695
  • Makes image capture work with a max size attribute - PR: #218
  • Add photo capture widget #203
  • Serve base64 image data as image files #2706 PR: #2725
  • Add Cycle sequences 1603
  • Sort by lastModified in the client case search #2692
  • Enable assigning multiple roles in forCaseRole in the eventFormDefinition #2694
  • Enable defining custom functions or valid JavaScript expressions that will be called when an event is opened and when an event is closed. On open and close events for case and case-events: #2696
  • Teach-specific strings in Russian for default content-set #2676
  • Uploads status such as app version when updating the app #2756
"},{"location":"whats-new/#bugfixes","title":"Bugfixes","text":"
  • Initialize git in content repository before running git commands #2667
  • Only show the links to historical releases when T_ARCHIVE_PWAS_TO_DISK and T_ARCHIVE_APKS_TO_DISK in the config.sh are set to true #2608
  • Fix form breaking when form name has single quote #2489
  • Add print options to archived forms #1987
  • Fix Grid having negative values #2294
  • Fix to allow for running on m1 Macs #2631 #2631 Thanks @fmoko and @evansdianga!
  • For projects using the Case Reporting screen but don't have anything in reports.js but do have markup in reports.html, avoid crash due to empty file #2657
  • V2 import script fixes #2675
  • Allow HTML markup in option labels 2453
  • Reset grid values when grid is restarted #
  • Mark last attempted automatically when grid is auto-stopped #2467
"},{"location":"whats-new/#new-documentation","title":"New Documentation","text":"
  • Deleting Records
  • Bullet points for Tangerine Development

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.18.0\n./start.sh v3.18.0\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.17.11\n
"},{"location":"whats-new/#v31712","title":"v3.17.12","text":"
  • Feature: Remove configurable characters from CSV output #2787.

This release also has bugfixes specific to the Class module, which now uses updated API's for form rendering.

  • Feature for Class/Teach: Archive or enable a class. Issue: #2580
  • Bugfix for Class/Teach: Teach loses data and blocks app if Class form is not submited #2783
  • Bugfix for Class/Teach: App should return user to previous Curriculum when resuming app. Issue: #2648
  • Refactor Class to handle changes in tangy-form; Bug in CSV rendering for Tangerine Teach. Issue: #2635
"},{"location":"whats-new/#v31711","title":"v3.17.11","text":"
  • Added support for custom update scripts for each group. Add either a before-custom-updates.js or after-custom-updates.js to the root of your content depending on when you wish the script to run. Script needs to return a Promise. See Issue 2741 for script example. PR: #2742
  • Add support for filtering PII variables on Case Participant data and Event Form data in Synapse caches. List the variable names in your group's content folder reporting-config.json. For example: { \"pii\": [\"foo_variable\"] }. This config was previously stored in the groups database.
  • Fixed bug that prevented rewind sync from working.

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.17.11\n./start.sh v3.17.11\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.17.10\n
"},{"location":"whats-new/#v31710","title":"v3.17.10","text":"
  • Skip optimizing sync-queue, sync-conflicts, and tangy-form views after Sync Protocol 2 sync completes.
  • Using T.case.load() in a form? This release fixes a bug where EventForm.formResponseId would be not set when submitting forms in cases where a form has loaded a different case and then the save case back again thus detaching the memory reference being previously set.
  • Remove trailing whitespace from variables for mysql outputs to avoid illegal column names.
  • Add response-variable-value API with support for returning jpeg and png base64 values as files.
  • Refactor TANGY-SIGNATURE and TANGY-PHOTO-CAPTURE output in CSVs to be URLs of the image files.
  • Creates work-around for deployments that are unable to use custom-scripts. Issue #2711 PR #2712

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.17.10\n./start.sh v3.17.10\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.17.9\n
"},{"location":"whats-new/#v3179","title":"v3.17.9","text":""},{"location":"whats-new/#new-features-and-buffixes","title":"New Features and Buffixes","text":"
  • Prevent failed calls to T.case.save() in forms by avoiding any saves to a case when a form is active. PR, Issue
  • Enable assigning multiple roles in forCaseRole in the eventDefinition #2694 - Cherry-picked commit 3e4938a0a80c57 only.
  • Enable defining custom functions or valid JavaScript expressions that will be called when an event is opened and when an event is closed. On open and close events for case and case-events: #2702

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.17.9\n./start.sh v3.17.9\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.17.8\n
"},{"location":"whats-new/#v3178","title":"v3.17.8","text":"
  • Fix use of initial batch size #2685
  • Created generate-form-json script that generates the form json for a group from its form.html file. Usage: docker exec tangerine generate-form-json group-uuid The script loops through a group's forms.json and creates a form.json file in each form directory, next to its forms.html. Before using this script, run npm install. Issue: #2686
  • The synapse module now uses the json from generate-form-json to exclude PII. Also, the synapse module takes substitution and pii fields to accommodate schema changes and pii fields not identified in forms. PR: #2697

Place these properties in the groups Couchdb:

  \"substitutions\": {\n    \"mnh_screening_and_enrollment_v2\": \"mnh01_screening_and_enrollment\"\n  },\n  \"pii\": [\n    \"firstname\",\n    \"middlename\",\n    \"surname\",\n    \"mother_dob\"\n  ]\n

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.17.8\n./start.sh v3.17.8\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.17.7\n
"},{"location":"whats-new/#v3177","title":"v3.17.7","text":"
  • fix CSV generation issue: #2681

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.17.7\n./start.sh v3.17.7\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.17.6\n
"},{"location":"whats-new/#v3176","title":"v3.17.6","text":"
  • fix issue w/ empty replicationStatus?.userAgent
  • Switched from just-snake-case to @queso/snake-case - better Typescript compatability.

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.17.6\n./start.sh v3.17.6\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.17.5\n
"},{"location":"whats-new/#v3175","title":"v3.17.5","text":"
  • Bumps tangy-form to 4.23.3, editor to 4.23.3. Issue: 2620
  • Update date carousel to 5.2.1 with fix for clicking the today button. PR: #2677

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.17.5\n./start.sh v3.17.5\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.17.4\n
"},{"location":"whats-new/#v3174","title":"v3.17.4","text":"
  • Enables support for reducing the number of documents processed in the changed feed when syncing using the 'changes_batch_size' property in app-config.json. This new setting will help sites that experience crashes when syncing or indexing documents. Using this setting will slow sync times. Default is 50. During recent tests, the following settings have been successful in syncing a location with over 12,700 docs that was experiencing crashes:
  • \"batchSize\": 50
  • \"writeBatchSize\": 50
  • \"changes_batch_size\": 20

Please do note that these particular settings do make sync very slow - especially for initial device sync. - Removed selector from push sync - was causing a crash on large databases. Using a filter instead in the push syncOptions to exclude '_design' docs from being pushed from the client. - Adds \"Encryption Level\" column to the Devices Listing, which shows if the device is running 'OS' encryption or 'in-app' encryption. - 'OS' encryption: Encryption provided by the device operating system; typically this is File-based (Android 10) or Full-disk encryption (Android 5 - 9). - 'in-app' encryption: Database is encrypted by Tangerine.

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.17.4\n./start.sh v3.17.4\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.17.3\n
"},{"location":"whats-new/#v3173","title":"v3.17.3","text":"
  • Automatically retry after failed sync. (https://github.com/Tangerine-Community/Tangerine/pull/2663)
  • Do not associate form response with Event Form if only opened and no data entered.
  • Fix issue causing Android Tablets using OS level encryption to spontaneously start using in-app encryption.

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.17.3\n./start.sh v3.17.3\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.17.2\n
"},{"location":"whats-new/#v3172","title":"v3.17.2","text":"
  • Add support for depending on Android Disk encryption as opposed to App Level encryption. Set turnOffAppLevelEncryption to true in client/app-config.json. Note that enabling this will not turn off App Level encryption for devices already installed, only new installations.
  • Fix race condition data conflict on EventFormComponent that is triggered when opening and submitting a form quickly. Prevent data entry until Case is loaded to avoid conflicting Case save of a fast submit.
  • Fix bug causing Device ID to not show up on About page on Devices.
  • When syncing, push before pull to avoid having to analyze changes pulled down for push.
  • Fix download links for archived APKs on Live channel.

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.17.2\n./start.sh v3.17.2\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.17.1\n
"},{"location":"whats-new/#v3171","title":"v3.17.1","text":"
  • Add support for Form Versions when it hasn't been used before by defaulting the first entry in formVersions when a form version isn't defined on a Form Response.
  • Fix issue causing Device Admin user log in to fail.
  • Restore missing sectionDisable function in skip logic for forms.

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Ensure git is initialized in all group folders.\ndocker start couchdb\ndocker start tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.17.1\n./start.sh v3.17.1\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.17.0\n
"},{"location":"whats-new/#v3170","title":"v3.17.0","text":"

New Features and Fixes

  • Device User Role access to Case Events and Event Forms #2598
  • Getting started with using Device User Roles
  • Demo: Device User role based access to Event Forms
  • Demo: Device user role based permissions for Case Events
  • Deactivate Case Participant API #2594
  • Demo: https://youtu.be/Ulh-yCqfbFA
  • Data Collector with a single click opens all pages of a completed form response #2596
  • skip() and unskip() functions are now available in tangy-form level on-change logic for skipping and unskipping sections, not inputs.
  • Fix print form as table for some forms. (https://github.com/Tangerine-Community/Tangerine/pull/2568)
  • Update the group icon on server #2355
  • Add window.uuid() API #2595

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Ensure git is initialized in all group folders.\ndocker start couchdb\ndocker start tangerine\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.17.0\n./start.sh v3.17.0\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.16.4\n
"},{"location":"whats-new/#v3165","title":"v3.16.5","text":"

Fixes

  • T_ARCHIVE_APKS_TO_DISK and/or T_ARCHIVE_PWAS_TO_DISK setting have no effect. Issue: #2608
  • Bug in CSV rendering for Tangerine Teach. Issue: #2635 new setting outputDisabledFieldsToCSV in groups doc

Developer Interest

There is now a content set for developing projects with the Class module enabled in content-sets/teach. Sets the following properties in app-config.json:

  • \"homeUrl\": \"dashboard\"
  • \"uploadUnlockedFormReponses\": true

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Ensure git is initialized in all group folders. \ndocker start couchdb\ndocker start tangerine\ndocker exec tangerine sh -c \"cd /tangerine/groups && ls -q | xargs -i sh -c 'cd {} && git init && cd ..'\"\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.16.5\n# If you are enabling the new mysql module, follow the instructions in `docs/system-administrator/mysql-module.md` to update the config.sh file (steps 1 through 3)\n# If you do not wish APK and PWA archives to be saved, set T_ARCHIVE_APKS_TO_DISK and/or T_ARCHIVE_PWAS_TO_DISK to false.\n# Then return here before starting tangerine\n# Now you are ready to start the server.\n./start.sh v3.16.5\ndocker exec tangerine push-all-groups-views\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.16.4\n# If setting up mysql return to step 5 in `docs/system-administrator/mysql-module.md`\n
"},{"location":"whats-new/#v3164","title":"v3.16.4","text":"

New Features

  • Warning about data sync: Any site that upgraded to v3.16.2 is at risk of having records stay on the tablet unless they upgrade to v3.16.3 or v3.16.4. After upgrading to v3.16.4, go to the Online Sync feature and click the new 'Advanced Options' panel. There are two new options for sync - Comparison Sync and Rewind Sync. Comparison sync enables the Sync feature to compare all document id's on the local device with the server and uploads any missing documents. Rewind Sync resets the sync \"placeholder\" to the beginning, ensuring that all docs are synced. It doesn't actually re-upload all docs; it instead checks that all docs have been uploaded. It is more thorough than Comparison Sync. Both of the features are for special cases and should not be used routinely. Issue: #2623

There are two settings that can be configured for Comparison sync: - compareLimit (default: 150) - Document id's must be collected from both the tablet and server in order to calculate what documents need to be sync'd to the server. This setting limits the number of docs queried in each batch. - batchSize (default: 200) - Number of docs per batch when pushing documents to the server. This same configuration setting is used for normal sync, so please take care when making changes to it.

This new \"Comparison\" option is very new and may have rough edges. In our experience, if the app crashes while using it, re-open the app and try again; chances are that it will work. If it consistently fails, lower the value for app-config.json's compareLimit property.

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Ensure git is initialized in all group folders.\ndocker start couchdb\ndocker start tangerine\ndocker exec tangerine sh -c \"cd /tangerine/groups && ls -q | xargs -i sh -c 'cd {} && git init && cd ..'\"\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.16.4\n# If you are enabling the new mysql module, follow the instructions in `docs/system-administrator/mysql-module.md` to update the config.sh file (steps 1 through 3)\n# If you do not wish APK and PWA archives to be saved, set T_ARCHIVE_APKS_TO_DISK and/or T_ARCHIVE_PWAS_TO_DISK to false.\n# Then return here before starting tangerine\n# Now you are ready to start the server.\n./start.sh v3.16.3\ndocker exec tangerine push-all-groups-views\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.16.3\n# If setting up mysql return to step 5 in `docs/system-administrator/mysql-module.md`\n
"},{"location":"whats-new/#v3163","title":"v3.16.3","text":"

New Features

  • Warning about data sync: Any site that upgraded to v3.16.2 is at risk of having records stay on the tablet unless they upgrade to v3.16.3. After upgrading to v3.16.3, run the new \"Push all docs to the server\" feature available from the Admin Configuration menu item. This feature resets push sync to the beginning, ensuring that all docs are pushed. It doesn't actually re-upload all docs; it instead checks that all docs have been uploaded.

  • Added \"Push all docs to the server\" feature to the Admin Configuration menu item.

  • Added Operating System and Browser Version to Device listing.

Fixes - Data collected after first registering and after updates fails to upload. Issue: #2623

Server upgrade instructions

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Ensure git is initialized in all group folders. \ndocker start couchdb\ndocker start tangerine\ndocker exec tangerine sh -c \"cd /tangerine/groups && ls -q | xargs -i sh -c 'cd {} && git init && cd ..'\"\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.16.3\n# If you are enabling the new mysql module, follow the instructions in `docs/system-administrator/mysql-module.md` to update the config.sh file (steps 1 through 3)\n# If you do not wish APK and PWA archives to be saved, set T_ARCHIVE_APKS_TO_DISK and/or T_ARCHIVE_PWAS_TO_DISK to false.\n# Then return here before starting tangerine\n# Now you are ready to start the server.\n./start.sh v3.16.3\ndocker exec tangerine push-all-groups-views\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.16.2\n# If setting up mysql return to step 5 in `docs/system-administrator/mysql-module.md`\n
"},{"location":"whats-new/#v3162","title":"v3.16.2","text":"

New Features

  • Enables filtering of Case Event Schedule by Device's Assigned Location PR: #2591

Fixes - Enables editing of device description. Commit: #2613

Server upgrade instructions

If you want to enable filtered Case Event Schedule by Device's Assigned Location, add filterCaseEventScheduleByDeviceAssignedLocation to your groups' app-config.json set to a value of true.

Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Ensure git is initialized in all group folders. \ndocker start couchdb\ndocker start tangerine\ndocker exec tangerine sh -c \"cd /tangerine/groups && ls -q | xargs -i sh -c 'cd {} && git init && cd ..'\"\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.16.2\n# If you are enabling the new mysql module, follow the instructions in `docs/system-administrator/mysql-module.md` to update the config.sh file (steps 1 through 3)\n# If you do not wish APK and PWA archives to be saved, set T_ARCHIVE_APKS_TO_DISK and/or T_ARCHIVE_PWAS_TO_DISK to false.\n# Then return here before starting tangerine\n# Now you are ready to start the server.\n./start.sh v3.16.2\ndocker exec tangerine push-all-groups-views\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.16.1\n# If setting up mysql return to step 5 in `docs/system-administrator/mysql-module.md`\n
"},{"location":"whats-new/#v3161","title":"v3.16.1","text":"

New Features

  • Improves sync stats and add \"Export Device List\" feature PR: #2610

Fixes - Fixes Editor form creation issue #2605 and form copy issue #2604 - Adds check for calculateLocalDocsForLocation before running update to index an index it depends upon. - Update tangy-form to 4.21.3, tangy-form-editor to 7.6.5 to fix dynamically set level tangy location not resuming correctly #202

Server upgrade instructions Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Ensure git is initialized in all group folders. \ndocker start couchdb\ndocker start tangerine\ndocker exec tangerine sh -c \"cd /tangerine/groups && ls -q | xargs -i sh -c 'cd {} && git init && cd ..'\"\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.16.1\n# If you are enabling the new mysql module, follow the instructions in `docs/system-administrator/mysql-module.md` to update the config.sh file (steps 1 through 3)\n# If you do not wish APK and PWA archives to be saved, set T_ARCHIVE_APKS_TO_DISK and/or T_ARCHIVE_PWAS_TO_DISK to false.\n# Then return here before starting tangerine\n# Now you are ready to start the server.\n./start.sh v3.16.1\ndocker exec tangerine push-all-groups-views\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.16.0\n# If setting up mysql return to step 5 in `docs/system-administrator/mysql-module.md`\n
"},{"location":"whats-new/#v3160","title":"v3.16.0","text":"

New Features

  • Warning about data sync: If you have implementations that have multiple tablets syncing from the same location, some docs may not be on all tablets due to issues with earlier versions of sync. This release resolves that particular issue and provides ways to ensure that all tablets share the same data. We have implemented several ways to rectify and understand potential data inconsistencies across tablets in the field:
  • After updating the server to 3.16.0 and after updating and syncing the clients, the Device dashboard will now display the number of docs on each tablet (\"All Docs on Tablet\") and the number of docs according to the device's Location configuration (\"Form Responses on Tablet for Location\").

    • Depending on the Configure/Sync settings, the \"All Docs on Tablet\" count may be close, but not exactly the same, since not all forms may be synced to the tablets.
    • The \"Form Responses on Tablet for Location\" count should be the same for all tablets that share the same location configuration. Please note that \"Form Responses on Tablet for Location\" count needs to be activated by adding \"calculateLocalDocsForLocation\": true to app-config.json; also note that it has not been widely tested and may be unstable. (If you activate this feature, you may also add \"findSelectorLimit\" to modify how many batches are used to calculate this value. Default is 200. Lower is safer but slower.)

    These data points may help in identifying data inconsistencies. Remember - only after updating and syncing the tablets, will these new doc counts be populated with data in the Devices listing. Making a note of the document counts per tablet will help establish a baseline. - Next step would be to run the new \"Force Full Sync\" feature, which is implemented in two ways: - If you add the new \"forceFullSync\" : true setting in the group's app-config.json, the client will perform a full sync upon the next update. Since this takes time and Internet bandwidth, you may wish to notify users before enabling this feature. - When logging in as \"admin\" user on the client tablet, a new menu item called \"Admin Configuration\" will be visible below the \"Settings\" item. This new item enables manual operation of the \"Force Full Sync\" feature. It is labeled \"Pull all docs from the server\" in the user interface. - You may adjust the settings for how many documents \"Force Full Sync\" downloads at a time by adjusting the initialBatchSize property in app-config.json. The default is 1000 documents per batch. This setting is also used when performing the initial load of documents on a tablet. - Tangerine Release Archives: Every Tangerine APK or PWA release is saved and tagged. If your site is configured for archives (which is the default), you may download previous Android releases. PR: #2567 - A \"Description\" field has been added to the Devices listing to faciliate identification of devices or groups of devices. - Beta Release Mysql module: Data sync'd to Tangerine can be output to a MySQL database. Warning: This should not yet be deployed on a production server; the code for this feature is still in development. We recommend creating a separate server for the Tangerine/MySQL installation and replicate data from the production server to the Tangerine server that would provide the MySQL service. Docs: docs/system-administrator/mysql-module.md PR: #2531 - Devices listing offers more information about the sync process, including version, errors, and sync duration.

Fixes - Changes to the sync code should improve sync stability and speed. #2592 You may configure certain sync properties: - initialBatchSize = (default: 1000) Number of documents downloaded in the first sync when setting up a device. - batchSize (default: 200) - Number of documents downloaded upon each subsequent sync. - writeBatchSize = (default: 50) - Number of documents written to the tablet during each sync batch. - Updated tangy-form-editor to v7.6.4, which improves functionality of duplicate entire section. PR: #173 - Updates the Schedule View to use date-carousel 5.2.0 which provides unix timestamps instead of date strings. #2589 - Upgrade tangy-form to fix issue causing on-open of first items to not run when proposing changes in an Issue. - Deactivate App.checkStorageUsage if using Sync Protocol 2. This was not compatible and should not run. - Allow projects to disable GPS warming to save on battery with disableGpsWarming in app-config.json. - Add missing import of editor/custom-scripts.js when using editor so Data Dashboards can have imported JS files.

Server upgrade instructions Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Ensure git is initialized in all group folders. \ndocker start couchdb\ndocker start tangerine\ndocker exec tangerine sh -c \"cd /tangerine/groups && ls -q | xargs -i sh -c 'cd {} && git init && cd ..'\"\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.16.0\n# If you are enabling the new mysql module, follow the instructions in `docs/system-administrator/mysql-module.md` to update the config.sh file (steps 1 through 3)\n# If you do not wish APK and PWA archives to be saved, set T_ARCHIVE_APKS_TO_DISK and/or T_ARCHIVE_PWAS_TO_DISK to false.\n# Then return here before starting tangerine\n# Now you are ready to start the server.\n./start.sh v3.16.0\ndocker exec tangerine push-all-groups-views\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.15.6\n# If setting up mysql return to step 5 in `docs/system-administrator/mysql-module.md`\n
"},{"location":"whats-new/#v3158","title":"v3.15.8","text":"
  • New Sync code reduces the number of network requests by disabling server checkpoints. It also supports three new app-config.json options to configure sync parameters that adjust data download size, how much data is written to the local database each batch, and initial data download:
  • batchSize: Number of docs to pull from the server per batch. Increasing this setting will decrease the number of network requests to the server when doing a sync pull. Default: 200
  • writeBatchSize: How many docs to write to the database at a time. If the database crashes, decreasing this option could be helpful. Default: 50
  • useCachedDbDumps: Enables caching of the group database to a file for a single download to the client upon initial device setup. This is an experimental feature therefore it is not enabled by default. (Some server code is also currently disabled.) Those files are stored at data/groups/groupName/client/dbDumpFiles. At this point, you must delete the dbDumpFiles if you wish to update the data in the initial device load. 2560
  • Disable the v3.15.0 update from groups that use sync-protocol 1.
  • Added 2021 to the report year.
  • Added simple network statistics to the device replicationStatus, which is posted after every sync.

Server upgrade instructions Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.15.8\n# Now you are ready to start the server.\n./start.sh v3.15.8\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.15.7\n
"},{"location":"whats-new/#v3157","title":"v3.15.7","text":"

New Features and Fixes - Fixes a bug in the CSV generation code that caused sections of rows in the CSV to output improperly. PR:#2558 - Adds a server config that allows the user to control the string used for variables that are undefined: T_REPORTING_MARK_UNDEFINED_WITH=\"UNDEFINED\" - The default value of the new config file is set to \"ORIGINAL_VALUE\" so existing Tangerine instances will not be effected.

Server upgrade instructions Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

Please add the below line into your config.sh to preserve current behavior (as a workaround for #2564)

T_REPORTING_MARK_UNDEFINED_WITH=\"\"\n

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.15.7\n# Now you are ready to start the server.\n./start.sh v3.15.7\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.15.6\n
"},{"location":"whats-new/#v3156","title":"v3.15.6","text":"

New Features and Fixes - New 'wakelock' feature for sync: When using the sync feature, the screen should not go to sleep or dim, enabling the sync process to proceed. This is especially useful during long sync processes. When you navigate to another page once Sync is complete, the wakeLock feature is disabled. - The Devices listing has a new option, \"View Sync Log\", which enables viewing status of the most recent replication, when available. - Added error messages when internet access drops during a sync. #2540 - Batch size for sync is configurable via pullSyncOptions and pushSyncOptions variable in a group's app-config.json. Default is 200. If the value is set too high, the application will crash.

Server upgrade instructions Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.15.6\n# Now you are ready to start the server.\n./start.sh v3.15.6\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.15.5\n
"},{"location":"whats-new/#v3155","title":"v3.15.5","text":"

Fixes - In CSV output, if a section on a form is opened and then the later skipped, inputs on that skipped section will appear in CSV output as skipped. However, if the section is never opened, the inputs would show up in the CSV as blank values. This fix ensures that these remaining inputs are marked as skipped in CSV output. - Fix sync from breaking when syncing with a group with no data yet. - Improve messaging during sync by removing floating change counts and showing the total number of docs in the database after sync.

Server upgrade instructions Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.15.5\n# Now you are ready to start the server.\n./start.sh v3.15.5\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.15.4\n
"},{"location":"whats-new/#v3154","title":"v3.15.4","text":"

Fixes - Sync: Sites with large datasets were crashing; therefore, we implemented a new sync function that syncs batches of documents to the server. PR: #2532

Server upgrade instructions

# Fetch the updates.\ncd tangerine\ngit fetch origin\ngit checkout v3.15.4\n# Now you are ready to start the server.\n./start.sh v3.15.4\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.15.3\n
"},{"location":"whats-new/#v3153","title":"v3.15.3","text":"

Fixes - After a large sync in sync protocol 2, improve overall app performance by indexing database queries. Because this may cause a long sync for projects not using this, you can set indexViewsOnlyOnFirstSync in app-config.json to true if you want to allow existing tables to avoid this long sync to catch up on views. - Add missing custom-scripts.js and custom-styles.css files to Editor app. We also add editor and client ID's to the body tag of the two app respectively. - Reduce database merge conflicts by preventing form responses from saving after completed. Prior to this version, on two tablets (or on a tablet and the server) if you opened the same form response and opened an item to inspect, it would cause a save on both tablets resulting in an unnesessary merge conflict. - New T.case.getCaseHistory(caseId) function for getting the history of save for a Case. Returns an array of JSON patches in RFC6902 format. Open a Case and run await T.case.getCaseHistory() in the console and it will pick up on the context. - New T.case.getEventFormHistory(caseId, caseEventId, eventFormId) function for getting the history of save for a form response in a Case. Returns an array of JSON patches in RFC6902 format. Open a Case, a Case Event, then an Event Form and run await T.case.getEventFormHistory() in the console and it will pick up on the context. - New opt-in app-config.json setting attachHistoryToDocs for enabling upload all history of Case and Event Form edits on a Tablet up to the Server. Without this setting on, the server only sees the history starting from time of upload. Note this has an impact on upload size of at least doubling it when turned on.

Important configuration notice - Set indexViewsOnlyOnFirstSync in app-config.json to true if you want to allow existing tables to avoid this long sync to catch up on views.

Server upgrade instructions

# Fetch the updates.\ncd tangerine\ngit fetch origin\ngit checkout v3.15.3\n# Now you are ready to start the server.\n./start.sh v3.15.3\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.15.2\n
"},{"location":"whats-new/#v3152","title":"v3.15.2","text":"

Fixes

  • Rshiny module: Replaces hard-coded underscore separator with the configurable sep variable.
  • Error when processing CSV's: 2517

Important configuration notice

The v3.15.0 release included an update to the Editor Search feature #2416 that requires adding a searchSettings property to forms.json. In addition to running the upgrade script for v3.15.0; you must also make sure that all forms in a group's forms.json have searchSettings configured, especially the shouldIndex property. Examples are in the Case Module README \"Configuring Text Search\" section.

Server upgrade instructions

cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.\ndf -h\n# Turn off tangerine and database.\ndocker stop tangerine couchdb\n# Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n# Fetch the updates.\ngit fetch origin\ngit checkout v3.15.2\n# Now you are ready to start the server.\n./start.sh v3.15.2\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.15.1\n
"},{"location":"whats-new/#v3151","title":"v3.15.1","text":"

Fixes

  • Prevent opening of Event Forms on Editor when there is no corresponding Form Response available.
  • Fix Issue type detection when deciding what is going to be in the 'current' tab.
  • Update CSV output for signatures to be 'signature captured' and ''.
  • Fix Issues view causing Issue search result to appear once per event such as comment or proposal.
  • Integrate fixes in v3.14.6 including T.case.isIssueContext() API, and better API partity between being in an Event Form in a Case and being in an Event Form in an Issue.

Server upgrade instructions

# Fetch the updates.\ncd tangerine\ngit fetch origin\ngit checkout v3.15.1\n# Now you are ready to start the server.\n./start.sh v3.15.1\ndocker exec -it tangerine push-all-groups-views  \ndocker exec -it tangerine reporting-cache-clear  \n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.15.0\n
"},{"location":"whats-new/#v3150","title":"v3.15.0","text":"
  • New Features and fixes
  • Editor User searches Cases by keyword #2416 - This feature enables searching by any of the variables assigned in searchSettings/variablesToIndex in forms.json.
  • Transfer Participant between Cases #2419. Find Participant UI: #2439.
  • Update to Content Set 2.1 adds a package.json and build step to pin lib versions and add a build step for custom-scripts.
  • Added error message to Updates error alert. ccc1864
  • New \"Release Online Survey\" menu on Server allows you to release a single form for data collection online. Note the original \"Deploy -> Release\" menu item has been moved to \"Deploy -> Release Offline App\".
  • Fixed issue where \"Tangy Gate\" form element could be added in Editor but would not appear on Tablets.
  • Support for new \"\" element that brings a Partial Date style form element with support for the Ethiopian Calendar.
  • If using Sync Protocol 2, the first sync when registering a Device is now faster in cases where there is a lot of data already collected. Also the blank User Profile created for the Admin user on a device is no longer uploaded resulting in less noise in the Device Users list.

  • Important deprecation notice

  • The groupName property, once used in app-config.json, is no longer supported in recent releases of Tangerine. The groupId property is used in its place. Groups that use groupName will not be able to sync; they must migrate to groupId. This issue affects groups using sync-protocol-1. #2447
  • When form responses are unlocked in a Data Issue, the on-submit hook no longer runs. If you need logic to run, use the new on-resubmit hook.
  • If using Sync Protocol 2, the \"Auto Merge\" feature that tries to fix database conflicts is now off by default and database conflicts will not be logged as Issues. If you would like to keep it on, set \"autoMergeConflicts\": true in your group's client/app-config.json file. However be aware that turning this on will result in inconsistent results (https://github.com/Tangerine-Community/Tangerine/issues/2484). Monitoring for database conflicts can now be done by monitoring the syncConflicts view via CouchDB Fauxton.
  • Server upgrade instructions:

    # Fetch the updates.\ncd tangerine\ngit fetch origin\ngit checkout v3.15.0\n# Now you are ready to start the server.\n./start.sh v3.15.0\n# Update the views - there are new views for Searches and Participant Transfers.\ndocker exec -it tangerine reporting-cache-clear \ndocker exec -it tangerine /tangerine/server/src/upgrade/v3.15.0.js\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.14.6\n

    "},{"location":"whats-new/#v3146","title":"v3.14.6","text":"

    Changes in v3.14.4 were abandoned, changes in v3.14.5 have been rolled into v3.15.0. The following are changes for v3.14.6.

    • Improve first sync performance: On first sync, skip push but set the last push variable to whatever we left on after the first pull.
    • Improve in-form API parity between context of a Case and context of an Issue proposal. Sets case context in more scenarious inside of Issue Form proposals.
    • Prevent form crashes and unintentional logic by adding the new T.case.isIssueContext() API for detecting if in the Issue context in a form.

    Server upgrade instructions:

    # Fetch the updates.\ncd tangerine\ngit fetch origin\ngit checkout v3.14.6\n# Now you are ready to start the server.\n./start.sh v3.14.6\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.14.3\n
    "},{"location":"whats-new/#v3143","title":"v3.14.3","text":"
    • Bugfix
    • Auto-merged conflicts overwrite \"canonical\" change made on Editor server #2441 - Prevents tablets from overwriting documents from Editor in special cases. After modifying the case record, add canonicalTimestamp to the document: \"canonicalTimestamp\":1603854576785
    • New Features and fixes for all Tangerine
    • Reduce number of unnecessary saves in Editor #2444
    • Improvements to Issues Listing #2398 Please update the group views (noted in the Server upgrade instructions below) in order to use the Issues Listing.
    • Upgrades in the Developers' Interest
    • Removed webpack from the Docker image. Custom apps should build their apps using their own webpack; the APK service will no longer perform that task.

    Server upgrade instructions:

    # Fetch the updates.\ncd tangerine\ngit fetch origin\ngit checkout v3.14.3\n# Now you are ready to start the server.\n./start.sh v3.14.3\n# Update the views - there is a new view used for Issues.\ndocker exec -it tangerine push-all-groups-views\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.14.2\n
    "},{"location":"whats-new/#v3142","title":"v3.14.2","text":"
    • Bugfix
    • Fixes file path issue when bundling custom scripts in APK's.

    Server upgrade instructions:

    # Fetch the updates.\ncd tangerine\ngit fetch origin\ngit checkout v3.14.2\n# Now you are ready to start the server.\n./start.sh v3.14.2\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.14.1\n
    "},{"location":"whats-new/#v3141","title":"v3.14.1","text":"

    This is identical to v3.14.0 but was released to fix a problem with tangerine-preview v3.14.0 on npm.

    "},{"location":"whats-new/#v3140","title":"v3.14.0","text":"
    • New Features and fixes for all Tangerine
    • Usability Improvement for Device Registration: Added \"Number of devices to generate\" field to Device Registration. Submitting a single form to add multiple devices to a group should simplify large deployments. #2402
    • Important bugfix for sync issue in poor network situations: If you currently have an active 3.13 deployment, run the 3.14 update on client to make sure all data is sync'd to the server. #2399
    • Automatic conflict resolution on client: Basic support for automatic merges of conflicts in EventForms. #2272 Documentation for testing conflicts
    • Form version support: Enables use of previous form versions for form display. #2365 Support for versioning is not yet implemented in the Editor; however, there is documentation on how to implement form versions manually.
    • User Interface updates: The 4.19.0 tangy-form lib version features the following fixes:
      • Required Field Asterisk (*) does not align with the question text #2363
      • Error Text and Warning Text have the same style - this is confusing for users #2364
    • Setting packageName in app-config.json causes app to crash: The docker-tangerine-base-image update to 3.7.0 improves Android and Cordova lib dependencies, and the release-apk code now rebuilds the Android code whenever an APK is built. #2366
    • New module for rshiny development: Adds option to csv module to change delimiter from '.' to '_'#2314
    • Documentation Update: Re-organization of some documentation and addition of missing image files. #2401
    • Upgrades in the Developers' Interest
    • Upgraded docker-tangerine-base-image to v3.7.1: Upgrade to Android API_LEVEL 30, Cordova 10, node:14.12.0-stretch. #1890 Caching cordova-android platform to avoid network issues when customizing packageName. #7
    • Important note for users of tangerine-preview There was a problem with v3.14.0 on npm; therefore, please use tangerine-preview v3.14.1.
    "},{"location":"whats-new/#v3131","title":"v3.13.1","text":"
    • Fix: Issues on Editor always ask us to rebase #2376
    • Fix: Issues screen will not load after upgrading from v3.10.0 to v3.13.0 #2378
    • Fix: Issues go missing after upgrading to v3.13.0 from v3.12.x #2377
    • Please be aware: this release was made in the release/v3.13.1-alt branch and to date has only been built as the v3.13.1-rc-2 image.

    Server upgrade instructions:

    # Fetch the updates.\ncd tangerine\ngit fetch origin\ngit checkout v3.13.1\n# Now you are ready to start the server.\n./start.sh v3.13.1\n# Run upgrade\ndocker exec -it tangerine /tangerine/server/src/upgrade/v3.13.1.js\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.13.0\n
    "},{"location":"whats-new/#v3130","title":"v3.13.0","text":"
    • New Features and fixes for all Tangerine
    • Download Location List as CSV: You can now download a location list as a CSV. If you prefer editing a Location List via something like Excel, this makes editing an existing location list easier, which can then be imported when done editing in Excel. Note: Advise careful use of this export feature until #2336 is fixed. #2107
    • Duplicate a Section:When editing a form, you can now easily duplicate an entire section with the \"duplicate section\" button. #2109. Warning - this feature does not handle complex objects such as tangy checkbox groups well; be sure to check the code it generates. This issue will be addressed in the next 3.13 point release.
    • Group Data Dashboard in Editor: \"Dashboard\" is now a menu item available in a Group under the Data menu. This link can be enabled by group role (disabled by default). When on the Dashboard page, it displays a customizable dashboard for that specific group. Customizing Dashboards currently requires HTML and Javascript knowledge but in the future we may build a configurator for Dashboards.
    • More Menu Permissions in Editor:Add additional Editor permissions to completely cover menu level access in a group
    • Automatic conflict resolution: After a sync pull on client, detects type of conflict and resolves it. View status of merges in the Issues feature. #1763
    • Fix extending session in Editor - When prompted to extend session shows up session is not really extended. #2266
    • New Features and Fixes for Case Module
    • Client \"Issues\" feature: \"Issues\" previously could only be viewed using Editor. With this release, Issues can now be accessed from Client in a Case module enabled Group via the top level \"Issues\" tab. This tab can be disabled adding or modifying \"showIssues\": false, to app-config.json. Note that only issues created targeting the \"CLIENT\" context (See CaseService API documentation) will show up in the Client \"Issues\" tab.
    • Easier searching on Client: Previously on Client when searching for \"Facility 8\" you would need to type exactly \"Facility 8\". Now search is case insensitive and you may type \"facility 8\" to match against \"Facility 8\".
    • T.case.setEventWindow API fix: Previously when setting an Event window, the end time for the window was mistakenly ignored and set to the start time. This is now fixed. #2304
    • New Features for Sync Protocol 2 Module
    • Export device sheets: When registering Devices, we now offer an option to print \"Device Sheets\". Device Sheets include the registration codes for a Device and also some human readable metadata. Each row can also be used as a label for each device that can be fastened to a device using affordable clear packing tape. #2269
    • Restore Backup on Android Tablet: Backups can now be restored. Restore is an option when first opening a freshly installed APK. #2127
    • Better support for working on the same Case on two devices: When working offline on the same Case on two Devices, after a sync, it may seem like the changes on one Tablet have gone missing for some time until the \"database conflicts\" are resolved using the CouchDB Futon interface on the server. Starting in v3.13.0 we'll start to employ algorithms for automatically merging to speed up the process of resolving these database conflicts.
    • Notes for System Administrators
    • After upgrade, you will no longer find group content directories in ./data/client/content/groups/, they will be in ./data/groups/. Inside each group's directory you will also find they have been split into a client and editor directory. All previous content will now be in the client directory while you may place content for the Group's Data Dashboard in the editor folder.

    Server upgrade instructions:

    This update changes the path to group content to /tangerine/groups/${groupId}/client. If your group is managing content via a Github/cron integration, you will need to change the path to content in its cron job. Change GROUP-UUID to your group id in the following command:

    cd /home/ubuntu/tangerine/data/groups/GROUP-UUID/client && GIT_SSH_COMMAND='ssh -i /root/.ssh/arc-forms-dev' git pull origin master && git add . && git commit -m 'auto-commit' && GIT_SSH_COMMAND='ssh -i /root/.ssh/arc-forms-dev' git push origin master\n

    The update:

    # Fetch the updates.\ncd tangerine\ngit fetch origin\ngit checkout v3.13.0\n# Now you are ready to start the server.\n./start.sh v3.13.0\n# Run upgrade\ndocker exec -it tangerine /tangerine/server/src/upgrade/v3.13.0.js\n# Add or modify `\"showIssues\": false,` to the group's app-config.json if you do not want to display the Issues tab in client.\n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.12.0\n
    "},{"location":"whats-new/#v3125","title":"v3.12.5","text":"
    • Fixed issue with black screen when moving from p2p tab to home
    "},{"location":"whats-new/#v3124","title":"v3.12.4","text":"
    • Fixed issue with QR code boundary boxes in tangy-form #158. Bumped tangy-form v4.17.10 and tangy-form-editor to 7.2.5
    "},{"location":"whats-new/#v3123","title":"v3.12.3","text":"
    • Fixed issue with mutually exclusive checkboxes in tangy-form #154. Bumped tangy-form v4.17.9 and tangy-form-editor to 7.2.3
    "},{"location":"whats-new/#v3122","title":"v3.12.2","text":"
    • When saving edits to a form, \"Show if\" logic has been written as tangy-if logic in the HTML. Form now on it will be written as show-if logic for consistency.
    • tangy-if logic in has in the past in just showing/hiding an question on a form. It will now also reset the value if there is input and it is then hidden.
    • In v3.12.0, caseService.getCurrentCaseEventId() was incorrectly removed. It has been added back, and an additional caseService.getCurrentEventFormId() function has been added for consistency.
    • Fix Android 10 compatibility issue with P2P Sync mechanism causing tablets to crash.
    "},{"location":"whats-new/#v3121","title":"v3.12.1","text":"
    • Change behavior of show-if logic so that when a question hides, the value is reset.
    • Adjust behavior of how Event Forms are added: If EventForm.autoPopulate is left undefined and required is true, then the form should be added.
    "},{"location":"whats-new/#v3120","title":"v3.12.0","text":"
    • New Features for Case Module
    • Data Collector finds Event Forms are automatically created on Case Event creation and after adding a Participant #2147 [Demo]
    • Data Collector has found a non required form has become required #2233
      • Demo Part 1: https://youtu.be/dnJk4LaGuQw
      • Demo Part 2: https://youtu.be/I0JOZounZc4
    • Data Collector finds Case Event is automatically marked as complete #2235 [Demo]
    • Data Collector sees indicator on Event Form when corresponding Form Response has not been synced to a device #2232 [Demo]
    • Data Collector views a dedicated page for a Participant's Event Forms for a specific Case Event #2236 [Demo]
    • Data Collector is redirected to custom route after Event Form is submitted #2237 [Demo]
    • Fixes for Case Module
    • Device User registering only sees user profiles they can associate with restricted by location the Device is assigned #2248
    • When all optional and incomplete forms are removed (no required forms in the event) from an event on the client the + button is not shown to re-add any of them #2113
    • Delete an incomplete form from a case does not refresh the screen #2114
    • Fixes for all of Tangerine
    • Autostop is not triggered when marking the entire lineas incorrect #1869
    • Mark entire line of grid as incorrect cannot be undone #1651
    • Meta data print screen Prompt and Hint are not displayed for Radio Buttons (single type) #1748
    • Form Metadata view of Checkboxes with one option is missing #2239
    • New features for Sync Protocol 2
    • Restore encrypted backup on Device #2127

    • API Changes for Case Module

    • caseEvent.status is now caseEvent.complete which has a value of true or false as opposed to the status strings.
    • caseService.startEventForm(...) is now caseService.createEventForm(...).
    • caseService.deleteEventFormInstance(...) is now caseService.deleteEventForm(...).
    • caseService.getCaseEventFormsData(...) is now caseService.getEventFormData(...).
    • caseService.setCaseEventFormsData(...) is now caseService.setEventFormData(...).

    Server upgrade instructions:

    # Fetch the updates.\ncd tangerine\ngit fetch origin\ngit checkout v3.12.0\n# Now you are ready to start the server.\n./start.sh v3.12.0\n# Run upgrade\ndocker exec -it tangerine reporting-cache-clear \n# Remove Tangerine's previous version Docker Image.\ndocker rmi tangerine/tangerine:v3.11.0\n
    Note that after running the upgrade script, your reporting caches may take some time to finish rebuilding.

    Android upgrade instructions: If you are upgrading an Android device that was installed with Tangerine v3.8.0 or greater, you will need to regenerate your APK and reinstall, otherwise you may use the over the air updater.

    "},{"location":"whats-new/#v3110","title":"v3.11.0","text":"
    • New Features in all Tangerine
    • Device Manager installs many Tangerine APKs on a single device #2182
    • CSV output enhancements:
      • Editor User indicates whether to include PII in CSV export #1771
      • User profile information available in CSV export #2081
      • Editor User exports CSV file that contains the group and form name #2108
    • New 'T' namespace for helper functions #2198
    • New Features in Tangerine with Case Module enabled
    • Data Collector changes location of Case #2098 [demo]
    • Data Collector views an alert #2020 [demo]
    • Data Collector views custom report #2143 docs
    • Developer notes
    • Group permissions #2187

    Server upgrade instructions:

    # Fetch the updates.\ncd tangerine\ngit fetch origin\ngit checkout v3.11.0\n# Now you are ready to start the server.\n./start.sh v3.11.0\n# Run upgrade\ndocker exec -it tangerine /tangerine/server/src/upgrade/v3.11.0.js\n
    Note that after running the upgrade script, your reporting caches may take some time to finish rebuilding.

    Android upgrade instructions: If your groups are using Sync Protocol 2 module, an APK reinstall is required. Release the APK and reinstall on all Android Devices. If your groups are not using Sync Protocol 2, you may upgrade Android tablets over the air using the usual release process.

    "},{"location":"whats-new/#v3100","title":"v3.10.0","text":"
    • New Features in all Tangerine
    • Editor User updates own profile and/or password #2166 [demo]
    • Editor User with appropriate permission manages site level permissions of users and edits details and password of user #2155 [demo]
    • Editor User views question configuration by category (as opposed to long list) #2097 [demo]
    • Server Admin creates group on command line with local or remote content set #2174
      • Demo: Server Admin creates group on command line from local or remote content set
      • Demo: Server Admin creates group on command line from content set in a private repository on Github
    • New Features in Tangerine with Case Module enabled
    • Tangerine User views form response in alternative templates #2176 [demo]
    • Data Manager manages Data Issues #1982 [demo]
    • Data Manager rebases proposed changes in Issue #2179 [demo]
    • Data Manager creates Data Issue #2144 [demo]
    • Data Collector causes Issue to be created due to use of API in the form #2145 [demo]
    • Data Collector causes Issue to be created due to discrepancy-if logic in form #2171 [demo]
    • Synapse consumes structured outputs based on Case ER Diagram #2051
    • New features in Tangerine with Sync Protocol 2 Enabled
    • Improve two-way sync efficiency in PouchDB by using doc_ids filter as opposed to mango query #2040
    • Developer notes
    • Resolve problems with client compilation in Angular #2091

    Upgrade instructions:

    # Fetch the updates.\ncd tangerine\ngit fetch origin\ngit checkout v3.10.0\n# Now you are ready to start the server.\n./start.sh v3.10.0\n# Run upgrade\ndocker exec -it tangerine /tangerine/server/src/upgrade/v3.10.0.js\n

    "},{"location":"whats-new/#v391","title":"v3.9.1","text":"
    • Fixes
    • Database views are missing when running tangerine-preview or npm start #2096
    • Event Schedule day view duplicates day event and show it in previous day as well #2103
    • According to date carousel, events appear off by one week #2094
    • Event Schedule templates are failing #2085
    • Reports form is not added to forms.json #2088
    • Events appear off by one day in Schedule List #2101
    • CouchDB port should be configurable in config.sh #2092
    • When opening the schedule view the first page is missing the header dates #2082
    • Data Collector unable to open an Event from the Event Schedule #2102
    • When editing a radio button options in editor, options should be in one column, not two #2090
    "},{"location":"whats-new/#v390","title":"v3.9.0","text":"
    • Features
    • Set and get properties for Case Event Forms #2023
    • Data Manager reviews Cases PR
    • Data Collector removes Event Form. PR
    • Fixes
    • Fix additional memory leaks in Case module causing tablets to slow down. PR
    • Make getValue() function in Event Form List Item related templates less likely to crash. change
    • Fixed incompatibilities with 2-way sync and P2P Sync.
    • Fixed issue causing tablets to crash when syncing with a database with tens of thousands of records.
    • Developer notes
    • Editor and Clients upgraded to Angular 8.
    • Changes
    • Due to current limitations of two way sync, two changes have been made to the Device form in Tangerine Editor. First, changing a Device's assigned location and sync settings after the Device record has been claimed will no longer be allowed. Second, devices will now always be required to be assigned to the last level in your location hierarchy.

    Upgrade instructions:

    When you run the upgrade script, if you are using sync protocol 2 and have enabled, forms configured for 2 way sync will now be configured to use CouchDB sync to push documents up as opposed to \"custom sync\".

    # Fetch the updates.\ncd tangerine\ngit fetch origin\ngit checkout v3.9.0\n# If you did not upgrade your config.sh in v3.8.1, migrate it now.\n# Move custom variables from config.sh_backup to config.sh. Note that T_ADMIN and T_PASS are no longer needed. \nmv config.sh config.sh_backup\ncp config.defaults.sh config.sh\n# To edit both files in vim you would run...\nvim -O config.sh config.sh_backup\n# Now you are ready to start the server.\n./start.sh v3.9.0\n# Run upgrade\ndocker exec -it tangerine /tangerine/server/src/upgrade/v3.9.0.js\n

    "},{"location":"whats-new/#v381","title":"v3.8.1","text":"
    • Client app performance improvements
    • Improved caching of files. We are caching important configuration files for faster page loads (app-config.json, forms.json, location-list.json) and the Roboto font and have reduced redundant rendering calls. 1991
    • Loading spinner when opening an Event Form in a Case. #1992
    • Fixed a memory leak when viewing a Case which was causing tablets to crash if spending too much time on a Case screen. #2000
    • Radiobuttons now load faster on forms.
    • Editor fixes
    • Fixed an issue causing editor content region to be untouchable when window was narrow. #1940
    • Improved CSV output so it now contains Release ID, Device ID, and Build Channel on every row #349
    • Developer notes
    • We focused on issues with slow performance on tablets when viewing forms. We are caching important configuration files (app-config.json, forms.json, location-list.json) and the Roboto font and have reduced redundant rendering calls. More information in the Globals doc.
    • Server Admin notes
    • We cleaned up config variables in config.sh, deprecated T_ADMIN and T_PASS #1986
    • New generate-cases command for load testing a large number of Cases based on your custom content in a group. #1993
    • New reset-all-devices command for reseting the server token and database keys for all devices. Note that after running this command you will need to reinstall on all devices and reregister with new QR codes. This command is useful if you are migrating a large amount of devices to a new group or a new server and you want to maintain Device ID consistency with the Device Serial numbers you are tracking.

    Upgrade instructions:

    # Fetch the updates.\ncd tangerine\ngit fetch origin\ngit checkout v3.8.1\n# Now migrate custom variables from config.sh_backup to config.sh. Note that T_ADMIN and T_PASS are no longer needed. \nmv config.sh config.sh_backup\ncp config.defaults.sh config.sh\n# To edit both files in vim you would run...\nvim -O config.sh config.sh_backup\n# Now you are ready to start the server.\n./start.sh v3.8.1\n

    "},{"location":"whats-new/#v380","title":"v3.8.0","text":"

    v3.8.0 is a big and exciting release! To accomodate the long list of changes, we split up this round of release notes into sections: General, Sync Protocol 2 Module, and Case Module, and Developer notes.

    "},{"location":"whats-new/#general","title":"General","text":"

    The following are features and fixes that are coming to all Tangerine installs. With this release comes an improved Editor UI experience, a faster device setup process, new form features, and much more.

    Group tabs are now in 4 sections Breadcrumbs allows you to navigate back up deeply nested areas
    • Editor User browses Group UI by nested categories (as opposed to flat list) #1880
    • Device Administrator is prompted to authorize permissions on first app load #1896
    • Data Collector defines password according to policy #1867
    • Data Collector views device info such as Device ID, Assigned Location, Server URL, Group Name, and Release Channel. #1834
    • Data Collector in checkboxes chooses \"none of the above\", then other options are unselected #1822
    • Editor distinguishes between inputs that are hidden and skipped #1800
    • Minor tweaks to the menu (now there is a single \"Sync\" item) and added tab bars to some pages for consistency.
    "},{"location":"whats-new/#sync-protocol-2-module","title":"Sync Protocol 2 Module","text":"

    Sync Protocol 2 is a new module that can be enabled on a Tangerine installation that adds Device management, the ability for form responses to sync to the server and back down to tablets, the ablity for two tablets to sync form responses with each other offline, and much more.

    Manage which devices have access to sync, when they last synced, when they last updated and which version Define which form responses are synced up and back down to tablets
    • Data Collector generates encrypted backup of Device #1909
    • Data Collector conducts a two-way sync with server only getting data from server relevant to their location #1755
    • Device sync by Location: Sync Protocol 2: Enables a \"Device Setup\" process on first boot of the client application. This requires you set up a \"Device\" record on the server. When setting up a Device record on the server, it will give you a QR code to use to scan from the tablet in order to receive it's device ID and token.
    • Data Collector syncs to server with large dataset #1757
    • Data collector synchronizes data between devices using an Offline P2P mechanism #279
    • Editor User configures two-way sync for form responses from specific forms #1753
    • Editor revokes access to syncing with server for a lost Device #1894
    "},{"location":"whats-new/#case-module","title":"Case Module","text":"
    • Data Collector views Case Events in Schedule with Estimated Day, Scheduled Day, Window, and Occurred On Dates #1737
    • Data Collector creates (another) instance of a repeatable form for a specific participant in a specific event(8hrs) #1786
    • Data Collector views which Participant they are filling out a form for #1820
    • Data Collector searches for a Case in a large dataset #1893
    • Improvements to Case Home search - limit docs to 25 when no phrase is entered: #1871. Added rule to delay search in Case Home until at least two characters have been entered. Search results now sorted by date record updated.
    • Lazy loading tabs in Case Home - this helps resolve some of the slowness in loading Case Home. Also disabled animations on tabs to remove jankiness.
    "},{"location":"whats-new/#developer-updates","title":"Developer Updates","text":"
    • Re-enabled git config in Dockerfile - still having git networking error even when off corp network.
    • Updated docker-tangerine-base-image to v3.4.0
    • New load testing doc.
    • Added random name generation to the script that generates new cases - useful for load testing and checking how well search listing works. If using the 'case-mother' switch, record templates are pulled from your group.
    "},{"location":"whats-new/#upgrade-instructions","title":"Upgrade instructions","text":"

    On the server, backup your data folder and then run the following commands.

    git fetch origin\ngit checkout v3.8.0\n./start.sh v3.8.0\ndocker exec -it tangerine /tangerine/server/src/upgrade/v3.8.0.js\n

    Replace all ocurrences of localStorage.getItem('currentUser') with window.currentUser.

    "},{"location":"whats-new/#v372","title":"v3.7.2","text":"
    • More fixes for upgrade process from v3.1.0.
    "},{"location":"whats-new/#v371","title":"v3.7.1","text":"
    • Fix translations update script.
    • Fix client update process when upgrading from v3.1.0.

    Upgrade instructions: On the server, backup your data folder and then run the following commands.

    git fetch origin\ngit checkout v3.7.1\n./start.sh v3.7.1\ndocker exec tangerine translations-update\n

    "},{"location":"whats-new/#v370","title":"v3.7.0","text":"
    • Fixes
    • When editing forms, they will only save back to the server after clicking the top level \"save\" button. There is also now messaging around when the save either completes successfully or fails.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1645
    • On <tangy-timed> when using auto stop, return the property instead of the instead of the truthfulness of the value which is always false.
      • PR: https://github.com/Tangerine-Community/tangy-form/pull/110
    • When uploadUnlockedFormReponses is set to true only incomplete forms are Synced up.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1725
    • In editor, modifying allowed pattern on text and number inputs does not work.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1770
    • Fix spacing between checkboxes in client
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1690
    • Fix click target and style for Case Event Form list
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1681
      • PR: https://github.com/Tangerine-Community/Tangerine/pull/1702
    • Fix Partial Date validation
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1683
      • PR: https://github.com/Tangerine-Community/tangy-form/pull/71
    • EF Touch changes
      • <tangy-eftouch multi-select go-next-on-selection=\"2\"> should become <tangy-eftouch multi-select=\"2\" go-next-on-selection>. This allows for expanding functionality of being able to use multi-select without go-next-on-selection but still limit the number of choices the user can make minus the transition.
      • no-corrections has been deprecated for new disable-after-selection attribute. When used with multi-select, the number of selections are still limited by the setting on multi-select, but changing selection is not allowed.
      • The required attribute when used with multi-select will only require just one value selected. If you need form example 2 selections to be valid, you can combine required-all multi-select=\"2\".
      • We have an API change where we used to have TangyEftouch.value.selection was sometimes a string when not using multi-select and then when using multi-select, is was an array of strings. Now TangyEftouch.value.selection will always be an array of strings.
    • Features
    • When editing forms, the user will be warned of any duplicate variable names that exist in the form.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1793
    • Improve messaging when an APK update fails to download
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1743
      • PR: https://github.com/Tangerine-Community/Tangerine/commit/2ede9d3fb9d43dda234bfdcfc4849769b9b08e69
    • Data Collector sends SMS message from form
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1745
    • Data Collector views events in schedule with icons, estimated date info, and scheduled date info
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1686
    • Data Collector views Case Module screens in French
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1711
    • Data Collector confirms case when opened
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1695
      • PR: https://github.com/Tangerine-Community/Tangerine/pull/1741
    • Improved support for changing color scheme of client app using custom-styles.css, possible to have \"dark mode\".
      • PR: https://github.com/Tangerine-Community/Tangerine/pull/1742
    • Data Collector shares all data on Device with other Users on the same Device.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1712
      • PR: https://github.com/Tangerine-Community/Tangerine/pull/1709
    • Data Collector finds Case Event status has changed to \"complete\" when all required forms are submitted.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1693
      • PR: https://github.com/Tangerine-Community/Tangerine/pull/1719
    • Data Collector finds all required Event Form instances in a Case Event are created upon opening the Case Event.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1691
      • PR: https://github.com/Tangerine-Community/Tangerine/pull/1718
    • Data Collector registers a Participant in a Case and views Event Forms grouped by Participant
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1692
      • PR : https://github.com/Tangerine-Community/Tangerine/pull/1723

    Upgrade instructions: On the server, backup your data folder and then run the following commands.

    git fetch origin\ngit checkout v3.7.0\n./start.sh v3.7.0\ndocker exec tangerine translations-update\n

    "},{"location":"whats-new/#v365","title":"v3.6.5","text":"
    • Fix timed grid output to exclude item level variables in logstash output https://github.com/Tangerine-Community/Tangerine/pull/1806

    Upgrade instructions: After the usual upgrade commands, also clear reporting caches with docker exec -it tangerine reporting-cache-clear.

    "},{"location":"whats-new/#v364","title":"v3.6.4","text":"
    • Fix usage of T_CSV_MARK_DISABLED_OR_HIDDEN_WITH in some cases.
    "},{"location":"whats-new/#v363","title":"v3.6.3","text":"
    • Allow disabled or hidden inputs output in CSV to be overridden using CSV_MARK_DISABLED_OR_HIDDEN_WITH in config.sh. The default value in config.defaults.sh is \"999\" which is what it has been for a few releases. When upgrading, do nothing if you want this to stay the same, otherwise use \"ORIGINAL_VALUE\" if you want to turn off the feature or set to your own custom value such as \"SKIPPED\".
    "},{"location":"whats-new/#v362","title":"v3.6.2","text":"
    • Fix import of location list from CSV https://github.com/Tangerine-Community/Tangerine/pull/1732/commits/05e57e8f1bb869dbd52b927d45fc223903e201db
    "},{"location":"whats-new/#v361","title":"v3.6.1","text":"
    • Fix form routing for archived and active forms.
    • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1722
    • PR: https://github.com/Tangerine-Community/Tangerine/pull/1724
    • Fix \"Mark entire line as incorrect in grids is not reflected in csv #1713\"
    • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1713
    • PR: https://github.com/Tangerine-Community/tangy-form/pull/103
    "},{"location":"whats-new/#v360","title":"v3.6.0","text":"
    • New Features
    • Support for changing the order of forms.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1523
      • PR: https://github.com/Tangerine-Community/Tangerine/pull/1707
    • Support for archiving a form.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1526
      • PR: https://github.com/Tangerine-Community/Tangerine/pull/1675
    • Improvements and support on all inputs for error-text, hint-text, question-number, and content translations.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1655
      • PR: https://github.com/Tangerine-Community/tangy-form/pull/88, https://github.com/Tangerine-Community/tangy-form/pull/86
    • Add support to <tangy-qr> for scanning data matrix codes.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1653
      • PR: https://github.com/Tangerine-Community/tangy-form/pull/87
    • New \"Capture Item at N Seconds\" feature for <tangy-timed> will prompt Data Collector to mark which item the child last read after a specific amount of time.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1586
      • PR: https://github.com/Tangerine-Community/tangy-form/pull/95
    • New goTo('itemID') helper function to navigate users to a specific item given some item level on-change logic.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1652
      • PR: https://github.com/Tangerine-Community/tangy-form/pull/92
    • New <tangy-signature> input for capturing signatures.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1656
      • PR: https://github.com/Tangerine-Community/tangy-form/pull/90
    • Visibility of labels and/or icons on item navigation now configurable with <tangy-form-item hide-nav-icons> and <tangy-form-item hide-nav-labels>.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1682
      • PR: https://github.com/Tangerine-Community/tangy-form/pull/73
    • Fixes
    • Fix Class tablets that are filling up their disk too fast.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1706
    • Fix metadata print screen options
      • PR: https://github.com/Tangerine-Community/Tangerine/pull/1703
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1670, https://github.com/Tangerine-Community/Tangerine/issues/1671
    • Fix missing camera permission blocking APK installs form using QR or Photo Capture
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1646, https://github.com/Tangerine-Community/Tangerine/issues/1578
    • Fix performance issues caused by needless TangyForm.on-change events from firing when they don't need to.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1656
      • PR: https://github.com/Tangerine-Community/tangy-form/pull/89
    • Fix data collector reviews completed fullscreen form
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1629
      • PR: https://github.com/Tangerine-Community/tangy-form/pull/75
    • <tangy-eftouch auto-progress> now distinguishes between going next on the time limit and going next on a number of selections. The API is now <tangy-eftouch go-next-on-selection=2> for going next on 2 selection and <tangy-eftouch go-next-on-time-limit> for going next on the time limit.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1597
      • PR: https://github.com/Tangerine-Community/tangy-form/pull/84
    • <tangy-eftouch> content is now more likely to fit above the fold, not overlap with content above it, be more consistent on smaller screens, and also adapt to screen size changes.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1591, https://github.com/Tangerine-Community/Tangerine/issues/1587
      • PR: https://github.com/Tangerine-Community/tangy-form/pull/79
    • <tangy-eftouch> suffered from going to next item twice due to time limit and selection being made at in a close window. This is now fixed.
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1596
      • PR: https://github.com/Tangerine-Community/tangy-form/pull/76
    • Fix Partial Date validation and for disabled attribute not reflecting
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1683
      • PR: https://github.com/Tangerine-Community/tangy-form/pull/71
    • Fix variable names in Editor to allow for only valid variable names. 2 or more characters, begin with alpha, no spaces, periods, allow _ no dash
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1566, https://github.com/Tangerine-Community/Tangerine/issues/1558, https://github.com/Tangerine-Community/Tangerine/issues/1461
      • PR: https://github.com/Tangerine-Community/tangy-form-editor/pull/77
    • Fix for Autostop for radio buttons -
      • Issue: https://github.com/Tangerine-Community/Tangerine/issues/1519
      • PR's:
      • https://github.com/Tangerine-Community/Tangerine/issues/1590
      • https://github.com/Tangerine-Community/tangy-form/pull/100
      • https://github.com/Tangerine-Community/tangy-form/pull/100
    • Experimental Features
    • When using the experimental Case module, Editors can now program forms to trigger the creation of a \"Data Query\" when Data Collectors are entering data. Data queries are then shown later in a \"Data Queries\" tab where clarification on prior data entered is requested.
      • PR: https://github.com/Tangerine-Community/Tangerine/pull/1661

    Upgrade instructions:

    On the server, backup your data folder and then run the following commands.

    git fetch origin\ngit checkout v3.6.0\n./start.sh v3.6.0\n

    Now you may publish a release to your Devices and run the \"Check for Update\" on each Device. Note that if you are looking to use the QR Code scanner and you have been using Android Installation, you will need to reinstall the App on Devices and make sure to note the additional permissions installation instructions noted in the README.md file for enabling the App to have Camera Access. If using the Web Browser Installation, there is no need to reinstall the app for Camera access.

    "},{"location":"whats-new/#v350","title":"v3.5.0","text":"
    • New Features
    • Forms with fullscreen enabled now have a toggle button for the user to enable/disable fullscreen mode. Form designers may specify the number of taps in order for fullscreen to disable. https://github.com/Tangerine-Community/tangy-form/pull/51, https://github.com/Tangerine-Community/tangy-form/pull/72, https://github.com/Tangerine-Community/tangy-form-editor/pull/73
    • An inputs object keyed by input name is now available for use in valid-if statements. https://github.com/Tangerine-Community/tangy-form/pull/65
    • A new Partial Date item is available https://github.com/Tangerine-Community/tangy-form/pull/57
    • Translations updates. #1613
    • New custom-styles.css file which can be added by modifying a group's assets folder. You may define CSS classes and then utilize them in the editor by adding them under each widget's class attribute.
    • New \"Copy form\" feature added to to Editor and more descriptive icon for adding a database record #1627
    • Fixes
    • Helper functions for timed grids are now safer, will not crash if a grid was skipped and info is not availble. https://github.com/Tangerine-Community/tangy-form/pull/61
    • Print view for a form had a bug where only the first page was printable. This is now fixed so that all pages may be printed. https://github.com/Tangerine-Community/Tangerine/pull/1605
    • Fix tangy-select test regression and work on EFTouch transition sound plays only on auto-progress #137
    • API change in tangy-select - use of secondaryLabel is supported but deprecated; Use optionSelectLabel instead. #1602
    • Fix the display of uploaded docs #1609
    • Enable auto-stop for untimed grids #1522
    • Increased clickable target for forms list and visits tab #1628

    Upgrade instructions:

    Backup your data folder and then run the following commands.

    git fetch origin\ngit checkout v3.5.0\n./start.sh v3.5.0\ndocker exec -it tangerine /tangerine/server/src/upgrade/v3.5.0.js \n

    If any of your on-change logic looks into a form item's contents using tangyFormItemEl.shadowRoot.querySelector(...) or this.$.content.querySelector(...), you must change it. The contents of the form can now be accessed at tangyFormItemEl.querySelector(...).

    Also, the content element is no longer available.

    For example:

    // replace\nvar el = this.$.content.querySelector('tangy-input[name=\\'classId\\']')\n//with \nvar el = this.querySelector('tangy-input[name=\\'classId\\']')\n

    The advantage of moving this content out of the shadow DOM is that you can now style it directly from your app.

    "},{"location":"whats-new/#v340","title":"v3.4.0","text":"
    • New Features
    • New groups now ordered by creation date: Creating new groups will now order them by the date the were created in the group list. #1584
    • Configurable Web App Device Orientation: You can now specify the Web App orientation (portrait, landscape, or any) on device using the T_ORIENTATION variable in config.sh. Add T_ORIENTATION=\"any\" to config.sh to have more flexible orientations for PWA's. The options for T_ORIENTATION are at https://developer.mozilla.org/en-US/docs/Web/Manifest/orientation
    • Media Library and Image support for Forms: Each group now has a media library tab where they can uplaod images which can then be utilized when inserting the new \"Image\" item on forms. #1138
    • New ACASI widget: The ACASI widget is braodly based on the EFTouch widget, but focused on a more static presentation of images and sounds. #56
    • Configurable font size in grids: You may now configure the font size in tangy-timed and tangy-untimed grids using the Option Font Size input. In tangy-form, it is exposed as option-font-size. Example of generated code: <tangy-timed required columns=\"3\" duration=80 name=\"class1_term2\" option-font-size=\"5\">
    • Auto-stop for tangy-radio-buttons: Add support for autostop in tangy-radio-buttons #49. In Editor, set the Threshold to the number of incorrect answers: screenshots. Autostop is implemented by using the hideInputsUponThreshhold helper, which takes a tangy-form-item element and compares the number of correct radio button answers to the value in its incorrect-threshold attribute. Example of generated code: <tangy-form-item id=\"item1\" incorrect-threshold=\"2\">
    • New \"correct\" attribute for radio button options: A new \"correct\" attribute has been added to tangy-list-item to store the correct value. There is a \"Correct\" checkbox next to each option. Example of generated code: <tangy-radio-buttons name=\"fruit_selection2\" label=\"What is your favorite fruit?\"> <option name=\"tangerine\">Tangerine</option> <option name=\"cherry\" correct>Cherry</option> </tangy-radio-buttons>
    • Fixes
    • Critical Sync and \"data loss\" fix: Some variants of v3.3.x saw cases where data seemed to be lost on the tablet and sync no longer worked. After this release is deployed to the server, release for your groups and instruct all tablets to upgrade. The upgrade process may take many minutes depending on the amount of data stored on the tablet due to a schema update in the database. For an in depth look at what this update does, see the code here.
    • Logstash Improvements #1516
      • User profiles were in a nested object, now they have been merged to be flat in the logstash output doc. See example here.
      • If a form response uses a location element, it will now be extracted out into a top level \"geoip\" property whose value is an object with \"lat\" and \"lon\" properties. See example here.
      • When new forms are created in the editor, they will no longer have a . character in their ID. This was causing some uneccessary and confusing logic in logstash config files. See PR here.
    • EFTouch: A large number of fixes have been made for EFTouch. See recent issues here.
    • Updated to tangy-form-editor ^5.18.0 for Change grid variables in CSV starting with variable_0 to variable_1.
    • A previous update to tangy-form to 3.15.1, tangy-form-editor to 5.17.0 to fixed Editing form level HTML requires two Save clicks
    • Beta Features
    • Two-way Sync: Allows for two-way sync of form responses. Can be configured to two way sync form responses for specific forms and also by geographic region defined in the user profile. See docs/feature-two-way-sync.md. and Add a tangy input inside a tangy box duplicates items, and enable Adjustable letter size for grids
    • Case Module
      • Add the \"case\" module to T_MODULES in config.sh and the default landing page for a group will be the cases search page and new \"Case Management Editor\" tab will appear in groups for creating and editing Case Definitions. #1517
      • Clientside search of Forms for Case Management Groups allows Cases to be found using the device camera to scan a QR code. See docs/case-management-group.md.
      • Add event time and scheduling to Case Mangement Groups #1518
      • New layout for Case and Case Event pages.

    Upgrade instructions:

    Backup your data folder and then run the following commands.

    git fetch origin\ngit checkout v3.4.0\n

    ./start.sh\n
    "},{"location":"whats-new/#v331","title":"v3.3.1","text":"

    This release fixes a feature that made it into v3.3.0 but had a bug and was disabled. This release fixes that bug and makes it available.

    • As an Editor user I want to be able to do an initial import of my location structure. #1117
    "},{"location":"whats-new/#v330","title":"v3.3.0","text":"
    • Features
    • Assessor reviews high level case variables, AKA \"Case Manifest\" #1399
    • Assessor changes language setting to Russian #1402
    • Untimed Grid subtest #1366
    • Editor Style Upgrades (April 2019) #1421
    • Group Names can now have spaces and special characters #1424
    • Editor configures Timed Grid to show or hide labels on buttons #1432
    • Server Admin tunes the reporting delay between when an upload occurs and it shows up in reporting outputs #1441
    • CSV output for single checkboxes now show up as \"0\" and \"1\" as opposed to \"\" and \"on\" #1367
    • CSV output for single radiobuttons now show up as \"0\" and \"1\" as opposed to \"null\" and \"on\" #1433
    • You can now limit who can add/see sitewide users to only the USER1 account by setting T_USER1_MANAGED_SERVER_USERS to \"true\" in config.sh #1381.
    • Client now has an \"About\" page with details about what Tangerine is #1465.

    Upgrade instructions:

    Backup your data folder and then run the following commands.

    git fetch origin\ngit checkout v3.3.0\n./start.sh\ndocker exec -it tangerine /tangerine/server/src/upgrade/v3.3.0.js \n

    "},{"location":"whats-new/#v320","title":"v3.2.0","text":"
    • Features
    • Assessor changes language of App #1315
    • Editor provides feedback given data entered earlier in the form #1384
    • Assessor starts new Case is immediately forwarded to first form #1362
    • Assessor finds Form and Event in Case has been disabled/enabled due to custom logic #1363
    • Assessor confirms participant info using data from another form #1385
    • Server Admin restarts machine to find containers have automatically come back up #1388
    • Server Admin sets up Tangerine outage alarm #1389
    • Developer Notes
      • Ability to define database views on a per module basis in Client Angular #1419
      • Integrate test harness and TypeScript with server using NestJS #1413
      • Fix client tests, organize shared services and guards into the shared module, move client/app/ to client/ #1398

    Upgrade instructions:

    git fetch origin\ngit checkout v3.2.0\n./start.sh\ndocker exec tangerine /tangerine/upgrades/v3.2.0.js \n
    - In each group's app-config.json, change \"direction\" to \"languageDirection\". - If using a translation other than English, change in each group's app-config.json, change \"languageCode\" to the corresponding language code. Current codes other than en for English is JO_ar for Jordanian and KH_km for Khmer.

    "},{"location":"whats-new/#v310","title":"v3.1.0","text":"
    • Features
    • Item Editor UX Improvements #810
    • Assessor verifies correct location selected by reviewing metadata of location #1191
    • As an assessor I'd like to include a hint option to be displayed below the question text #1279
    • Grids: helper functions for grids #1183
    • Ability to mark an entire row as incorrect on grids #1333
    • Assessor's backed up form responses are archived when storage is filling up #1304
    • Assessor scans a QR Code into form #1309
    • All hidden inputs have reporting values of \"999\" #1349
    • Merge reporting output of radiobuttons into one column.
    • Bug fixes
    • Editor not properly logging users out resulting in getting stuck every 24 hours #1314
    • Min and Max for input number cannot be saved through the interface #1297
    • time on grids cannot be changes and is always 60 seconds #1301
    • Unclosed tags in html container can break form #1289
    • Tangy timed option values disappear #1302

    Note that #1349 will bve optional in future releases and you may not want to upgrade until that time.

    Upgrade instructions:

    git fetch origin\ngit checkout v3.1.0\n./start.sh\ndocker exec tangerine reporting-cache-clear\n

    "},{"location":"whats-new/#v300-beta13","title":"v3.0.0-beta13","text":""},{"location":"whats-new/#upgrade-instructions-from-v3-betas","title":"Upgrade instructions from v3 betas","text":"
    git fetch origin\ngit checkout v3.0.0\n# Note the new T_UPLOAD_TOKEN variable which is a replacement for the old upload account variables.\nmv config.sh config.sh_backup\ncp config.defaults.sh config.sh\nvim config.sh\n./start.sh\ndocker exec tangerine push-all-groups-views\ndocker exec tangerine reporting-cache-clear\n

    For existing groups, you need to edit their app-config.json files in the ./data/client/content/groups folders. Replace them with the following template and make sure to update variables such as groupName, uploadToken, and serverUrl.

    {\n   \"listUsernamesOnLoginScreen\" : true,\n   \"modules\" : [ ],\n   \"groupName\" : \"pineapple\",\n   \"securityQuestionText\" : \"What is your year of birth?\",\n   \"hideProfile\" : false,\n   \"direction\" : \"ltr\",\n   \"columnsOnVisitsTab\" : [],\n   \"hashSecurityQuestionResponse\" : false,\n   \"uploadUnlockedFormReponses\" : false,\n   \"uploadToken\" : \"change this to match T_UPLOAD_TOKEN in config.sh\",\n   \"securityPolicy\" : [\n      \"password\"\n   ],\n   \"homeUrl\" : \"case-management\",\n   \"serverUrl\" : \"https://f571f419.ngrok.io/\",\n   \"centrallyManagedUserProfile\" : false,\n   \"registrationRequiresServerUser\" : false\n}\n

    "},{"location":"whats-new/#new-features-since-v300-beta12","title":"New features since v3.0.0-beta12","text":"
    • Server admin imports client archives into server #1166
    • After exporting data from clients, we now have an easy command line tool to import them. Place those exported files in ./data/archives folder and then run docker exec tangerine import-archives.
    • Consumers of reporting API find user profile data appended to form responses #1147
    • New logstash module for installations that want to use logstash to migrate data to an Elastic Search instance.
    • Enable by adding logstash to the list of modules in config.sh, then clear reporting caches docker exec -it tangerine bash; cd /tangerine/server/src/scripts; ./clear-all-reporting-cache.js;. You will find new <groupName>-logstash databases in CouchDB that you can configure logstash to consume.
    • Upload Tokens instead of upload usernames and passwords.
    • In your config.sh change T_UPLOAD_TOKEN to a secret phrase and then in existing groups add that to app-config.json as an \"uploadToken\" property and uploadUrl to serverUrl but without the username and password and upload/<groupName>. For example, \"uploadUrl\": \"http://uploader:password@foo.tangerinecentral.org/upload/foo\" would become \"serverUrl\":\"http://foo.tangerinecentral.org\", \"groupName\":\"foo\", \"uploadToken\":\"secret_foo_passphrase\".
    • If you not planning on updating clients right away, in config.sh set T_LEGACY=\"true\" to support the older upload API that those clients expect. When all clients are upgraded, set that variable back to false.
    • Editor edits location list for group #982
    • @TODO
    • Editor creates, edits, and deletes form responses on the server #1047
    • Editor exports CSV of a form for a month of their choosing #1143
    • Editor sees user profile form related columns joined to CSV of all forms #1142
    • On client, prevent users from editing their own profile.
    • To impact new groups, change T_HIDE_PROFILE to \"true\" in config.sh .
    • To modify existing groups, change \"hideProfile\" in group level app-config.json to true.
    • Assessor registers on tablet, downloads form responses created on server #1129
    • On device registration, after user creates account, will force user to enter 6 character code that references online account.
    • To impact new groups, change T_REGISTRATION_REQUIRES_SERVER_USER to \"true\".
    • To modify existing groups, change \"registrationRequiresServerUser\" in group level app-config.json to true.
    • Editor updates client user profile on server, Assessor sees updated profile after next sync #1134
    • On client sync, will result in any changes made to a user profile on the server to be downloaded and reflected on the client.
      • To impact new groups, change T_CENTRALLY_MANAGED_USER_PROFILE to \"true\" in config.sh.
      • To modify existing groups, change \"centrallyManagedUserProfile\" in group level app-config.json to true.
    • Editor views tangy-timed items_per_minute calculation in the CSV #1100
    • <tangy-location> can be filtered by entries in the profile by adding attribute <tangy-location filter-by-global>. In the editor when editing a <tangy-location> you will find a new option \"Filter by locations in the user profile?\" you can check.
    • Advanced forms features (no GUI for these features)
    • <tangy-input-group> can be used to create repeatable groups of inputs. See the demo here.
    • Geofence for v3 #941
      • If you location list has latitude and longitude properties for each location, you can validate your <tangy-location> selection given a geofence in <tangy-gps>. See the screenshots here and a code example of how to build this in your form here.
    • Upload incomplete form responses (important for Class module)
    • To modify existing groups, set \"uploadUnlockedFormReponses\" to true in app-config.json.
    • Server Admin clears reporting cache #1064
    • Server Admin runs script to update views in databases #962
    • Server Admin limits by site or by group the number of form responses uploaded end up in reporting outputs #1155
    • This feature brings two new settings to config.sh.
    • Set T_PAID_MODE to \"site\" to limit on a sitewide level, use \"group\" to limit on a per group level.
    • Set T_PAID_ALLOWANCE from \"umlimited\" to a specific number like \"1000\" to limit form responses that end up in reporting outputs to one thousand.
    • This mechanism works by marking uploaded form responses as \"paid\". When you first upgrade to this release, none of your form responses will be marked as paid and will not end up in reporting outputs until they are marked as paid against the allowance. If you want to mark all current uploaded form responses as paid and only mark against their allowance for future uploads, set the allowance to unlimited and after the reporting caches have been built, set the allowance desired and run ./start.sh again.
    • Optional Modules you can turn on and off in config.sh T_MODULES list.
    • Note that if you are going to override the default T_MODULES list with an additional module such as class, don't forget to add modules such as csv if you still need them!
    • Reporting outputs (inluding CSVs) include the information about the number of children a location has. #1174
    "},{"location":"whats-new/#known-issues","title":"Known issues","text":"
    • Memory leak results in Error: spawn ENOMEM #886
    • On the server command line run crontab -e and then add the following entry to restart the program every 24 hours 0 0 * * * docker stop tangerine; docker start tangerine.
    "},{"location":"whats-new/#200-pre-release","title":"2.0.0 (pre-release)","text":""},{"location":"whats-new/#user-stories","title":"User Stories","text":"
    • As a Tangerine Database admin, I want to control which users have the \"Manager\" role for creating new groups #218
    • As a Tangerine Editor User, I expect to see timestamps on CSVs down to the second #223
    • As a user, if I end up on a http:// URL I want to be redirected to the https:// version of that URL #98
    "},{"location":"whats-new/#bugs","title":"Bugs","text":"
    • New groups default Client tabs are set up for workflow, should be vanilla tangerine #230
    • When Tangerine is first installed, User1 does not have the required Manager role so groups cannot be created #229
    • School Location Subtest does not render after upgrading from Tangerine 0.4.x to v2.0.0 #189
    • If a group was upgraded from 0.x.x and does not have a media folder, APK generating fails #186
    • Deleting group does not set security correctly on resulting \"deleted\" database #227
    • Large CSVs fail to generate #221
    • When a new Workflow is created it is missing retrictToRole, reporting, and authenticityParameters #228
    • Ensure /var/log/couchdb exists so CouchDB does not crash #216
    "},{"location":"whats-new/#technical","title":"Technical","text":"
    • Document how to use SSL with Tangerine #219
    • Things to add to .gitignore #185
    • Clean up build process so client does not need to compile twice #74
    "},{"location":"whats-new/#upgrade-directions","title":"Upgrade directions","text":"
    • This is the first release with upgrade scripts so you will need to run all upgrade scripts between the version you started at and this one.

    For example, if you are at Tangerine 0.4.6, then you must run...

    docker exec -it tangerine-container /tangerine-server/upgrades/v1.0.0.sh\ndocker exec -it tangerine-container /tangerine-server/upgrades/v2.0.0.sh\n

    If you are at Tangerine 1.7.8, then you must run...

    docker exec -it tangerine-container /tangerine-server/upgrades/v2.0.0.sh\n

    "},{"location":"whats-new/#v220","title":"v2.2.0","text":""},{"location":"whats-new/#user-stories_1","title":"User Stories","text":"
    • As a Site Owner I want to know how many results have been uploaded given arbitrary time period #457
    "},{"location":"whats-new/#technical_1","title":"Technical","text":"
    • Refactor start.sh and config.defaults.sh to allow configurable ports and tag #456
    "},{"location":"whats-new/#upgrade-direcections","title":"Upgrade direcections","text":"
    docker exec -it tangerine-container /tangerine-server/upgrades/v2.2.0.sh\n
    "},{"location":"artwork/icons/","title":"Icons for v3","text":"

    Gimp source and examples of icons are in the icon-source directory adjacent to this file.

    "},{"location":"data-collector/","title":"Data Collector Guide","text":"
    • Using P2P Sync for Offline Data Transfer

    More coming soon!

    "},{"location":"data-collector/backups/","title":"Backups","text":""},{"location":"data-collector/backups/#backups","title":"Backups","text":"

    Data managers can take backups of the data on a device using the Export Data feature. Log in as the \"admin\" user on the device to export data for all users. The Export Data feature is in the dropdown menu in the top-right corner after log-in.

    The backup files will be saved in the /storage/self/primary/Documents/Tangerine/backups/ directory on the device.

    "},{"location":"data-collector/backups/#copy-device-backups-to-a-computer","title":"Copy device backups to a computer","text":"

    Transfering the backup requres the Android Debug Bridge installed on the computer. Make sure to add the command adb to your executable PATH environment variable.

    Use the following commands to save all of the database backup files:

    For MacOS:

    adb pull /storage/self/primary/Documents/Tangerine/backups/tangerine-variables ~/Desktop/\nadb pull /storage/self/primary/Documents/Tangerine/backups/tangerine-lock-boxes ~/Desktop/\nadb pull /storage/self/primary/Documents/Tangerine/backups/users ~/Desktop/\nadb pull /storage/self/primary/Documents/Tangerine/backups/shared-user-database ~/Desktop/\n

    For Windows:

    adb pull /storage/self/primary/Documents/Tangerine/backups/tangerine-variables %USERPROFILE%\\Desktop\\\nadb pull /storage/self/primary/Documents/Tangerine/backups/tangerine-lock-boxes %USERPROFILE%\\Desktop\\\nadb pull /storage/self/primary/Documents/Tangerine/backups/users %USERPROFILE%\\Desktop\\\nadb pull /storage/self/primary/Documents/Tangerine/backups/shared-user-database %USERPROFILE%\\Desktop\\\n
    "},{"location":"data-collector/data-collector-overview/","title":"Tangerine Data Collection","text":"
    • Deploying Tangerine for Offline Data Collection
    • Data Collection
    • Data Synchronization
    • Update the App - Check for Updates
    "},{"location":"data-collector/data-collector-overview/#quality-management","title":"Quality Management","text":"
    • Forms
    • Data Security
    • Forms Review and Release Process
    • General Data Quality Rules
    "},{"location":"data-collector/data-collector/","title":"Data collector","text":""},{"location":"data-collector/data-collector/#data-collection-on-device","title":"Data collection on Device","text":"

    Select the instrument to use, and follow the prompts, sections, and items on the screen, as generated in the instrument/form editor. To select an answer option, just tab on it.

    Warning

    Different to all other inputs, where the user/assessor selects the applicable / correct response which will then be marked in blue, in grids, the user has to select all INCORRECT items (they will turn blue).

    Answer the rest of the questions as they are presented to you.

    "},{"location":"data-collector/data-collector/#resuming-instrument","title":"Resuming Instrument","text":"

    Should an administration be interrupted, or to be completed at a later stage, navigate to \"Visits\" on the top of the tablet screen.

    Warning

    The visits tab groups all forms by location. Without a location list loaded and a location input on your form, your forms will be availalbe under the noLocation group.

    Select the appropriate location and date (as applicable) from the menu. Tangerine will automatically return the user to the last, incomplete section/page of the instrument.

    You can return from the \"Visits\" page to the main screen by hitting the Tangerine icon on the top left corner

    "},{"location":"data-collector/data-sync/","title":"Data sync","text":""},{"location":"data-collector/data-sync/#data-synchronization","title":"Data Synchronization","text":"

    Whenever possible during data collection, recommend that users / assessors to sync their data to the tablet by navigating to the profile menu, and select \"Sync\".

    The Sync screen provides an overview of the data upload/sync status to date, including the number of responses not yet uploaded as shown below.

    Once the sync is complete, Tangerine will show \"100%\" for the field \"Percentage uploaded\".

    "},{"location":"data-collector/deployment/","title":"Deploying Tangerine for Offline (or Online) Data Collection","text":"

    Reffer to Update the App to see how to update an app that is already installed

    After you have rendered your forms it is time to deploy them. A deployment means that you will create a version of the forms to be installed(or updated) on a tablet. Tangerine offers different type of deployment releases available under your group's Deploy tab.

    Note that we use a release for both to create a new installation but also to create an update for tablets that have the app installed already.

    Release Offline (require installation) APK (Android package file) creates a link with an APK file that can be downloaded and installed on an Android device in offline mode. Browser (PWA) creates a link to the app to be installed inside the Chrome browser on Window, Linux, or Mac but also on Android Chrome mobile app. The installation requires internet connection but the application can be used offline once installed Release Online Survey - deploys a single form to be used by a user in a browser without installation but requires internet connection

    Upon selecting \"Release Offline App\", you will see the screen below.

    Tangerine offers two deployment types, test release and live release:

    1. Test Release -- This release option (\"release to QA\") is recommended for testing the instruments. When you make changes and updates to the instruments and release your changes as \"Test Release\", tablets that have the \"live\" version of Tangerine installed will NOT receive this update. HOWEVER, any data synced from the tablet devices even in a \"Test Release\" deployment goes into the main database (thus mark your tests clearly as \"TESTS\" to facilitate data cleaning.

    2. Live Release -- When instruments/forms are final, or instrument edits have been tested, use this release option (\"release to production\"). In this case, tablets that are already collecting data, or have the group's apk installed, will received an update request when connecting to the Internet the next time. All data collected from this release will also be added to the main database.

    Tangerine also offers two deployment /installation strategies, Android installation or web browser installation:

    • Android Installation. This is the standard deployment package where an actual apk file can be generated on the computer, downloaded, and then copied over to a mobile device via a USB cable and installed. This method of deployment is suitable in slow network environment or when the apk is large.

    • Web Browser Installation. This deployment strategy requires an Internet connection on the tablet for the Tangerine to be installed. Once installed, the app can work again offline. This method is suitable in places of good connectivity.

    NOTE: We recommend thoroughly testing your instruments and its data output before releasing them! To test your instrument use the Test Release mode.

    "},{"location":"data-collector/deployment/#using-standard-installation","title":"Using standard installation","text":""},{"location":"data-collector/deployment/#android-installation","title":"Android Installation","text":"

    Creating a deployment release is the last step before your app can be used on a tablet. Go to your group and click Deploy->Release Offline App and click the Generate Live Release under Android installation. On the next screen you can enter some additional information of your deployment. The Version tag is what the tablet user can use to make sure they have updated their tablet (this can be seen under the About page on the tablet)

    You can change the default release tag and add some description to it. If your group has the historical APK feature enabled, this information will be visible in the listing of historical releases. I will leave the default date and time as my version tag. Now click Release APK

    Wait.

    The process of building the apk can take a couple of minutes. Please be patient and don't navigate away from the page. Once it has completed you will see a screen like the one below presenting a link for you to download. YOu can now download this APK file and install it on a tablet.

    Using the same steps you can create a Test release. We recommend that you use Test releases only for testing purposes and always install live releases on data collection tablets. This is true even for your assessor's training. Once you install a Live release on the data collector's tablet you will be able to push updates to this tablet.

    Warning

    Every time an instrument/form is changed, added, or deleted from the group, it is necessary to create a release and alert each tablet user to use the \"Check for Update\" option in order to update their application.

    "},{"location":"data-collector/deployment/#web-browser-installation","title":"Web Browser Installation","text":"

    This deployment strategy creates a link/URL to a \"progressive web app\" (PWA) for direct installation from the web to the Android tablet or smartphone. Click on Web Browser Installation Test Release/Live Release. Wait.

    Creating a deployment release is the last step before your app can be used on a tablet. Go to your group and click Deploy->Release Offline App and click the Generate Live Release under Browser installation. On the next screen you can enter some additional information of your deployment. The Version tag is what the tablet user can use to make sure they have updated their tablet (this can be seen under the About page on the tablet)

    Note that you should only install 1 application in 1 browser otherwise unexpected conditions may be created. We use the Chrome's Persons option to create a profile for each group that we have in Tangerine. Doing this ensures that your group's data stays isolated and your app always works. Learn how to create Chrome Persons here

    You can change the default release tag and add some description to it. If your group has the historical APK feature enabled, this information will be visible in the listing of historical releases. I will leave the default date and time as my version tag. Now click Release PWA

    The process of building the PWA is very quick. Once it has completed you will see a screen like the one below presenting a link for you to copy. You can use this link and install the Browser Release on a tablet, or inside the Chrome or Edge browsers on any Operating System

    We will discuss how to install the browser release in Browser/PWA Installation topic

    Using the same steps you can create a Test release. We recommend that you use Test releases only for testing purposes and always install live releases on data collection tablets. This is true even for your assessor's training. Once you install a Live release on the data collector's tablet you will be able to push updates to this tablet.

    Warning

    Please keep in mind that the Browser/PWA release requires an internet connection to be installed on a tablet or browser on a computer. The installation must be done correctly for the app to be accessible offline. Refer to our Browser/PWA Installation for more information.

    Warning

    Every time an instrument/form is changed, added, or deleted from the group, it is necessary to release the apk/pwa again, but NO NEW INSTALLATION is necessary on the tablets. Instead, instruct Tangerine tablet users to connect their tablets, select their profile page (3 vertical white dots on top right of tablet screen).

    Warning

    This update approach will not only apply any instrument/form edits, new forms, or form deletions, but also any updates to the Tangerine application made in the meantime and applied to your group (if any).

    "},{"location":"data-collector/deployment/#create-an-online-release","title":"Create an Online Release","text":"

    Creating a deployment release is the last step before your form can be filled in on a tablet or a computer. Go to your group and click Deploy->Release Online Survey. Here you will see a list of forms that have been publish and those that can be published. On the image below we see that one form is listed under Published Surveys and other forms are under Unpublished Surveys.

    To publish a form click the check mark icon beside it on any of the forms under Unpublished Surveys.

    Clicking on this icon brings that form up to the Published Surveys section and you can see the form's name plus some other info and actions on the right hand side. Those are: an icon to unpublished the form, the date of publishing, and the link to this form

    Right after you publish a form you will also see a message at the bottom of the screen confirming your action.

    To unpublish a form, click the icon beside it. This will take the form down to Unpublished forms and the link will no longer be available for this form to be filled in.

    Warning

    If you have made any changes on your form, you must unpublish it and re-publish it in order for those changes to be visible in the form's link.

    To share your form for data entry, copy the link from the Published Survey listing and send it to your data accessors Note that the online survey can be opened only on Chrome, Edge, Safari, and later versions of Firefox.

    "},{"location":"data-collector/deployment/#creating-releases-video","title":"Creating Releases Video","text":""},{"location":"data-collector/deployment/#using-apk-device-setup-installation","title":"Using APK Device Setup Installation","text":"
    1. Connect the tablet to WiFi (or connect direct to network using a SIM card)

    Note: here you can also copy the file from your computer to the tablet using a cable

    1. Download the APK file to your tablet and tap it to install. You can also copy the APK file to the tablet with a cable

    Note that here you may get a warning for enabling Unknown Sources. This setting allows you to install an app coming from outside the Play store. Please allow this in your settings.

    The instructions below are generic and they may differ for your version of Android.

    1. If you receive the blocked install message like in the image below click Settings to enable Unknown Sources. Tap the Settings button

    Make sure you select the Unknown sources

    Click OK to Confirm

    To continue with the installation note all different permission Tangerine needs. We are making user of the GPS location, and contents on device \u2013 this one is for data export.

    SMS is used only if configured in your forms to send an sms

    Taking pictures is also used only when configured in your forms

    Click Next

    Click Install

    Wait for the installation to complete

    Now you have the app installed. you can find it in the app drawer or your main screen.

    To open the app click Open

    The app is now installed. You can proceed and Register a user. In general we let the data assessor go through the registration process and create their username and password. Note that more than one users can share a tablet.

    Fill in your information and click Submit

    The next step is to create your assessor profile. All of the data on the user profile(Assessor/ Enumerator profile) is attached to each form collected on this device

    Enter the information presented on the user profile page and tap Submit

    You are now free to use the app

    "},{"location":"data-collector/deployment/#using-device-setup-installation-2-way-sync-setup","title":"Using Device Setup Installation - 2 way sync setup","text":""},{"location":"data-collector/deployment/#installation","title":"Installation","text":"

    Tangerine offers two deployment /installation strategies, Android installation or web browser installation:

    • Android Installation. This is the standard deployment package where an actual apk file can be generated on the computer, downloaded, and then copied over to a mobile device via a USB cable and installed. This method of deployment is suitable in slow network environment or when the apk is large.

    • Web Browser Installation. This deployment strategy requires an Internet connection on the tablet for the Tangerine to be installed. Once installed, the app can work again offline. This method is suitable in places of good connectivity. This method of installation can also be used for installing app on your Chrome browser on a PC or laptop

    For both of the installation models you will need a Registration Code (QR Code) or a device ID and Token. If you don't have this information you will not be able to install the application on your device.

    We recommend that the initial device setup is done by a responsible person at the site. This can be someone who will handle queries regarding Tangerine or an IT staff. Each device registration requires that an admin user is create on that device. This admin user is the one that can authorize the registration of a user account. This is to make sure that your data can only be accessed by authorized personnel and no untheorized accounts exist on the tablet.

    "},{"location":"data-collector/deployment/#installation-on-a-tabletphone","title":"Installation on a tablet/phone","text":"

    Copy the apk file to the tablet and open it. Follow the installation prompts until you receive a message that the app has been installed. Locate the Tangerine app in the application drawer and click it.

    Warning

    Note that you must be online on the tablet to do the initial installation.

    The first step is to select the language for the user interface

    Select the language and click Submit

    Now enter the administration password for this tablet. You may wish to use the same admin password for all tables at your site. This same password will be required each time a user is registering to use the app on this device.

    Select Yes if you have a device code or No if you are going to insert a device ID and Token for the registration.

    Insert the ID and Token or click the Scan icon to scan the registration QR code.

    Click Submit when done. The next screen will show you some information for this device. If it is correct select Yes, if the scanned device code and ID correspond to a different device select No and start over with the correct device code.

    On the next screen you will see some synchronization information. The app at this moment is contacting the server and obtaining users assigned to your device location. If you have already collected data on another tablet for this location, this data will also be pulled.

    Click Next and then go to the Registration tap. Ask your administrator to enter the admin password and enter your user information below. Click submit when ready.

    If you are an administrator handing off the tablet to a user, enter your password and ask the user to enter their username and password. Here the Year of Birth can be used by the user to reset their password in case they forgot it.

    On the next screen you will see a dropdown of all users for this location. Select the one that corresponds to you and click Submit.

    You will now see a screen similar to the one below where you can start working

    "},{"location":"data-collector/deployment/#installation-in-your-chrome-browser","title":"Installation in your Chrome browser","text":"

    Tangerine can be installed and used offline in the Chrome browser. To do this we follow similar installation instructions as above.

    Warning

    You must be online on to do the initial installation.**

    The first step is to follow the link for Browser installation that has been given to you or copied directly after it's generation in the backend. Copy the link and paste it in the Chrome's address bar. You will see a screen indicating that the app is being installed.

    After a successful installation you will receive a confirmation screen like the one below. Do not click the link to proceed.

    Click the + icon beside the address bar to install Tangerine in your browser. A popup will open to give you the option to install the app. Click Install

    Depending on your browser setup, you may be asked to create a shortcut on your desktop or in your program folder or the browser may close automatically and offer you the link for Tangerine to open. If you see the link click it

    If you cannot find it type this into the address bar of your browser: [chrome://apps/]{.underline}

    Warning

    Always start Tangerine from the application icon and not from the URL address. Only one Tangerine instllation per browser profile is allowed.

    Click the Tangerine app to start the application.

    Select the language and click Submit

    Now enter the administration password for this tablet. You may wish to use the same admin password for all tables at your site. This same password will be required each time a user is registering to use the app on this device.

    Select Yes if you have a device code or No if you are going to insert a device ID and Token for the registration.

    Insert the ID and Token or click the Scan icon to scan the registration QR code. If your PC or laptop doesn't have a camera that can be used to scan the barcode, you'd have to type in the ID and Token

    Click Submit when done. The next screen will show you some information for this device. If it is correct select Yes, if the scanned device code and ID correspond to a different device select No and start over with the correct device code.

    On the next screen you will see some synchronization information. The app at this moment is contacting the server and obtaining users assigned to your device location. If you have already collected data on another tablet for this location, this data will also be pulled.

    Click Next and then go to the Registration tap. Ask your administrator to enter the admin password and enter your user information below. Click submit when ready.

    If you are an administrator handing off the tablet to a user, enter your password and ask the user to enter their username and password. Here the Year of Birth can be used by the user to reset their password in case they forgot it.

    On the next screen you will see a dropdown of all users for this location. Select the one that corresponds to you and click Submit.

    You will now see a screen similar to the one below where you can start working

    "},{"location":"data-collector/deployment/#synchronization","title":"Synchronization","text":""},{"location":"data-collector/deployment/#user-setup","title":"User setup","text":"

    Follow to below steps to prepare the Tangerine backend to allow installation of your app on the user device or Chrome browser. The menu items used during setup can be found under the Deploy link in the left side navigation menu.

    Click the Device Users section to create a new tablet user profile. On this screen you will see a listing of all users already created. At the bottom left of the screen there is a'+' icon which allows you to add a new device user profile.

    • Click the + icon to create a user profile on the server

    • Fill in all information required in the profile and click Submit

    • You will see that next to the Submit button a blue check marks appears indicating that the profile was saved

    • Repeat the above steps for all users
    "},{"location":"data-collector/deployment/#device-setup","title":"Device setup","text":"

    Now we have to create the 'virtual' devices that will be associated with a real device or browser upon installation. The devices that you create represent a real user device. Each virtual device can be associated with a real tablet only once. After being claimed the device cannot be reused (unless reset and the app re-installed). Each device requires that we assign it to a particular location. This assigned location is automatically attached to each form collected from the tablet. We can also control the synchronization level. This level will indicate to Tangerine what records should be kept in sync across tablets. If you select to synchronize on a top level, all tablets will contain all records collected over all of the facilities in under this top level. It may be better to choose to synchronize only at the bottom level.

    The device listing go to Deploy->Devices

    • Here, if you have some devices already created you will see a full listing with some other information

    • The device listing gives you:

      • The ID of the device

      • The assigned location

      • Whether this device has already been used in an installation or not. A checkmark under Claimed indicates that it has been used.

      • Registered on gives you the date this device was first registered

      • The last synchronization date for this device

      • Updated on is the date this device was last updated

      • Version is the version of the application this device is running

      • Under options you will have access to a menu allowing you to Edit, Reset, Delete, get The QR registration code for a device.

    Create a new device by going to Deploy->Devices

    • Click the + icon at the bottom of the page

    • Note that you have the ID and Token listed here (can also be access by clicking Edit for a particular device on the device listing screen) In cases where the QR code cannot be sent to the site for installation you can also use the ID and Token to install Tangerine.

    • Select the location this device is assigned to

    • Select the synchronization level

    • Select the actual site to be used for synchronization. Be careful here not to assign the device to one location and select a different one for synchronization.

    • Click Submit

    • The device now shows up on the of the list and you can get the QR code for it by clicking the Options menu and selecting Registration Code

    • Repeat the above steps for all devices that you need to use on your project.

    NOTE: you may wish to store the device ID and Token in a file for safe keeping. Such a sample file can be found here. Put the device ID and the Token in the corresponding columns and the QR code will be generated for you. Keep in mind that his worksheet functions correctly only on Google Drive. You can also print this file and distribute the installation codes on paper.

    "},{"location":"data-collector/deployment/#device-modification","title":"Device modification","text":"

    It may happen that you want to modify a device that is already in use. Although you will be able to do that in the interface, this modification is not pushed to the actual tablet. The way to go here is to apply the modification reset the device, and send the new Registration Code or ID and Token to the admin to reinstall the application. Make sure that before re-installing the app all data is synchronized.

    NOTE: You can edit all devices that have not been claimed yet.

    To Edit a device, click the Edit button in the Options menu. Apply any modifications and click Submit

    To Delete a device , click the Delete button in the Options menu. Keep in mind that any device that you delete will disallow this device from synchronization or updates. Use this option if you have had one of your devices stolen or lost.

    To Reset a device, click the Reset button in the options menu. This action will disallow the device from receiving updates or synchronizing data. Use this option, if one of your staff members leaves and will no longer use this device. Make sure data is synchronized before you reset the device.

    "},{"location":"data-collector/update-app/","title":"Update app","text":""},{"location":"data-collector/update-app/#updating-the-app","title":"Updating the App","text":"

    The app update in Tangerine can bring up new content or updates. to your forms but it can also update the version of the app you are currently using.

    To check which version of the app you are on go to the top right menu and select About. When you scroll down you will see the Device Information section. The most important information here is the Version Tag. In the example below the version tag is: 2022-06-16-14-21-15

    If the default tag is used it will be in a similar format, where you see the date and time of the release. Ask your supervisor for the correct version tag to make sure you are running the right forms.

    You can also see the build channel below. This indicates if you are running a Live or a Test release

    To update the app you have to find the Check for Updates link in the top right menu. Make sure you are connected to the network and tap the Check for updates link.

    After you tap the Check for updates link you will receive a pop up message. This message asks you to confirm that you want to perform the check for updates. Clicking OK will continue with the check and will update the app. Clicking Cancel will cancel the update.

    Tap OK to continue:

    After the update has been downloaded you will come back to a similar screen. Tap Continue to apply any pending actions.

    You will now be logged out of the app and have to log back in. Every time you do an update you'd have to log in to the app again. After the update go to the About page and verify that the Version tag has changed.

    You can see below that my version tag is now: 2022-06-24-14-10-00. If your version tag didn't change there was no update available to be installed. Updates can be pushed only by users with access to the backend.

    "},{"location":"data-collector/p2p/p2p-sync/","title":"Using P2P Sync for Offline Data Transfer","text":"

    Use the P2P sync feature to transfer data between two or more tablets without an Internet connection.

    Note: The tablets must be running Android 8 (Oreo) to use this function.

    In the following example, your tablet will be syncing data from your tablet to two other peers' tablets running Tangerine. The goal is to have the same data on all tablets. At the end of the process, data will be transferred from your tablet to the Internet.

    "},{"location":"data-collector/p2p/p2p-sync/#accessing-the-p2p-feature","title":"Accessing the P2P feature:","text":"

    Each peer should select Sync from the menu.

    Click the P2P Sync tab.

    "},{"location":"data-collector/p2p/p2p-sync/#discovery","title":"Discovery","text":"

    Gather in a circle with your peers. You and your peers should click the Discovery button. It does not matter who clicks first.

    "},{"location":"data-collector/p2p/p2p-sync/#endpoints","title":"Endpoints","text":"

    After a short time - a minute of two, the screen will show the device name, a list of available endpoints, and information about the data transfers in the Log.

    In the screenshot, your device name is highlighted in green (89678). The list of available endpoints - your peers' tablet names - is highlighted in red. A log of diagnostic information is highlighted in blue.

    (Please note that the endpoint names are randomly generated. The names you see when using this feature will be different.)

    "},{"location":"data-collector/p2p/p2p-sync/#syncing-to-an-endpoint","title":"Syncing to an Endpoint","text":"

    At this point in the process, your peers don't need to push any buttons; they only need to monitor the sync process for errors. Your tablet will be called the \"master\" tablet because it is controlling the sync operations. Once the \"master\" tablet has collected all of the data from the tablets, it can be connected to the Internet and upload all of this data to the server.

    Ask your peers which one has the tab at the top of the endpoints list marked \"35747 - Pending\". Upon identifying that peer, ask them to pay attention to the screen. Now you may click \"35747 - Pending\" to initiate the data transfer. Notice how the endpoint button you click turns a darker shade of grey to indicate that it has been pressed.

    Your tablet will send its data to your peer's tablet and then your peer's tablet will send its data back to your tablet, as well as the data you just sent. It's a little redundant, but this is part of reaching \"eventual consistency\" for all of the tablets.

    Notice that more data is added to the Log as the connection is made between the tablets and data transfer is initiated:

    When the data transfer is complete, the endpoint list updates to show that you are ready to sync the next device (\"Done! Sync next device.\").

    Ask your peer if they received any error messages; if not, it is safe to proceed to the next peer's tablet. Ask the peer who has the tablet marked \"29726\" to be ready. Click the endpoint marked \"29726: Pending\".

    When the data transfer is complete, the endpoint list updates to show that you are ready to sync the next device (\"Done! Sync next device.\"). Ask your peer if they received any error messages. If none, great! Since you're at the end of the endpoints list, you are done with this first part.

    "},{"location":"data-collector/p2p/p2p-sync/#do-it-again","title":"Do it again!","text":"

    When you synced data from your \"master\" tablet to the second tablet, it received data from the first tablet, which was transferred to the \"master\" when it was sync'd. But the first tablet still needs to receive data from the second tablet. So, you will need to repeat this whole process, starting from the first tablet (35747) and then to the second (29726). (You actually don't need to sync again the final device sync'd in the process (29726), but it is easier to explain this process as a simple round-robin.)

    It may be useful to confirm that any records created on other tablets has indeed been transferred.

    English:

    French:

    As mentioned earlier, once the sync process is complete (and you've done it twice), you may conect the \"master\" to the Internet and transfer data to the server.

    "},{"location":"data-collector/p2p/p2p-sync/#tips","title":"Tips","text":"
    • Each time you visit the P2P page, your device name will change. It is a randomly generated number.
    • Errors are highlighted in pink. It is fine to ignore the error marked \"State set to CONNECTED but already in that state\".
    "},{"location":"data-manager/","title":"Data Manager","text":""},{"location":"data-manager/data-structure/","title":"Data Structure","text":"

    A Case is a Document in a CouchDB database. A Case consists of 4 other Entities: CaseEvent, EventForm, and Participant. A Case is described by its CaseDefinition. A CaseDefinition also contains CaseEvetnDefinition(s) that describe what CaseEvents can exist in a Case, EventFormDefinition(s) that define what EventForms can exist in what CaseEvents, and Roles that describe what kind of Participants can exist in a Case. Lastly, all EventForms relate to separate FormResponse Documents in the database.

    "},{"location":"data-manager/data-structure/#formresponse-schema","title":"FormResponse Schema","text":""},{"location":"data-manager/data-structure/#casedefinition-schema","title":"CaseDefinition Schema","text":"
    {\n  // A unique string that will be used to refer to the CaseDefinition.\n  \"id\": \"case-type-1\",\n  // An ID of a Form listed in forms.json that will be used to store Case level variables. \n  \"formId\": \"case-type-1-manifest\",\n  ...\n}\n
    "},{"location":"data-manager/data-structure/#case-schema","title":"Case Schema","text":"
    {\n  // A UUID generated by Tangerine to refer to the Case internal to Tangerine. Useful if Study IDs have collisions.\n  \"id\": \"00673b47-8452-4b4c-9597-5a56fe8a059c\",\n  // Because every Case is also a FormResponse, a Case includes top level form info you would find on a FormResponse.\n  \"form\": {\n    // The ID of the FormResponse this Case relates to. Shoudl be the same as the related CaseDefinition's formId.\n    \"id\": \"case-type-1-manifest\",\n    ...\n  },\n  // The related CaseDefinition's id property. \n  \"caseDefinitionId\": \"case-type-1\",\n  ...\n}\n
    "},{"location":"data-manager/data-structure/#caseeventdefinition-schema","title":"CaseEventDefinition Schema","text":""},{"location":"data-manager/data-structure/#caseevent-schema","title":"CaseEvent Schema","text":""},{"location":"data-manager/data-structure/#eventformdefinition-schema","title":"EventFormDefinition Schema","text":""},{"location":"data-manager/data-structure/#eventform-schema","title":"EventForm Schema","text":""},{"location":"data-manager/mysql/","title":"MySQL","text":""},{"location":"data-manager/on-device-data-corrections/","title":"On Device Data Corrections using Issues","text":"

    Enable \"Allow Creation of Issues\" in your App Config for Devices and Data Collectors will be able to propose changes to data that has already been submitted. After the Data Collector syncs, Data Managers may view those proposals in the Issues UI in their corresponding group, then comment or choose to merge the issue.

    • Video Demo of configuration and usage

    "},{"location":"data-manager/on-device-data-corrections/#configuration","title":"Configuration","text":"
    • To enable Devices to create Issues, add \"allowCreationOfIssues\": true to the group's client/app-config.json and release to Devices.
    • To allow Devices to see Issues that have been created on the Device or been synced down to the Device, add \"showIssues\": trueto the group's client/app-config.json and release to Devices.
    • To template out the resulting Issue title and descriptions, add templateIssueTitle and templateIssueDescription to Case Definitions.
    • Example template for Issue Title: Issue for ${caseService.getVariable('study_id')} (${caseService.getVariable('firstname')} ${caseService.getVariable('surname')}, ${caseService.getVariable('village')}) by ${userId}
    "},{"location":"data-manager/on-device-data-corrections/#on-device-data-merge-using-issues","title":"On Device Data Merge using Issues","text":"

    Optionally, enable \"Allow Merge of Issues\" in your App Config for Data Collectors on Devices to see the \"Commit\" button on Issues. Clicking the \"Commit\" button will take the form and case changes in the proposal and make them the current version that appears in the case and form response.

    Although the \"Merge\" setting can be available for all Data Collectors, it is best practice to require some oversite for data collectors to merge issues. The Data Manager can set User Roles to control.

    To allow merge on tablet by user role, add user role (defined in user-profile.html) the \"update\" permissions section for event definitions or form definitions in the case definition file. For example, the case definition file excerpt below would allow all users except the \"data_collector_role\" to merge cases.

    {\n    \"id\": \"hosehold-case\",\n    \"formId\": \"household-case-manifest\",\n    ...\n    \"eventDefinitions\": [\n      {\n        \"id\": \"enrollment-event\",\n        \"name\": \"Household Baseline Visit\",\n        \"description\": \"\",\n        \"repeatable\": false,\n        \"estimatedTimeFromCaseOpening\": 0,\n        \"estimatedTimeWindow\": 0,\n        \"required\": true,\n        \"permissions\": {\n          \"create\": [\"admin\"],\n          \"read\":   [\"admin\", \"data_manager_role\", \"data_collector_role\"],\n          \"update\": [\"admin\", \"data_manager_role\"],\n          \"delete\": [\"admin\"]\n        },\n
    "},{"location":"data-manager/on-device-data-corrections/#configuration_1","title":"Configuration","text":"
    1. Consider which strategy best allows data collectors to merge corrections while providing oversight to Issues.
    2. To enable Devices to Merge Issues, add \"allowMergeOfIssues\": true to the group's client/app-config.json and release to Devices.
    3. Add User Role permissions to the case definitions file
    "},{"location":"developer/","title":"Developer Guide Contents","text":"
    • Bullet points for Tangerine Development
    • i18n-translation.md
    • modules.md
    • tangerine-dev-tutorial-notes.md
    • Reverse Proxy for Developers
    "},{"location":"developer/#application-design","title":"Application design","text":"
    • Data Structures
    • Bootstrapping Tangerine
    • Viewing Forms and Form Data
    • Supporting custom elements
    • Tangerine Globals
    • How Tangerine code is generated
    "},{"location":"developer/#debugging","title":"Debugging","text":"
    • Creating Clean Development Content
    • debugging-reporting.md
    • debugging_node_apps.md
    "},{"location":"developer/#managing-deployments","title":"Managing Deployments","text":"
    • Upgrades
    • Installing Multiple Tangerine apps on the same tablet
    • Deleting Records
    "},{"location":"developer/#testing","title":"Testing","text":"
    • Load Testing
    • Testing conflicts
    "},{"location":"developer/#developing-cordova-plugins","title":"Developing Cordova Plugins","text":"
    • cordova-plugin-development.md
    "},{"location":"developer/#tangerine-class-projects","title":"Tangerine Class Projects","text":"
    • class-docs.md
    • class-deletions.md
    "},{"location":"developer/#troubleshooting","title":"Troubleshooting","text":"
    • Docker Network Issues
    "},{"location":"developer/bootstrapping-tangerine/","title":"Bootstrapping Tangerine","text":"

    During a recent Angular 8 upgrade process, we had to change how Tangerine initializes. If Tangerine is running inside a Cordova app, it must wait until the 'deviceReady' event is emitted. Our earlier method of doing this in main.ts is no longer possible; therefore, we are intercepting the Service initialization process by using APP_INITIALIZER to pause the app while Cordova loads. Here is the relevant comit. For more information, view the Predefined tokens and multiple provider section in the Angular documentation.

    "},{"location":"developer/class-deletions/","title":"Deletions in Tangerine","text":"

    We're archiving records instead of deleting them in Tangerine by setting archive:true on the root of the doc and filtering queries by && !archive.

    Please note that vanilla Tangerine uses the archived flag.'

    For most queries, you simply must simply append && !archive to the query in order to ensure your views filter for the archive flag.

    Sample view that filters by archive:

        responsesByClassIdCurriculumId: {\n      map: function (doc) {\n        if (doc.hasOwnProperty('collection') && doc.collection === 'TangyFormResponse' && !doc.archive) {\n          if (doc.hasOwnProperty('metadata') && doc.metadata.studentRegistrationDoc.classId) {\n            // console.log(\"matching: \" + doc.metadata.studentRegistrationDoc.classId)\n             emit([doc.metadata.studentRegistrationDoc.classId, doc.form.id], true);\n          }\n        }\n      }.toString()\n    },\n

    Sample function to archive some records:

      async archiveStudent(column) {\n    let studentId = column.id\n    console.log(\"Archiving student:\" + studentId)\n    let deleteConfirmed = confirm(_TRANSLATE(\"Delete this student?\"));\n    if (deleteConfirmed) {\n      try {\n        let responses = await this.classViewService.getResponsesByStudentId(studentId)\n        for (const response of responses as any[] ) {\n          response.doc.archive = true;\n          let lastModified = Date.now();\n          response.doc.lastModified = lastModified\n          const archiveResult = await this.classViewService.saveResponse(response.doc)\n          console.log(\"archiveResult: \" + archiveResult)\n        }\n        let result = await this.dashboardService.archiveStudentRegistration(studentId)\n        console.log(\"result: \" + result)\n      } catch (e) {\n        console.log(\"Error deleting student: \" + e)\n        return false;\n      }\n    }\n  }\n
    "},{"location":"developer/class-docs/","title":"Getting Started","text":""},{"location":"developer/class-docs/#how-to-get-data-out-of-a-tangyformresponse","title":"How to get data out of a TangyFormResponse","text":"
    const studentRegistrationDoc = await dashboardService.getResponse(this.studentId);\nconst srInputs = this.getInputValues(studentRegistrationDoc);\n\n  getInputValues(doc) {\n    let inputs = doc.items.reduce((acc, item) => [...acc, ...item.inputs], [])\n    let obj = {}\n    for (const el of inputs) {\n      var attrs = inputs.attributes;\n      for(let i = inputs.length - 1; i >= 0; i--) {\n        obj[inputs[i].name] = inputs[i].value;\n      }\n    }\n    console.log(\"obj: \" + JSON.stringify(obj))\n    return obj;\n  }\n
    "},{"location":"developer/cordova-plugin-development/","title":"Cordova plugin development","text":""},{"location":"developer/cordova-plugin-development/#getting-started","title":"Getting started","text":"

    It is a lot easier to build a cordova plugin for Tangerine using a generic Cordova project instead of developing directly in Tangerine, because in Tangerine access to the actual client Cordova code is hidden away in /tangerine/client/builds/apk. So first use the cordova cli to generate a new project.

    "},{"location":"developer/cordova-plugin-development/#refreshing-your-new-plugin-in-your-cordova-project","title":"Refreshing your new plugin in your Cordova project","text":"

    After making modifications to the plugin, rm and add the plugin and the cordova android platform before building.

    cordova plugin rm cordova-plugin-nearby-connections\ncordova platform rm android\ncordova platform add android@8\ncordova plugin add ../../Tangerine-Community/cordova-plugin-nearby-connections\ncordova build android\n
    "},{"location":"developer/cordova-plugin-development/#updating-the-cordova-plugin-inside-tangerine","title":"Updating the cordova plugin inside Tangerine","text":"

    After your done the bulk of your Cordova development, you will need to modify the docker-tangerine-base-image to include the new plugin. After updating the base image, don't forget to update the Dockerfile.

    Sometimes you may need to view an update to the plugin but you don't want to go to the trouble of updating the base image. It is possible to work on the plugin code and then refresh the code in Tangerine. First you will need to share the source code with your docker instance Add the following to develop.sh:

      --volume $(pwd)/../cordova-plugin-nearby-connections:/tangerine/client/cordova-plugin-nearby-connections \\\n

    Once your container has started, docker exec into it, and run the following:

    cd /tangerine/client/builds/apk\ncordova plugin rm cordova-plugin-nearby-connections\ncordova plugin add ../../cordova-plugin-nearby-connections --save\n
    Sometimes cordova can have issues with cleaning the build; here's a way to make sure you have the updated code:
    cd /tangerine/client/builds/apk\ncordova plugin rm cordova-plugin-nearby-connections\ncordova platform rm android\ncordova platform add android@8\ncordova plugin add ../../cordova-plugin-nearby-connections --save\ncordova build android\n

    "},{"location":"developer/cordova-plugin-development/#updating-angular-client-code-used-in-the-apk","title":"Updating Angular client code used in the APK","text":"

    IF you're developing Cordova plugins for Tangerine and make changes to the Angular client code that is displayed in the apk, you will need to refresh the apk build. Run the following code:

    cd /tangerine/client && \\\nrm -rf builds/apk/www/shell && \\\nrm -rf builds/pwa/release-uuid/app && \\\ncp -r dev builds/apk/www/shell && \\\ncp -r pwa-tools/updater-app/build/default builds/pwa && \\\ncp -r dev builds/pwa/release-uuid/app\n

    Then generate the apk.

    To check if it worked, you can search for the new code in these files:

    vi builds/apk/www/shell/main.js\nvi builds/pwa/release-uuid/app/main.js\n

    T0 uninstall and re-install the apk:

    adb uninstall org.rti.tangerine\nadb install qa/apks/group-long-uuisd/platforms/android/app/build/outputs/apk/debug/app-debug.apk\n
    "},{"location":"developer/creating-clean-dev-content/","title":"Creating Clean Development Content","text":"

    If you are trying to fix an issue, it is helpful to begin development using content that is known to support common Tangerine features. This can be more reliable than using a project's content because that content may have missing forms that create bugs that have nothing to do with the issue you are trying to resolve.

    "},{"location":"developer/creating-clean-dev-content/#client","title":"Client","text":"
    cd client\nnpm install\nrm -rf client/src/assets\ncp -r ../content-sets/<your pick>/client src/assets\ncp src/assets/app-config.defaults.json src/assets/app-config.json\ncp ../translations/translation* src/assets/\nnpm start\n
    "},{"location":"developer/creating-clean-dev-content/#server","title":"Server","text":"

    The create-group command to the rescue!

    The following command downloads a content set known to support common Tangerine features and is used for load-testing. Notice that it is a github repo; therefore, you may clone it and modify at will.

    docker exec tangerine create-group \"New Group C\" https://github.com/rjsteinert/tangerine-content-set-test.git

    There is also support for creating a group using local content from the content-sets directory' in the Tangerine repository. Currently, there is support for creating a case-module:

    docker exec tangerine create-group \"New Group D\" case-module

    You may also configure how inputs are populated by custom functions; see the Case generation section in the Load testing doc.

    If you add --help to the create-group command you may see other options as well.

    docker exec tangerine create-group --help

    To see more examples, check out the demo video from the v3.10.0 release.

    "},{"location":"developer/data-structures/","title":"Tangerine Data Structures - Document Collections and Types","text":"

    If the goal is to have data output to CSV via the changes log, the collection property of the document should be \"TangyFormResponse\". This is the only collection that is currently supported by data processing on the server-side.

    "},{"location":"developer/data-structures/#tangyformresponse","title":"TangyFormResponse","text":"

    A TangyFormResponse is a JSON object that contains all the data that a user has entered into a Tangy Form. It is the data that is stored in the database when a user submits a Tangy Form. It is also the data that is returned when you call the dashboardService.getResponse() method.

    Tangerine modules - such as CSV and mysql - can be adapted to support different data structures in a TangyFormResponse. The default case is to output a CSV file with a header row and a row for each TangyFormResponse. The header row contains the names of the inputs in the TangyFormResponse. The data rows contain the values of the inputs in the TangyFormResponse.

    A register - such as an Attendance register used in Teach - is a snapshot of a list of things being tracked, such as students attending a class. (Contrast this to a typical TangyFormResponse that is a snapshot of form inputs.) A register is for collecting data about multiple people or objects, whereas a formResponse is collecting data for a single unit or participant. An example of a register is a form of type 'attendance' or 'scores' which is used to collect data in the Attendance feature of the Teach module. There is a property called attendanceList that has an array of students. When processing the changes feed, the csv module detects the 'attendance' type and creates a new row for each student in the attendanceList.

    In the case module context, a case definition manages participants and the forms submitted with data about each participant. A class project does not have a file similar to a case definition; it is more rigid and stores much of those relationships in the app logic.

    One could say a case is similar to a Class in Teach in that Teach saves metadata about a school, classes and students, but it is a rigid structure. A case is more flexible and can be used to track any type of data.

    "},{"location":"developer/debugging-reporting/","title":"Debugging reporting","text":""},{"location":"developer/debugging-reporting/#debugging-the-reporting-cache-process","title":"Debugging the Reporting Cache process","text":"

    Summary of steps:

    1. Turn on reporting modules in config.sh.
    2. Run develop.sh.
    3. Create a group.
    4. Generate data.
    5. Stop the keep alive for reporting worker by commenting out this.keepAliveReportingWorker() in server/src/app.service.ts.
    6. Enter the container on command line with docker exec -it tangerine bash.
    7. Clear reporting cache with command reporting-cache-clear.
    8. Run a batch with debugger enabled by running command node --inspect-brk=0.0.0.0:9228 $(which reporting-worker-batch).
    9. Latch onto debugging session using Chrome Inspect. You may need to click \"configure\" and add localhost:9228 to \"Target discovery settings\".
    "},{"location":"developer/debugging-reporting/#instructions","title":"Instructions","text":"

    Configure your project to use the CSV and Logstash modules:

    T_MODULES=\"['csv', 'logstash']\"\n

    Start the development environment...

    ./develop.sh\n

    Create a group called foo in the GUI. Then open ./server/src/app.service.ts and comment out the call to this.keepAliveReportingWorker(). It's important to do these two things in this order otherwise the group could be disconnected from reporting.

    \"exec\" into the container and note how foo has been added to the /reporting-worker-state.json file.

    docker exec -it tangerine bash\ncat /reporting-worker-state.json\n

    Seed the foo group with 100 form responses.

    docker exec tangerine generate-uploads 100 foo\n

    Clear the cache

    ```shell script docker exec tangerine reporting-cache-clear

    If you get the error message 'Waiting for current reporting worker to stop...', you must exec into the container and remove the semaphore:\n\n```shell script\nrm /reporting-worker-running\n

    and then run reporting-cache-clear again.

    Start the reporting-worker-batch.js batch process manually and check for errors

    ```shell script node --inspect-brk=0.0.0.0:9228 $(which reporting-worker-batch)

    In Chrome, go to `chrome://inspect`, click `Configure...`, and add `127.0.0.1:9228` as an entry in \"Target discovery settings\".\n\n## Debugging Demo \n\nhttps://www.youtube.com/watch?v=AToUBoApw8E&feature=youtu.be\n\nNow manually trigger a batch. After the command finishes, verify the batch by checking `http://localhost:5984/_utils/#database/foo-reporting/_all_docs`.\n
    node --inspect-brk=0.0.0.0:9228 $(which reporting-worker-batch)
    There will be only 15 docs in your reporting db because that is the batch size.\n\nAlthough Tangerine in develop.sh mode runs node in a debugger process, you must launch a separate node process to debug the batch reporting worker.\n\nIf no errors occurred, copy the temporary state to the current state.\n
    cp /reporting-worker-state.json_tmp /reporting-worker-state.json
    Keep repeating to continue processing...\n
    cat /reporting-worker-state.json | /tangerine/serversrc/scripts/reporting-worker-batch.js | tee /reporting-worker-state.json_tmp cp /reporting-worker-state.json_tmp /reporting-worker-state.json
    If you would like to debug, add the `--inspect-brk=0.0.0.0:9227` option to the `run-worker.js` command.\n
    cat /reporting-worker-state.json | node --inspect-brk=0.0.0.0:9227 /tangerine/server/src/scripts/reporting-worker-batch.js | tee /reporting-worker-state.json_tmp
    When you run that command, it will wait on the first line of the script for a debugger to connect to it. In Chrome, go to `chrome://inspect`, click `Configure...`, and add `127.0.0.1:9227` as an entry in \"Target discovery settings\". Now back to the `chrome://inspect` page and you will find under the `Remote Target #127.0.0.1` group, a new target has been discovered called `/tangerine/server/reporting/run-worker.js`. Click `inspect` and now you should be able to set breakpoints and walk through the code. You may not be able to set breakpoints in all files so use \"step into\" and the `debugger` keyword to get the debugger to the focus you want.\n\n\nIf you want to keep the cache worker running, use watch.\n
    watch -n 1 \"cat /reporting-worker-state.json | node /tangerine/server/reporting/run-worker.js | tee /.reporting-worker-state.json | json_pp && cp /.reporting-worker-state.json /reporting-worker-state.json\"
    If you need to clear a reporting cache, don't simply delete the reporting db. Use\n
    reporting-cache-clear

    You typically need to remove the semaphore before running reporting-cache-clear, especially if there was a crash\n
    rm /reporting-worker-running

    ## A typical report debugging workflow:\n\nRemember to setup config.sh properly! (Make sure  T_MODULES=\"['csv','logstash']\")\nComment out keepAliveReportingWorker in /server/src/app.service.ts.\nRemember to add `127.0.0.1:9228` as an entry in \"Target discovery settings\" in chrome://inspect/#devices\n\nYou may need to add `debugger` before the line of code you wish to debug. \n\ndocker exec into your container\n
    docker exec -it tangerine bash
    Then you'll typically need to rm the reporting-worker-running - it keeps reporting-cache-clear from running if a previous debug session crashed.\n
    rm /reporting-worker-running reporting-cache-clear node --inspect-brk=0.0.0.0:9228 $(which reporting-worker-batch)
    Switch back to Chrome, open `chrome://inspect`. The debugger will be the session that looks like this:\n
    Target /usr/local/bin/reporting-worker-batch file:///tangerine/server/src/scripts/reporting-worker-batch.js Inspect ```

    When it launches, it will wait on the first line of the script for a debugger to connect to it. Click F8 to run. If all is right and good in this world, it will stop at your debugger statement. When the batch has completed, your debugger window will close.

    "},{"location":"developer/debugging_node_apps/","title":"Debugging node apps","text":"

    In develop.sh, the port 9229 should be opend.

    docker run \\\n  -d \\\n  --name tangerine-container \\\n  -p 80:80 -p 5984:5984 -p 9229:9229 \\\n  --env \"DEBUG=1\" \\\n  --env \"NODE_ENV=development\" \\\n  etc...\n

    Add the folloiwng to your node process:

    --inspect=[::]:9229 index.js\n

    for example, reporting/shart.sh:

    nodemon --inspect=[::]:9229 index.js

    using the leh* db for testing workflow csv generation

    http://localhost/app/group-leh_wi_lan_pilot_2018/index.html#assessments

    "},{"location":"developer/deletion-strategy/","title":"Deletion Strategy","text":""},{"location":"developer/deletion-strategy/#introduction","title":"Introduction:","text":"

    To delete a case in Editor, click the \"Trashcan\" button to the right of the case ID in the \"breadcrumbs\" area at the top of the case document. This adds archived:true to the case document as well as all eventForm responses for the case.

    The client search index code filters out docs with the archived:true flag.

    Once you start using this deletion feature, create an update using the following code:

    await window['T'].search.createIndex()\nawait userDb.query('search', { limit: 1 })\n

    This code will rebuild the index on client in order to return the correct results when running a search on a tablet that already has data collected on it.

    "},{"location":"developer/deletion-strategy/#difference-between-delete-and-archive-feature-in-editor","title":"Difference between Delete and Archive feature in Editor","text":"

    In Editor, the \"Delete\" and \"Archive\" buttons both add the archived:true flag to a document, but they do differ in the following ways: - Deleting a case creates \"stub\" documents with minimal properties. The case doc keeps its inputs so search works properly. - A case that has been archived can be un-archived. The only difference between an archived doc and a non-archived doc is the presence of the archived:true flag.

    "},{"location":"developer/deletion-strategy/#deleting-a-record-in-tangerine-manually-on-the-server","title":"Deleting a record in Tangerine manually on the server","text":"

    When using this manual method, deletions on the client do not follow deletions on the server.

    Open the document in Fauxton. Click the \"Delete\" button on the right hand side of the header to delete. This will create a bare-bones document that includes the _deleted\" flag. By default, deleted docs are not included in replication; however, there is a way to query them (see below).

    "},{"location":"developer/deletion-strategy/#testing-deletions","title":"Testing deletions","text":"

    When you open a new case on client, don't enter any data, but click next. The app does create a case and it can be sync'd. Although the case does not display in the case home search results on client, this record does get output on the server via CSV export.

    When you use the Delete button in Fauxton, it removes all data except for this tombstone:

    {\n  \"_id\": \"6c27f5c8-6e08-4245-ae57-cef7d63099de\",\n  \"_rev\": \"2-28819102bb2ec0e3390f28d94467212a\",\n  \"_deleted\": true\n}\n

    When the client syncs, it does not get this new rev.

    "},{"location":"developer/deletion-strategy/#viewing-deletions","title":"Viewing deletions:","text":"
    curl -X POST -H \"content-Type: application/json\" \"http://admin:password@localhost:5984/group-2627a0a7-852a-4f51-9d5d-b7ae53130976/_changes?filter=_selector\" -d '{\"selector\": {\"_deleted\": true}}'\n

    response:

    {\n  \"results\": [\n    {\n      \"seq\": \"16-g1AAAAIBeJyVz0sOgjAQBuABjI-FZ9AjUJpaXMlNtC8CBNuFutab6E30JnqTWh4J0UQDm5nkz8yXmRIAplkgYa6NNlIl2mTmcCxd7DPgC2ttkQVsvHfBhIRrgST5Hv6xzpeu8k0reLWgMBacx32FpBK2reDXAltRRGLcV9hVwvlDEDiKKev7hR65ChfXHHLtFIIICzkdpNwa5d4pVDIs0mG3PBrlWSlQK5HCKUJIwuykpUpzreRf4dUItmBe8QYjK50u\",\n      \"id\": \"6c27f5c8-6e08-4245-ae57-cef7d63099de\",\n      \"changes\": [\n        {\n          \"rev\": \"2-28819102bb2ec0e3390f28d94467212a\"\n        }\n      ],\n      \"deleted\": true\n    }\n  ],\n  \"last_seq\": \"20-g1AAAAIfeJyV0F0OgjAMAOAp_j54Bj0CY5mDJ7mJbusIEtwe1Ge9id5Eb6I3wTFIiBoNvLRJ035pmyOEJqkHaKaNNqBibVKzP-S23OdIzIuiyFKPj3a2MKZ-JDHQz-Yf42Jho1jVQs8JihApRNhWiEthXQt9J_AlwzQkbYVNKZzeBEmCkPG2V-iBjehsk0UujUIx5b5gnZRrpdwahQEnMum2y71SHqWCnBIokmCMAU2PGlSy1Qr-Cs9KcD8ZOiEBSUX09dXsBf6mphA\",\n  \"pending\": 0\n}\n

    To get deleted doc, use the rev: http://localhost:5984/group-2627a0a7-852a-4f51-9d5d-b7ae53130976/6c27f5c8-6e08-4245-ae57-cef7d63099de?rev=2-28819102bb2ec0e3390f28d94467212a

    "},{"location":"developer/deletion-strategy/#how-to-create-deletions-on-client","title":"How to create deletions on client","text":"

    Potential steps: - Use the _changes example above to get a list of deleted docs on the server. - Process the results and use the pouchdb remove function on each doc/rev.

    "},{"location":"developer/deletion-strategy/#restoring-deleted-documents","title":"Restoring deleted documents","text":""},{"location":"developer/deletion-strategy/#using-couchdb-wedge","title":"Using couchdb-wedge","text":"

    Using the restore-deleted-doc command from couchdb-wedge, we can give it a URL, db, and docId to restore.

    npm install -g couchdb-wedge\nwedge restore-deleted-doc --url http://username:password@source-server.com:5984 --db my-db --docId 1234\n
    "},{"location":"developer/deletion-strategy/#using-curl","title":"Using curl","text":"

    Get the revs for the deleted doc:

    curl -H 'Accept: application/json' 'http://server:5984/group-uuid-devices/6013f414-6401-4903-b0f9-fb862779cc3f?revs=true&open_revs=all'\n

    Command returns:

    {\n  \"_id\": \"6013f414-6401-4903-b0f9-fb862779cc3f\",\n  \"_rev\": \"4-46fb2d064595bb6b2068b20450f2a3f9\",\n  \"_deleted\": true,\n  \"_revisions\": {\n    \"start\": 4,\n    \"ids\": [\n      \"46fb2d064595bb6b2068b20450f2a3f9\",\n      \"59e68396a5b632f62e3ab3930dda3d45\",\n      \"09463546fcf1177408821fccada40269\",\n      \"e3af953eab52ed5f3d56c9f57fdeb2f9\"\n    ]\n  }\n}\n

    Get the previous rev id by subtracting 1 from the _revisions.start property and appending the value of the second element in the _revisions.id array:

    Result should be 3-59e68396a5b632f62e3ab3930dda3d45

    Now query the server for that _rev:

    http://server/group-uuid-devices/6013f414-6401-4903-b0f9-fb862779cc3f?rev=3-59e68396a5b632f62e3ab3930dda3d45

    To overwrite the old deleted entry you have to post or put back the document with the correct id and the latest revision number (not the pre delete revision number, but the revision number the document has now it has been deleted).

    An easier way to do this is to use the COPY command:

    curl -X COPY \"server:5984/group-uuid-devices/6013f414-6401-4903-b0f9-fb862779cc3f?rev=3-59e68396a5b632f62e3ab3930dda3d45\" -H \"Destination: 6013f414-6401-4903-b0f9-fb862779cc3f\"

    "},{"location":"developer/development-bullet-points/","title":"Bullet points for Tangerine Development","text":"

    Here are my steps when developing for Tangerine: - Launch ngrok.io to provide https in front of the app. - Check settings in config.sh: o T_HOST_NAME='SOME-NAME.ngrok.io' \u2013 this is critical for sync to work. I don\u2019t think PWA\u2019s work w/ IP addresses - must have a domain name. o T_MODULES=\"['csv','sync-protocol-2, case']\" - Launch Tangerine in developer mode: ./develop.sh - Once it is up, drop to console and do the following to get a docker console: docker exec -it tangerine bash - Create a new group: docker exec tangerine create-group \"New Group D\" case-module - Here is a doc on creating a basic case module group which users sync protocol 2, which has the bi-directional syncing/user mgmt.: https://docs.tangerinecentral.org/developer/creating-clean-dev-content/ - Create a tablet user at Deploy -> Device Users - While you\u2019re in there, go to Deploy -> Devices and create a device. - Go to your Tangerine instance using the URL you configured in T_HOST_NAME. Go to the new group you just created and release a PWA: o Go to \u201cRelease Offline App\u201d. In Web Browser Installation, select \u201cGenerate Test Release\u201d. o Click \u201c Release PWA\u201d button. Copy the url it displays once it has generated the PWA. It should generate the PWA using the T_HOST_NAME. - In Chrome, enter a new Profile (makes life easier when testing\u2026) and paste the URL for the PWA. - In the Device Setup , choose \u201cno\u201d for the Device QR code to scan\u201d. Switch to your Tangerine app, go to Deploy -> Devices and edit the device you created earlier and copy the device ID and Token. - Once it is setup, login w/ admin or the user you created. - To generate cases, look at the Case generation page: https://docs.tangerinecentral.org/developer/load-testing/ o Don\u2019t worry about the substitutions part, just crank out some cases, substituting your group name (something like group-98e646c1-77e5-45ef-8a19-31501c2142a3) for GROUP-UUID below: o docker exec tangerine generate-cases 10 GROUP-UUID o After generating cases, sync!

    "},{"location":"developer/development-bullet-points/#troubleshooting","title":"Troubleshooting","text":"

    If you have problems, check the app-config. File in the group you created. It is at tangerine/data/groups/group-4de0b30c-1c90-4efd-8dcf-e83527109038/client/app-config.json. In the group I setup on the server:

    \"serverUrl\":https://project.server.org/

    PWA\u2019s won\u2019t work for that because I no longer have a load balancer setup nor DNS pointing to that instance. So if things are flaky on the groups you generate, this is a good thing to check. Also check the config.sh for the T_HOST_NAME as noted above.

    To confirm your config.sh settings are correct, your group\u2019s app-config.json should have: \"syncProtocol\":\"2\",\" \"homeUrl\":\"case-home\", \"serverUrl\":https://SERVER.nkgrok.io/

    "},{"location":"developer/docker-network-issues/","title":"Docker Network Issues","text":""},{"location":"developer/docker-network-issues/#overview","title":"Overview","text":"

    If you develop behind a corporate firewall, you may run into issues when building Tangerine from the Dockerfile relating to network access to file resources. Why would this happen? - Your corporate network may use the same ports as the virtual private network that docker creates. - Your local DNS may may configured to use an internal corporate DNS which causes resolution problems when offline.

    If you experience these problems, add the following switches to your docker config file:

    \"default-address-pools\": [\n        {\n            \"base\": \"172.80.0.0/16\",\n            \"size\": 24\n        },\n        {\n            \"base\": \"172.90.0.0/16\",\n            \"size\": 24\n        }\n    ],\n  \"dns\": [\n    \"75.75.75.75\",\n    \"8.8.8.8\"\n  ]\n
    "},{"location":"developer/docker-network-issues/#background","title":"Background","text":""},{"location":"developer/docker-network-issues/#networking","title":"Networking","text":"

    Error:

    bower polymer#^2.0.0                       ECMDERR Failed to execute \"git ls-remote --tags --heads https://github.com/Polymer/polymer.git\", exit code of #128 fatal: unable to access 'https://github.com/Polymer/polymer.git/': gnutls_handshake() failed: The TLS connection was non-properly terminated.\n\nAdditional error details:\nfatal: unable to access 'https://github.com/Polymer/polymer.git/': gnutls_handshake() failed: The TLS connection was non-properly terminated.\n

    Quoting correspondence with a colleague:

    \"Docker creates bridge networks on the set of ranges 172.[17-31].0.0/16 (and some others) by default. If a server had a Docker network on 172.19.0.0/16, it could receive traffic from the VPN, but it would send its response to the bridge network, where it wouldn\u2019t go anywhere.\"

    Fortunately, we can change the default address pools for Docker networks by changing the configuration for the Docker daemon: https://github.com/moby/moby/pull/36396

    I\u2019m setting ours to the default address example in that pull request:\"

        \"default-address-pools\": [\n        {\n            \"base\": \"172.80.0.0/16\",\n            \"size\": 24\n        },\n        {\n            \"base\": \"172.90.0.0/16\",\n            \"size\": 24\n        }\n    ]\n
    "},{"location":"developer/docker-network-issues/#dns","title":"DNS","text":"

    Error:

    request to https://registry.npmjs.org/@vaadin/vaadin-usage-statistics/-/vaadin-usage-statistics-2.1.0.tgz failed, reason: getaddrinfo EAI_AGAIN registry.npmjs.org registry.npmjs.org:443\n

    I think this is where I found this solution:

    https://github.com/npm/npm/issues/16661

    So, I needed to configure the docker DNS config. The first item is my local ISP (Comcast_ DNS server, the second is Google\u2019s.)

    \"dns\": [ \"75.75.75.75\", \"8.8.8.8\" ]

    Change them to your needs.

    Anyway, the good news is that with both the default-address-pools and dns properties in my docker config, my build works both connected and disconnected to the RTI VPN.

    "},{"location":"developer/how-tangerine-is-built/","title":"How Tangerine code is generated","text":"

    When the develop.sh script is run, the Dockerfile builds tangerine into dist/tangerine-client and copies the built code into builds/apk/www/shell and builds/pwa/release-uuid/app.

    "},{"location":"developer/how-tangerine-is-built/#building-files","title":"Building files","text":"

    When Dockerfile is complete, it runs entrypoint-development.sh and watches for changes, sending its output to the dev directory:

    ./node_modules/.bin/ng build --watch --poll 100 --base-href ./ --output-path ./dev \n
    "},{"location":"developer/how-tangerine-is-built/#copy-files","title":"Copy files","text":"

    If you need to make an apk using the updated code, run the following script:

    cd /tangerine/client && \\\nrm -rf builds/apk/www/shell && \\\nrm -rf builds/pwa/release-uuid/app && \\\ncp -r dev builds/apk/www/shell && \\\ncp -r pwa-tools/updater-app/build/default builds/pwa && \\\ncp -r dev builds/pwa/release-uuid/app\n
    "},{"location":"developer/how-tangerine-is-built/#potential-workflow-after-updating-a-lib","title":"Potential workflow after updating a lib","text":"

    In this workflow, you're testing changes to a library such as tangy-form. Make these changes inside the container in the node_modules directory for your library.

    Run ./node_modules/.bin/ng build --base-href ./ --output-path ./dev inside the client dir in the container. It will rebuild all of the libs.

    (Note that each time you run the ng build script above it removes the dev directory before building. This may cause problems when you try to list files in that directory. Do a cd .. & cd dev & ls -ls and all will be good.)

    Next, run the script in the \"Copy files\" section to copy these generated build files to the correct location.

    If this part of the chain is working, then check the output of the file copy process.

    If all is good, release a new APK from the Tangerine UI.

    "},{"location":"developer/how-tangerine-is-built/#tips","title":"Tips","text":"

    The release-apk.sh script shows the steps when building an APK.

    "},{"location":"developer/i18n-translation/","title":"i18n/Translation","text":"

    In Tangerine there are two kinds of translations, content translations and application translations. Content translations are embedded in form content by Editor Users using <t-lang> tags, while application translations are embedded in application level code using the t function in Web Components, _TRANSLATE function in an Angular TS file, or translate pipe in Angular component templates.

    "},{"location":"developer/i18n-translation/#content-translations","title":"Content Translations","text":"

    Translations for specific languages are embedded in content, thus portable and specific to that content. The <t-lang> component (https://github.com/ICTatRTI/translation-web-component) is used to detect the language assigned to the HTML doc. In the following example, the label on the hello input will be \"Hello\" if English is set as the language, \"Bonjour\" if French is selected as the language.

        <tangy-input \n        name=\"hello\"\n        label=\"\n            <t-lang en>Hello</t-lang>\n            <t-lang fr>Bonjour</t-lang>\n        \"\n    >\n    </tangy-input>\n
    "},{"location":"developer/i18n-translation/#application-translations","title":"Application Translations","text":"

    In application code, instead of placing inline translations, a centrally managed JSON file is sourced for replacing strings. At ./translations/translation.fr.json you will find the JSON file use for translations when the French language is selected.

    {\n    \"Accuracy\": \"Pr\u00e9cision\",\n    \"Accuracy Level\": \"Niveau de pr\u00e9cision\",\n    \"Add New User\": \"Ajouter un nouvel utilisateur\",\n    \"Add User to Group\": \"Ajouter un utilisateur \u00e0 un groupe\",\n    ...\n}\n

    You'll also find the Russian translation at ./translations/translation.ru.json.

    {\n    \"Accuracy\": \"\u0410\u043a\u043a\u0443\u0440\u0430\u0442\u043d\u043e\u0441\u0442\u044c\",\n    \"Accuracy Level\": \"\u0423\u0440\u043e\u0432\u0435\u043d\u044c \u0430\u043a\u043a\u0443\u0440\u0430\u0442\u043d\u043e\u0441\u0442\u0438\",\n    \"Add New User\": \"\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043d\u043e\u0432\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\",\n    \"Add User to Group\": \"Add User to Group\",\n    ...\n}\n

    And many more. Each file defines an object where the keys are what to replace in the application and the values are what to replace strings with for that language. Depending on where in the application the string is, there are different techniques for exposing a string to translation.

    In Web Components libraries such as <tangy-form> and <tangy-form-editor>, they use a special t function. Translating strings in template literals looks like...

    this.shadowRoot.innerHTML = `\n  <h1>\n    ${t('Hello')}\n  </h1>\n  ...\n`\n

    Often times Polymer templates are used which won't let you embed functions. In that case, in connectedCallback a this.t object is assembled and then used in the Polymer template.

        connectedCallback() {\n        super.connectedCallback()\n        this.t = {\n            hello: t(\"Hello\")\n        }\n    }\n    template() {\n        return html`\n            [[t.hello]] \n        `\n    }\n

    In Angular Components, the translate pipe is available in templates and _TRANSLATE function for translating in TS files outside of templates.

    <h1>\n    {{'Hello'|translate}}\n</h1>\n
        const helloString = _TRANSLATE('Hello')\n
    "},{"location":"developer/i18n-translation/#application-translation-workflow","title":"Application Translation Workflow","text":"
    1. Add new translatable string(s) to ./translations/translation.en.json.
    2. With develop.sh running, run docker exec tangerine make-translations-consistent to spread this translateable to the other translation json files.
    3. With develop.sh running, run docker exec tangerine export-translations-csvs to spread this translateable to the other translation csv files.
    4. Commit changes to the translations folder.
    5. Send the translations CSVs to corresponding translator.
    6. When all translation CSVs have been updated, with develop.sh running, run docker exec tangerine import-translations-csvs to convert translation CSVs to JSON files.
    7. Add instructions to CHANGELOG upgrade notes that docker exec tangerine translations-update will need to be run to update all groups with updated translation files.
    "},{"location":"developer/i18n-translation/#other-notes","title":"Other notes","text":"

    Mat-pagination needs a special service to enable use of translation.json - see class/_services/mat-pagination-intl.service.ts

    "},{"location":"developer/i18n-translation/#right-to-left-languages-rtl","title":"Right to left languages (RTL)","text":"

    Mat-menu does not support RTL out of the box, but it's simple to get it working: add dir=\"rtl\" to its enclosing element.

    <span dir=\"rtl\">&nbsp;&nbsp;&nbsp;\n  <button mat-button [matMenuTriggerFor]=\"reportsMenu\" class=\"mat-button\">{{'Select Report'|translate}}</button>\n  <mat-menu #reportsMenu=\"matMenu\">\n    <button mat-menu-item [matMenuTriggerFor]=\"groupingMenu\">Class grouping</button>\n  </mat-menu>\n  <mat-menu #groupingMenu=\"matMenu\">\n    <button mat-menu-item *ngFor=\"let item of formList\" routerLink=\"/reports/{{item.id}}/{{item.classId}}\">{{item.title}}</button>\n  </mat-menu>\n</span>\n

    mat-table also needs some twekas to work - Css:

    .mat-column-Name {\n  padding-right:5px;\n}\n\nth.mat-header-cell {\n  text-align: right;\n}\n
    "},{"location":"developer/install-multiple-apks-config/","title":"Installing Multiple Tangerine apps on the same tablet","text":"

    To install more than one Tangerine app on a tablet, you must configure the packageName and appName properties in the group's app-config.json.

    \"packageName\": \"org.rti.tangerine.custom\",\n\"appName\": \"Custom\"\n

    Using these properties would create an APK with the package name org.rti.tangerine.custom. The icon to launch the app would display \"Custom\".

    When uninstalling the app, you would use the updated package name:

    shell script adb uninstall org.rti.tangerine.custom

    If you don't add these properties, the defaults are: - PACKAGENAME = \"org.rti.tangerine\" - APPNAME = \"Tangerine\"

    "},{"location":"developer/load-testing/","title":"Load testing","text":""},{"location":"developer/load-testing/#client-side-testing","title":"Client-side testing","text":"

    Generate a PWA. Go to any case record and enter the following in the js console:

    this.caseService.generateCases(1)\n
    You may change the number of cases generated. It uses the current case as a template for the generated cases. TODO: Use the case-export.json in the group.

    You can check how many docs are in the db with:

    this.userService.getSharedDBDocCount()\n

    Select \"Sync Online\" to test syncing a large recordset.

    "},{"location":"developer/load-testing/#server-side-generation","title":"Server-side generation","text":"

    One may populate a vanilla Tangerine instance with records using the cli:

    docker exec tangerine generate-uploads 500 group-uuid 2000 100\n

    That command generates 500 sets (each of which has 2 records) in batches of 100, posted every 2000 ms. Each doc are generated from templates in server/src/scripts/generate-uploads.

    Add the 'class' switch to the end of that command will generate a studentRegistrationDoc in addition to the other 2 docs. (Read server/src/scripts/generate-uploads/bin.js for more details.)

    You may need to modify the templates to suit the docs you wish to generate.

    "},{"location":"developer/load-testing/#case-generation","title":"Case generation","text":"

    You may create a group for testing using the create-group command. See the creating clean dev conntent doc for more information. There is a case-module option that creates generic case forms. You may also use your own custom group forms.

    Case generation uses a case-export.json file placed in the group directory as the template for record generation. To create this json file, generate a PWA and create a new case. While viewing the case, open the javascript console and use the copy(await this.caseService.export()) command to copy the json. Then paste this data into a case-export.json file. Please note that some groups, such as those created by the case-module mentioned earlier, already have a case-export.json file; however, you may be testing for different scenarios so feel free to create your own.

    Create or modify the custom-generators.js file if you have different variable substitutions. This file exports: - customGenerators: An object that has custom functions you may define - customSubstitutions: An array of substitutions.

    Here is an example of substitutions:

    const substitutions = [\n    {\n        \"type\": \"caseDoc\"\n    },\n    {\n        \"type\": \"demoDoc\",\n        \"formId\": \"registration-role-1\",\n        \"substitutions\": {\n            \"first_name\": {\n                \"functionName\": \"firstname\",\n                \"runOnce\":\"perCase\"\n            },\n            \"last_name\": {\n                \"functionName\": \"surname\",\n                \"runOnce\":\"perCase\"\n            },\n            \"consent\": {\n                \"functionName\": \"yes_no\",\n                \"runOnce\": false\n            }\n        }\n    }\n]\n

    In this example, there are two files that can have variable substitution: - caseDoc - This is the case manifest, which has the doc.type === 'case'. No substitutions are listed for this doc. - demoDoc - This is the demographics form that corresponds to the formId, which is \"registration-role-1\" in the example.

    Since the current case-module example does not have any substitutions happening in the caseDoc inputs, there are no entries for substitutions in it. The demoDoc does have substitutions. The substitutions are key/value pairs. The substitutions key is the variable name of the input you wish to substitute, and the substitutions value is an object that may declare several properties: - functionName: how the function is called - runOnce: if the function is executed when the script is initialized per case, or when each doc is generated. the pre-built randomised field you wish to substitute.

    In this example, first_name is being populated by the firstname function, which is run when each case is generated.

    Case generation also performs other types of randomization. Here are some examples: - firstname: Randomises a female first name - surname: Randomizes a last name - tangerineModifiedOn: Today's date, offet by the running tally of docs being generated. The time is similarly offset. - day: day part of tangerineModifiedOn, padded with 0 if needed. - month: month part of tangerineModifiedOn, padded with 0 if needed. - year: year part of tangerineModifiedOn. - date: year + '-' + month + '-' + day; - participant_id: Random number under 1000000. - participantUuid: A UUID.

    Before generating cases, create a device registration in order to properly generate a location property in the generated docs. When setting location, case generation uses the first doc in the group's devices database. If you don't have one, sync won't work properly. Case generation will fail if there are no device registrations.

    To generate cases, use the following docker command:

    docker exec tangerine generate-cases 1 group-uuid\n

    This would generate one case. Change the number to generate more.

    "},{"location":"developer/load-testing/#clean-things-up","title":"Clean things up","text":"

    To delete all generated records (but keep the views), use bulkdelete.

    "},{"location":"developer/modules/","title":"Tangy Modules","text":"

    Modules provide additional features to Tangerine, such as: - automatically add forms to the client when a new group is created (via groupNew hook) - data transformation for reporting (via flatFormResponse hook)

    Modules: - Class

    Steps to add a module - Create an index.js file inside server/src/modules/moduleName using the sample below as a guide. - Implement any relevant hooks. Available hooks: - flatFormResponse - groupNew - declareAppRoutes - clearReportingCache - reportingOutputs - Forms that need to be copied over to the client should be placed in server/src/modules/moduleName.

    "},{"location":"developer/modules/#activating-modules","title":"Activating modules","text":"

    Add the module name to T_MODULES in config.sh. When a new group is created, the modules listed in T_MODULES will be added to the new group's app-config.json.

    T_MODULES=\"['csv','sync-protocol-2','synapse','case']\"\n

    If you need to add a module to an existing group, modify the modules property in app-config.json/

       \"modules\" : [\n      \"csv\"\n   ],\n
    "},{"location":"developer/modules/#example-module-indexjs","title":"Example module index.js","text":"

    This example from the class module implements the flatFormResponse and groupNew hooks:

    ``` const clog = require('tangy-log').clog const fs = require('fs-extra')

    module.exports = { hooks: { flatFormResponse: function(data) { return new Promise((resolve, reject) => { debugger; let formResponse = data.formResponse let flatFormResponse = data.flatFormResponse if (formResponse.metadata && formResponse.metadata.studentRegistrationDoc && formResponse.metadata.studentRegistrationDoc.classId) { let studentRegistrationDoc = formResponse.metadata.studentRegistrationDoc flatFormResponse[sr_classId] = studentRegistrationDoc.classId; flatFormResponse[sr_student_name] = studentRegistrationDoc.student_name; flatFormResponse[sr_student_id] = studentRegistrationDoc.id; flatFormResponse[sr_age] = studentRegistrationDoc.age; flatFormResponse[sr_gender] = studentRegistrationDoc.gender; } resolve({flatFormResponse, formResponse}) }) }, groupNew: function(data) { return new Promise(async (resolve, reject) => { const {groupName, appConfig} = data clog(\"Setting homeUrl to dashboard and uploadUnlockedFormReponses to true.\") appConfig.homeUrl = \"dashboard\" appConfig.uploadUnlockedFormReponses = true // copy the class forms try { await fs.copy('/tangerine/server/src/modules/class/', /tangerine/client/content/groups/${groupName}) clog(\"Copied class module forms.\") } catch (err) { console.error(err) } resolve(data) }) }, } } ```

    This code will be automatically run when the TangyModules (server/src/modules/index.js) is run.

    "},{"location":"developer/modules/#hooks","title":"Hooks","text":"

    Example:

    const data = await tangyModules.hook('groupNew', {groupName, appConfig})\n
    "},{"location":"developer/reporting-mango-tips/","title":"Tips for making queries for Reports using Mango","text":"

    Mango is a query language available to Couchdb based upon MongoDB.

    "},{"location":"developer/reporting-mango-tips/#general-info-about-mango","title":"General info about Mango:","text":"
    • https://pouchdb.com/guides/mango-queries.html
    • https://docs.couchdb.org/en/stable/api/database/find.html
    • https://github.com/cloudant/mango
    "},{"location":"developer/reporting-mango-tips/#some-things-to-watch-out-for","title":"Some things to watch out for:","text":""},{"location":"developer/reporting-mango-tips/#sorting","title":"Sorting","text":"

    Add the key you're sorting upon - in the following case, it is tangerineModifiedOn - to the index:

     await createIndex({\n    index: {\n      fields: [\n        'type',\n        'status',\n        'tangerineModifiedOn'\n      ]\n    }\n  })\n
    "},{"location":"developer/reporting-mango-tips/#or-and-ne","title":"$or and $ne","text":"

    Mango abandons the indexes and does live queries, which can cause the query to fail. For $ne you can do a partial filter to improve performance.

    Here is a good discussion of the issue w/ these Mango expressions: https://stackoverflow.com/a/41897093

    Here is an example of this issue in Tangerine: #2367

    Example lifted from the Couch doc on Partial Indexes:

    To improve response times, we can create an index which excludes documents where \"status\": { \"$ne\": \"archived\" } at index time using the \"partial_filter_selector\" field:\n\n{\n  \"index\": {\n    \"partial_filter_selector\": {\n      \"status\": {\n        \"$ne\": \"Open\"\n      }\n    },\n    \"fields\": [\"type\", \"status\", \"tangerineModifiedOn\"]\n  },\n  \"ddoc\" : \"type-not-open\",\n  \"type\" : \"json\"\n}\n
    "},{"location":"developer/reverse-proxy-for-developers/","title":"Reverse Proxy for Developers","text":""},{"location":"developer/reverse-proxy-for-developers/#reverse-proxy-software","title":"Reverse proxy software","text":"

    local-ssl-proxy is a Node.js app that can be used to proxy requests from a local development server to a remote server over HTTPS. This is an alternative to using a reverse proxy tunnel service such as ngrok.io or tunnelto.dev.

    "},{"location":"developer/reverse-proxy-for-developers/#generate-ssl-certificates","title":"Generate SSL certificates","text":"

    Here's a nice primer on creating a self-signed SSL certificate: https://deliciousbrains.com/ssl-certificate-authority-for-local-https-development/ I've lifted these examples from that article. Although this example focuses on MacOS, the primer in the link has examples for Linux and Windows.

    In the following example, the code creates a key and cert for a local dev server named tangy.test. Note that this script does not have the -des3 switch, which forces the use of a password, because the script is intended for use with local development servers.

    openssl genrsa -out tangy.test.key 2048

    You'll answer a bunch of questions. The most important one is the Common Name (e.g. server FQDN or YOUR name). Enter the name of your local dev server here, e.g. tangy.test.

    openssl req -new -key tangy.test.key -out tangy.test.csr

    You should now have two files: myCA.key (your private key) and myCA.pem (your root certificate).

    "},{"location":"developer/reverse-proxy-for-developers/#adding-the-root-certificate-to-macos-keychain","title":"Adding the Root Certificate to macOS Keychain","text":"

    sudo security add-trusted-cert -d -r trustRoot -k \"/Library/Keychains/System.keychain\" myCA.pem

    The tutorial has examples of adding the root certs to other devices, which might be handy for Android and IOS development.

    "},{"location":"developer/reverse-proxy-for-developers/#creating-ca-signed-certificates","title":"Creating CA-Signed Certificates","text":"

    Now create tangy.test.ext:

    authorityKeyIdentifier=keyid,issuer\nbasicConstraints=CA:FALSE\nkeyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment\nsubjectAltName = @alt_names\n\n[alt_names]\nDNS.1 = tangy.test\n

    The final step:

    openssl x509 -req -in tangy.test.csr -CA myCA.pem -CAkey myCA.key \\ -CAcreateserial -out tangy.test.crt -days 825 -sha256 -extfile tangy.test.ext

    We now have three files: tangy.test.key (the private key), tangy.test.csr (the certificate signing request, or csr file), and tangy.test.crt (the signed certificate). We can configure local web servers to use HTTPS with the private key and the signed certificate.

    "},{"location":"developer/reverse-proxy-for-developers/#using-local-ssl-proxy","title":"Using local-ssl-proxy","text":"

    At this point you can launch Tangerine, which will respond to requests on port 80. Then launch local-ssl-proxy:

    local-ssl-proxy --source 443 --target 80 --cert ~/ssl/server.crt --key ~/ssl/server.key

    You should be able to access Tangerine via https://localhost. Next step - configure your local dev domain in DNS:

    "},{"location":"developer/reverse-proxy-for-developers/#dns-settings","title":"DNS settings","text":"

    Add your local dev domain to /etc/hosts. The domain 'tangy.test' is used in this example; replace with your own domain:

    ##\n# Host Database\n#\n# localhost is used to configure the loopback interface\n# when the system is booting.  Do not change this entry.\n##\n127.0.0.1       localhost\n255.255.255.255 broadcasthost\n::1             localhost\n127.0.0.1       tangy.test\n

    Now you should be able to access Tangerine using https://tangy.test`.

    "},{"location":"developer/supporting-custom-elements/","title":"Supporting custom elements and external libs","text":""},{"location":"developer/supporting-custom-elements/#adding-new-elements","title":"Adding new elements","text":"

    To add a new custom element or to add support for new polymer or other web components, you must make them accessible to Angular: - add to package.json - import into polyfills

    You also need to add CUSTOM_ELEMENTS_SCHEMA to your module to support custom tags in your templates:

    schemas: [CUSTOM_ELEMENTS_SCHEMA],\n
    "},{"location":"developer/supporting-custom-elements/#manual-imports","title":"Manual imports","text":"

    Some libs need to be imported manually because they are not available as ES6 modules. Add the lib using a script tag -

    <script src=\"./libs/plotly-latest.min.js\"></script>\n

    and then add to angular.json:

    \"assets\": [\n    \"src/libs/plotly-latest.min.js\"\n]\n
    "},{"location":"developer/supporting-custom-elements/#resolving-incompatibilities","title":"Resolving incompatibilities","text":"

    The \"skipLibCheck\": true switch in tscondig.json will causes type checking of declaration files (files with extension .d.ts) to be skipped. (Stack Overflow discussion) If you run into an error such as ERROR in node_modules/tangy-form/tangy-form-response-model.d.ts:18:17 - error TS1039: Initializers are not allowed in ambient contexts. this switch may be useful to getting the app to compile. We decided to not use it - simply removing it worked fine - but if it's a choice between the app compiling or not, it's worth using.

    "},{"location":"developer/sync-sessions/","title":"Sync Sessions","text":"

    A deviceToken, which is persisted in the client device record, is used for authentication with the server and is passed in the syncSessionUrl by sync.service. This is passed using the following code:

    const syncSessionInfo = <SyncSessionInfo>await this.http.get(`${syncDetails.serverUrl}sync-session-v2/start/${syncDetails.groupId}/${syncDetails.deviceId}/${syncDetails.deviceToken}`).toPromise()\n

    The syncSessionService.start() method verifies the token, starts a sync session, and returns the following object:

    <SyncSessionInfo>{\n        syncSessionUrl: `${config.protocol}://${syncUsername}:${syncPassword}@${config.hostName}/db/${groupId}`,\n        deviceSyncLocations: device.syncLocations\n      }\n

    This syncSessionUrl is used to create the connection to the Couchdb for replication.

    "},{"location":"developer/tangerine-dev-tutorial-notes/","title":"Commands","text":""},{"location":"developer/tangerine-dev-tutorial-notes/#create-group-and-generate-casees","title":"Create group and generate case(es)","text":"
    docker exec tangerine create-group \"CM-1\" case-module\n\ndocker exec tangerine generate-cases 1 group-09a2a880-1317-4cbf-b944-0fd059fa7007\n
    "},{"location":"developer/tangerine-dev-tutorial-notes/#open-docker-shell","title":"Open docker shell","text":"
    docker exec -it tangerine bash\n
    "},{"location":"developer/tangerine-dev-tutorial-notes/#refresh-code","title":"Refresh code","text":"

    Run this inside tangerine docker shell

    cd /tangerine/client && rm -rf builds/apk/www/shell \n&& rm -rf builds/pwa/release-uuid/app && cp -r dev builds/apk/www/shell \n&& cp -r pwa-tools/updater-app/build/default builds/pwa \n&& cp -r dev builds/pwa/release-uuid/app\n
    "},{"location":"developer/tangerine-dev-tutorial-notes/#adb-commands","title":"ADB commands","text":"
    adb devices\n
    adb install data/client/releases/prod/apks/group-09a2a880-1317-4cbf-b944-0fd059fa7007/platforms/android//app/build/outputs/apk/debug/app-debug.apk\n
    adb uninstall org.rti.tangerine\n
    "},{"location":"developer/tangerine-globals/","title":"Globals in Tangerine","text":""},{"location":"developer/tangerine-globals/#globals-in-memory","title":"Globals in memory","text":"

    In-memory globals won't survive refreshing the browser.

    We are caching important configuration files (app-config.json, forms.json, location-list.json) to avoid having to keep fetching those docs from the db.

    Use the following code to take advantage of this caching: - await this.appConfigService.getLocationList(); - await this.tangyFormsInfoService.getFormsInfo(); - await this.appConfigService.getAppConfig; - await this.tangyFormService.getFormMarkup(this.eventFormDefinition.formId);

    CaseDefinitionsService also has implements of caseDefinitions, but that is not exposed publicly. More info in this PR: https://github.com/Tangerine-Community/Tangerine/pull/1991

    "},{"location":"developer/tangerine-globals/#globals-that-are-stored-in-a-database","title":"Globals that are stored in a database","text":"

    Database variables will persist after page refreshes or app reboots.

    Use VariableService. Stores data in 'tangerine-variables' pouchdb as a key/value pair. The key is the _id in the doc. The value can be a string, JSON object, or any other data type that can be persisted in a pouchdb.

    await this.variableService.set('tangerine-device-is-registered', true)\n
    await this.variableService.get('tangerine-device-is-registered')\n
    "},{"location":"developer/tangerine-globals/#widely-used-configuration-variables","title":"Widely-used Configuration Variables","text":""},{"location":"developer/tangerine-globals/#server","title":"Server","text":"

    They are not globals, but they are mighty useful. The TangerineConfigService provides variables set in config.sh. Expose it in your constructor:

    private readonly configService: TangerineConfigService,\n
    And then you may use it:

    const userOneUsername = this.configService.config().userOneUsername\n
    "},{"location":"developer/tangerine-globals/#client","title":"Client","text":"

    Use await this.appConfigService.getAppConfig; to fetch app-config.json settings in client.

    "},{"location":"developer/testing-conflicts/","title":"Conflicts","text":"

    The goal is to follow the CRDT (conflict-free replicated data type) pattern in resolving conflicts. When the app tries to merge two conflicting records, how should it sync the conflicting values: which value should win? The afore-mentioned Wikipedia page offers some guidance: \"As an example, a one-way Boolean event flag is a trivial CRDT: one bit, with a value of true or false. True means some particular event has occurred at least once. False means the event has not occurred. Once set to true, the flag cannot be set back to false. (An event, having occurred, cannot un-occur.) The resolution method is \"true wins\": when merging a replica where the flag is true (that replica has observed the event), and another one where the flag is false (that replica hasn't observed the event), the resolved result is true \u2014 the event has been observed.\"

    We have not yet reached this level of conflict resolution. We have first started with comparing data from Event Forms, detecting some basic conflicts such as missing formResponseId, complete, or required properties or detecting if there is a new event form and then merging according to rules specific to each difference. In general, the event form conflicts are resolved by adding the missing property or form. For metadata that are in conflict, the most recent metadata is merged (wins). There is also a check for new events.

    Unit tests are available that test the conflicts mentioned above. You can also create scenarios on a tablet.

    "},{"location":"developer/testing-conflicts/#testing-conflicts-on-a-tablet","title":"Testing Conflicts on a tablet","text":""},{"location":"developer/testing-conflicts/#tips","title":"Tips","text":"

    After each scenario, it is useful to run Sync to make sure that no more docs need to be sync'd:

    Status: Complete\n   Pulled from the server: 0\n   Pushed to the server: 0\n

    "},{"location":"developer/testing-conflicts/#supported-scenarios","title":"Supported Scenarios","text":""},{"location":"developer/testing-conflicts/#difftype-eventform-tablet-1-opens-but-doesnt-complete-event-form-tablet-2-opens-and-completes-event-form","title":"DiffType: EventForm - Tablet 1 opens but doesn't complete Event Form, Tablet 2 opens and completes Event Form","text":"

    Steps

    Setup: - Create a new case with pwa1. Fill out \"Registration for Role 1\" - enter '0' for \"How many participant of type Role 2 would you like to enroll in this case?\", submit. Create a New Event of type \"An Event with an event form you can delete\". Sync. - In PWA2, sync.

    Create a divergence: - In PWA1, open the form and exit (don't submit form). This should create a diverging tree in the revisions. - In PWA2, Enter the case you just synced. Enter the event of type \"An Event with an event form you can delete\" and complete the form in \"An Event with an Event Form you can delete.\" Sync.

    Syncing to create the conflict: - In PWA1, Sync. This should create a conflict. Note that Sync status displays \"Conflicts detected.\" This conflict is resolved on the client and sync'd to the server. - Check data/issues on server. There should be a new issue, which should display the following:

    Merged: true\nDiffTypes:\n    (1) DIFF_TYPE__METADATA\n
    - In PWA2, sync. This should NOT create a conflict. - Check to see that data is identical on both PWA's.

    "},{"location":"developer/testing-conflicts/#difftype-event-tablet-1-creates-an-new-event-and-tablet-2-creates-a-new-event","title":"DiffType: Event - Tablet 1 creates an new Event and Tablet 2 creates a new Event","text":"

    Steps

    Setup: - Create a new case with PWA1. Fill out \"Registration for Role 1\" - enter '0' for \"How many participant of type Role 2 would you like to enroll in this case?\", submit. Sync - In PWA2, sync. Enter the case you just synced. Create a New Event of type \"An Event with an event form you can delete\" and complete the form in \"An Event with an Event Form you can delete.\" Sync.

    Create a divergence: - In PWA1, enter the same case (don't sync yet) and create a New Event of type \"An Event with an event form you can delete\". Complete the form in \"An Event with an Event Form you can delete.\"

    Syncing to create the conflict: - In PWA1, sync. This should create a conflict. Note that Sync status displays \"Conflicts detected.\" This conflict is resolved on the client and sync'd to the server. - Check data/issues on server. There should be a new issue, which should display the following:

    Merged: true\nDiffTypes:\n\n    (1) DIFF_TYPE__EVENT\n    (1) DIFF_TYPE__EVENT_FORM\n    (1) DIFF_TYPE__METADATA\n

    • Check the case on PWA1. There should be 2 instances of \"An Event with an Event Form you can delete\" - one from PWA1, and another from PWA2.
    • In PWA2, sync. This should create a conflict. Note that Sync status displays \"Conflicts detected.\" This conflict is resolved on the client and sync'd to the server.
    • Check data/issues on server. There should be a new issue, which should display the following:
      Merged: true\nDiffTypes:\n\n    (1) DIFF_TYPE__EVENT\n    (1) DIFF_TYPE__EVENT_FORM\n    (1) DIFF_TYPE__METADATA\n
    • Check the case on PWA2. There should be 2 instances of \"An Event with an Event Form you can delete\" - one from PWA1, and another from PWA2.
    "},{"location":"developer/testing-conflicts/#difftype-eventform-tablet-1-creates-a-new-event-form-and-tablet-2-makes-some-other-change","title":"DiffType: EventForm - Tablet 1 creates a new Event Form and Tablet 2 makes some other change","text":"

    Steps

    Setup: - Create a new case with PWA1. Fill out \"Registration for Role 1\" - enter '0' for \"How many participant of type Role 2 would you like to enroll in this case?\", submit. Create a New Event of type \"An Event with an event form you can delete\".\" Open that new event but do not click on the form \"An Event Form you can delete\". Sync. - In PWA2, sync. Enter the case you just synced. View the \"Registration for Role 1\" form. Sync.

    Create a divergence: - In PWA1, Enter the same case (don't sync yet) and enter a New Event of type \"An Event with an event form you can delete\". Complete the form in \"An Event with an Event Form you can delete.\"

    Syncing to create the conflict: - In PWA1, sync. This should create a conflict. Note that Sync status displays \"Conflicts detected.\" This conflict is resolved on the client and sync'd to the server. - Check data/issues on server. There should be a new issue, which should display the following:

    Merged: true\nDiffTypes:\n\n(1) DIFF_TYPE__METADATA\n
    - In PWA2, sync. This should create a conflict. Note that Sync status displays \"Conflicts detected.\" This conflict is resolved on the client and sync'd to the server. - Check data/issues on server. There should be a new issue, which should display the following:
    Merged: true\nDiffTypes:\n\n    (1) DIFF_TYPE__METADATA\n
    - Check the case on PWA2. There should be 1 instances of \"An Event with an Event Form you can delete\" - with the form completed from PWA1

    "},{"location":"developer/testing-conflicts/#difftype-metadata-change-location-on-tablet-1-and-tablet-2","title":"DiffType: Metadata - Change location on Tablet 1 and Tablet 2","text":"

    Steps: - In PWA1, pull up the case you just created. Submit a \"Change Location of Case\" form, setting it for Facility 1. Don't Sync. - In PWA2, pull up the same case. Submit a \"Change Location of Case\" form, setting it for Facility 2. Sync. - In PWA1, sync. Note the error displayed:

    Status: Error\n4 docs synced; 0 pending; ERROR: \"Document update conflict\"\n
    Sync again. Note that Sync status displays \"Conflicts detected.\" This conflict is resolved on the client and sync'd to the server.

    • In PWA1, pull up the case. Note that there are two \"Change location of case\" forms, one for Facility 1 and another for Facility 2. In the js console, enter T.case._case.location.facility. It should display \"K0xhy1Su\".
    • In PWA2, sync. Note the error displayed:

      Status: Error\n4 docs synced; 0 pending; ERROR: \"Document update conflict\"\n
      Sync again. Note that Sync status displays \"Conflicts detected.\" This conflict is resolved on the client and sync'd to the server.

    • In PWA2, pull up the case. Note that there are two \"Change location of case\" forms, one for Facility 1 and another for Facility 2. In the js console, enter T.case._case.location.facility. It should display \"K0xhy1Su\".

    "},{"location":"developer/testing-conflicts/#difftype-metadata-modify-case-variables-on-tablet-1-and-tablet-2","title":"DiffType: Metadata - Modify Case variables on Tablet 1 and Tablet 2","text":"

    TODO: Create a form in the Case Module that uses setVariable and getVariable function

    "},{"location":"developer/testing-conflicts/#scenarios-not-yet-supported","title":"Scenarios not yet supported","text":""},{"location":"developer/testing-conflicts/#difftype-eventform-tablet-1-removes-an-event-form-and-tablet-2-makes-some-other-change","title":"DiffType: EventForm - Tablet 1 removes an Event Form and Tablet 2 makes some other change","text":""},{"location":"developer/testing-conflicts/#difftype-eventform-tablet-1-makes-an-event-form-required-and-tablet-2-makes-some-other-change","title":"DiffType: EventForm - Tablet 1 makes an Event Form required and Tablet 2 makes some other change","text":""},{"location":"developer/testing-conflicts/#difftype-eventform-tablet-1-makes-adds-an-event-form-variable-and-tablet-2-makes-some-other-change","title":"DiffType: EventForm - Tablet 1 makes adds an Event Form variable and Tablet 2 makes some other change","text":""},{"location":"developer/testing-conflicts/#difftype-eventform-tablet-1-makes-modifies-existing-event-form-variable-and-tablet-2-makes-some-other-change","title":"DiffType: EventForm - Tablet 1 makes modifies existing Event Form variable and Tablet 2 makes some other change","text":""},{"location":"developer/testing-conflicts/#difftype-eventform-tablet-1-makes-modifies-existing-event-form-variable-and-tablet-2-modifies-the-same-event-form-variable-with-same-value","title":"DiffType: EventForm - Tablet 1 makes modifies existing Event Form variable and Tablet 2 modifies the same Event Form variable with same value","text":""},{"location":"developer/testing-conflicts/#difftype-eventform-tablet-1-makes-modifies-existing-event-form-variable-and-tablet-2-modifies-the-same-event-form-variable-with-different-value","title":"DiffType: EventForm - Tablet 1 makes modifies existing Event Form variable and Tablet 2 modifies the same Event Form variable with different value","text":""},{"location":"developer/testing-conflicts/#exploring-unexpected-sync-conflicts","title":"Exploring unexpected sync conflicts","text":""},{"location":"developer/testing-conflicts/#difftype-metadata-two-cases-view-the-same-case-but-make-no-modification","title":"DiffType: Metadata - Two cases view the same case but make no modification","text":"

    A metadata conflict is easy to create: whenever a case is viewed, its metadata is modified.

    Steps: - launch 2 PWA's with the group, based on the case module - docker exec tangerine create-group \"Test Auto-merge 1\" case-module - consider editing the \"Registration for Role 1\" \"Registration\" section by changing the QR code into an input, just to make testing easier. - Create a new case with pwa1. Fill out \"Registration for Role 1\" - enter '0' for \"How many participant of type Role 2 would you like to enroll in this case?\", submit, and sync. - in PWA2, sync, and open the new case. Create a New Event of type \"An Event with an event form you can delete\" . Go into the event and form and submit the \"An Event Form you can delete\" form. Sync. Notice that so far, no new conflicts have been created. - In PWA1, sync. Note that Sync status displays \"Conflicts detected.\" Check data/issues on server - should be type (1) DIFF_TYPE__METADATA. Merged: true. - In PWA2, sync. Note that Sync status displays \"Pulled from the server: 1\".

    "},{"location":"developer/testing-conflicts/#difftype-eventform-data-conflict-1-dont-touch-the-event","title":"DiffType: EventForm - data conflict 1 - Don't touch the event","text":"

    So far, this has not made a conflict for me...

    Steps: - Create a new case with pwa1. Fill out \"Registration for Role 1\" - enter '0' for \"How many participant of type Role 2 would you like to enroll in this case?\", submit. On the same case, create a New Event of type \"An Event with an event form you can delete\". Don't view that event or enter data in its form. Sync. - In PWA2, sync. Enter the case you just synced and complete the form in \"An Event with an Event Form you can delete.\" Sync. - In PWA1, sync. The new form does not appear. Do a hard refresh. The new form should now appear in the case. Sync. - In PWA2, sync. Conflicts arise. Or not. Check data/issues on server - should be type (1) DIFF_TYPE__METADATA. Merged: true. There is a 50/50 chance this record won't have a conflict...

    "},{"location":"developer/testing-conflicts/#difftype-eventform-data-conflict-2-touch-the-event","title":"DiffType: EventForm - data conflict 2 - Touch the event","text":"

    So far, this has not made a conflict for me...

    Steps: - Create a new case with pwa1. Fill out \"Registration for Role 1\" - enter '0' for \"How many participant of type Role 2 would you like to enroll in this case?\", submit. On the same case, create a New Event of type \"An Event with an event form you can delete\". View the event, but don't view the form. Sync. - In PWA2, sync. Enter the case you just synced and complete the form in \"An Event with an Event Form you can delete.\" Sync. - In PWA1, sync. The new form does not appear. Do a hard refresh. The new form should now appear in the case. Sync. - In PWA2, sync. Conflicts arise. Or not. Check data/issues on server - should be type (1) DIFF_TYPE__METADATA. Merged: true. There is a 50/50 chance this record won't have a conflict...

    "},{"location":"developer/testing-conflicts/#difftype-eventform-data-conflict-3-open-but-dont-save-the-form","title":"DiffType: EventForm - data conflict 3 - Open but don't save the form","text":"

    Steps: - Create a new case with pwa1. Fill out \"Registration for Role 1\" - enter '0' for \"How many participant of type Role 2 would you like to enroll in this case?\", submit. On the same case, create a New Event of type \"An Event with an event form you can delete\". View the event, then view the form, but don't submit it. Sync. - In PWA2, sync. Enter the case you just synced and complete the form in \"An Event with an Event Form you can delete.\" Sync. - In PWA1, sync. The new form does not appear. Do a hard refresh. The new form should now appear in the case. Sync. - In PWA2, sync. Conflicts arise. Or not. Check data/issues on server - should be type (1) DIFF_TYPE__METADATA. Merged: true. There is a 50/50 chance this record won't have a conflict... - So far, this has not made a conflict for me...

    "},{"location":"developer/upgrades/","title":"Upgrades","text":""},{"location":"developer/upgrades/#implementing-upgrades","title":"Implementing Upgrades","text":"

    There are two ways to implement upgrades to Tangerine configuration or databases that cannot be automatically upgraded: - via a shell script, runnable via docker exec -it tangerine /tangerine/server/src/upgrade/v3.9.0.js - adding to the updates.ts array. This will run automatically when the client app starts if a new version was deployed.

    "},{"location":"developer/upgrades/#upgrading-a-server","title":"Upgrading a server","text":"

    You must run each version's server image before running its upgrade. for example, if you are upgrading from v3.6.4 to v3.13.0, follow these steps:

    git clone https://github.com/Tangerine-Community/Tangerine.git\ncd Tangerine\ngit fetch origin\ngit checkout v3.6.4\ncp config.defaults.sh config.sh\n# configure T_HOST_NAME, T_PROTOCOL (https), and T_MODULES (csv)\n./start.sh v3.6.4\ndocker exec -it tangerine reporting-cache-clear\ngit checkout v3.7.1\n./start.sh v3.7.1\ndocker exec tangerine translations-update\ngit checkout v3.8.0\n./start.sh v3.8.0\ndocker exec -it tangerine /tangerine/server/src/upgrade/v3.8.0.js\ngit checkout v3.8.1\nmv config.sh config.sh_backup\ncp config.defaults.sh config.sh\n# To edit both files in vim you would run...\nvim -O config.sh config.sh_backup\n# No upgrade script for this relese.\ngit checkout v3.9.0\n./start.sh v3.9.0\ndocker exec -it tangerine /tangerine/server/src/upgrade/v3.9.0.js\ngit checkout v3.10.0\n./start.sh v3.10.0\ndocker exec -it tangerine /tangerine/server/src/upgrade/v3.10.0.js\ngit checkout v3.11.0\n./start.sh v3.11.0\ndocker exec -it tangerine /tangerine/server/src/upgrade/v3.11.0.js\n# There was a bug in 3.11.0 that causes a blank screen in earlier APK's\n# It is resolved in v3.12.\ngit checkout v3.12.0\n./start.sh v3.12.0\n# Run upgrade\ndocker exec -it tangerine reporting-cache-clear \ngit checkout v3.13.0\n./start.sh v3.13.0\ndocker exec -it tangerine reporting-cache-clear \ndocker exec -it tangerine /tangerine/server/src/upgrade/v3.13.0.js\n# Remove previous Tangerine version's Docker images.\ndocker rmi tangerine/tangerine:v3.6.4\ndocker rmi tangerine/tangerine:v3.8.0\ndocker rmi tangerine/tangerine:v3.8.1\ndocker rmi tangerine/tangerine:v3.9.0\ndocker rmi tangerine/tangerine:v3.10.0\ndocker rmi tangerine/tangerine:v3.11.0\ndocker rmi tangerine/tangerine:v3.13.0\n
    "},{"location":"developer/upgrades/#limits-to-upgrades","title":"Limits to Upgrades","text":"

    Read the instructions in the CHANGELOG.md.

    If testing a pre-v3.8 APK, it will fail to run on v3.8 or higher due to lack of newer Cordova plugins.

    "},{"location":"developer/upgrades/#update-tips","title":"Update tips","text":"

    Be aware that sync-protocol 2 uses a shared database; therefore, you don't want to do the same update whenever a different user logs in.

    The requiresViewsRefresh property will update All Default User Docs, which may place too much load on the tablet when it re-indexes those views.

    The following code checks for that scenario and show how to update a single view:

      {\n    requiresViewsUpdate: false,\n    script: async (userDb, appConfig, userService: UserService) => {\n      // syncProtocol uses a single shared db for all users. Update only once.\n      if (appConfig.syncProtocol === '2' && localStorage.getItem('ran-update-v3.9.0')) return\n      console.log('Updating to v3.9.0...')\n      await userDb.put(TangyFormsDocs[0])\n      await userDb.query('responsesUnLockedAndNotUploaded')\n      localStorage.setItem('ran-update-v3.9.0', 'true')\n    }\n
    "},{"location":"developer/viewing-forms-and-data/","title":"Viewing Forms and Form Data","text":"

    Use TangyFormService to retrieve form definitions and response data. The revision is used to get the correct version of the form.

        this.formResponse = await this.tangyFormService.getResponse(this.eventForm.formResponseId)\n    const tangyFormMarkup = await this.tangyFormService.getFormMarkup(this.eventFormDefinition.formId, this.formResponse.formVersionId)\n

    But there are other ways of getting data out of Tangerine. First you need to see where you are getting data from.

    "},{"location":"developer/viewing-forms-and-data/#mapping-of-components-to-forms","title":"Mapping of components to forms","text":"

    EventFormListItemComponent - listing of forms in an event CaseEventListItemComponent - listing of events (such as Followup ANC Visits) in a case.

    "},{"location":"developer/viewing-forms-and-data/#helper-functions-already-in-components","title":"Helper functions already in components","text":"

    In the component for a list, helper functions may already expose the properties you need to populate a template. In EventFormListItemComponent, notice the variable exposed:

        const response = await this.formService.getResponse(this.eventForm.formResponseId)\n    const getValue = (variableName) => {\n// more code inside getValue();\n      }, {})\n// snip\n    const caseInstance = this.case\n    const caseDefinition = this.caseDefinition\n    const caseEventDefinition = this.caseEventDefinition\n    const caseEvent = this.caseEvent\n    const eventForm = this.eventForm\n    const eventFormDefinition = this.eventFormDefinition\n    const formatDate = (unixTimeInMilliseconds, format) => moment(new Date(unixTimeInMilliseconds)).format(format)\n    const TRANSLATE = _TRANSLATE\n    eval(`this.renderedTemplateListItemIcon = this.caseDefinition.templateEventFormListItemIcon ? \\`${this.caseDefinition.templateEventFormListItemIcon}\\` : \\`${this.defaultTemplateListItemIcon}\\``)\n    eval(`this.renderedTemplateListItemPrimary = this.caseDefinition.templateEventFormListItemPrimary ? \\`${this.caseDefinition.templateEventFormListItemPrimary}\\` : \\`${this.defaultTemplateListItemPrimary}\\``)\n    eval(`this.renderedTemplateListItemSecondary = this.caseDefinition.templateEventFormListItemSecondary ? \\`${this.caseDefinition. v}\\` : \\`${this.defaultTemplateListItemSecondary}\\``)\n
    If there is not a response for a form, response will be false; therefore, if you do a getValue() in your template, be sure to test if response is true.

    If you wish to display the startDatetime in your template, note that is is part of the response object - it is returned as response.startDatetime. In other cases - for values inside the form - use getValue(variableName) - but test if response is true first! Also, remember that the variableName is one of the id's in the inputs array, which is inside each item in the items array.

    "},{"location":"developer/viewing-forms-and-data/#testing-your-templates","title":"Testing your templates","text":"

    Here's an easy way to test your template code: in the js console, use the copy() function to copy the value for your template:

    copy(this.caseDefinition.templateEventFormListItemSecondary)\n
    Then add the fields or functions you need. In this case, I'm adding a getValue:

    `<t-lang en>Status</t-lang><t-lang fr>Statut</t-lang>: ${!eventForm.complete ? '<t-lang en>Incomplete</t-lang><t-lang fr>Incompl\u00e8te</t-lang>' : '<t-lang en>Complete</t-lang><t-lang fr>Achev\u00e9e</t-lang>'} ${response ? `Version: ${getValue(\"content_release_version\")}`: ''}`\n

    Output:

    \"<t-lang en>Status</t-lang><t-lang fr>Statut</t-lang>: <t-lang en>Complete</t-lang><t-lang fr>Achev\u00e9e</t-lang> Start date: 3/13/2020, 11:25:19 AM\"\n

    Note that I was testing for existence of response, and also nesting templates to show the \"Version\" text if there was a value for content_release_version.

    Another example:

    <t-lang en>Status</t-lang><t-lang fr>Statut</t-lang>: ${!eventForm.complete ? '<t-lang en>Incomplete</t-lang><t-lang fr>Incompl\u00e8te</t-lang>' : '<t-lang en>Complete</t-lang><t-lang fr>Achev\u00e9e</t-lang>'} ${response ?Start date: ${response.startDatetime}: ''}

    "},{"location":"developer/viewing-forms-and-data/#debugging-templates","title":"debugging templates","text":"

    To make the dev tool stop on a breakpoint in a Case Definition's template, add the following debugger statement to the content of the template.

    ${(()=>{debugger})()}\n

    When that template loads, the Chrome devtools will pause and you can inspect local variables/functions available and try running them in the console. Note that different templates will have different helper functions and variables available.

    "},{"location":"editor/","title":"Editor Quick Links","text":""},{"location":"editor/#form-development-guides","title":"Form Development Guides","text":"
    • Getting Started with Editor
    • Tangerine Form Developer's Cookbook
    • Local Content Development with Tangerine Preview
    • Configuration Guide
    • The Tangerine Preview tool for advanced users writing forms in HTML and using Git for version control
    • Form Versions
    • Password Policy
    • Reserved words in Tangerine
    "},{"location":"editor/#case-module","title":"Case Module","text":"
    • Case Management Data Model
    • Case Module Cookbook
    • Configuring Case functionality
    • Custom Case Reports
    "},{"location":"editor/content-sets/","title":"Content sets","text":""},{"location":"editor/content-sets/#content-sets","title":"Content Sets","text":"

    Content Sets are groups of forms and configuration you can use as a template for new groups.

    "},{"location":"editor/content-sets/#anatomy-of-a-content-set","title":"Anatomy of a Content Set","text":""},{"location":"editor/content-sets/#version-1-tangerine-v3130","title":"Version 1 (< Tangerine v3.13.0)","text":"

    In the root directory of a v1 Content Set, you will find the following: - ./app-config.json_example (required) - ./forms.json (required)

    "},{"location":"editor/content-sets/#version-2-tangerine-v3130","title":"Version 2 (> Tangerine v3.13.0)","text":"

    Starting in Tangerine v3.13.0, the second iteration of Content Sets was launched. In the root directory of a v2 Content Set, you will find the following:

    • ./client/ (required): The folder containing content that will be deployed to Tablets.
    • ./client/app-config.defaults.json (required): Defaults to use for app-config.json. For example, a Case Module enabled group would have a \"homeUrl\" property with a value of \"case-home\".
    • ./client/forms.json (required)
    • ./editor/ (required): A folder containing assets pertanent to how Editor behaves.
    • ./editor/index.html (required): The file loaded when displaying the Dashboard in a group's Editor.
    • ./README.md (suggested)
    • ./docs/ (suggested)
    "},{"location":"editor/content-sets/#version-21-tangerine-3150","title":"Version 2.1 (>= Tangerine 3.15.0)","text":"

    Starting in Tangerine v3.13.0, content sets gained a package.json and build step. The package.json specifies the required libs and scripts for the content set. If the content set uses custom scripts, these scripts are compiled by the included webpack. Note that .gitignore ignores the compiled code - client/custom-scripts.js - to avoid conflicts.

    To install the 2.1 content set - instead of npm install, run npm run install-server.

    Be sure to update any cron jobs to include the new build commands if they are using content set 2.1:

    git pull npm rn install-server npm build

    If using Content set 2, this build process is not necessary. However, it is an advantage for cs2 users to upgrade to be able to pin the webpack version in packagejson and also to not have the huge custom-scripts file.

    "},{"location":"editor/content-sets/#creating-a-new-content-set","title":"Creating a new Content Set","text":""},{"location":"editor/content-sets/#importing-a-content-set-into-tangerine","title":"Importing a Content Set into Tangerine","text":"

    New group set: - git clone tangerine starter repo - Modify content as needed - Push to Git - On server instance, setup GH deploy key by navigating to your Repository on Github and click on Settings -> Deploy keys -> Add deploy key and paste your Docker instances /root/.ssh/id_rsa.pub in the key contents, enable \"Allow write access\" and save. Run docker exec tangerine create-group \"New Group A\" https://github.com/id/tangerine-content.git using the cli - Add crontab entry that uses the non-pub key to do the pull, npm run install-server, npm run build.

    "},{"location":"editor/custom-apps/","title":"Custom Apps","text":""},{"location":"editor/custom-apps/#creating-a-custom-app","title":"Creating a Custom App","text":"
    cd tangerine/content-sets\ncp -r custom-app my-app\ncp ../translations/* my-app/client/\ncd my-app\ngit init\ngit add .\ngit commit -m \"First.\"\ngit branch -M main\ngit remote add origin <your apps origin> \ngit push -u origin main\nnpm install\nnpm start\n
    "},{"location":"editor/form-versions/","title":"Form Versions","text":"

    Throughout the lifetime of a form, many versions of a form may be deployed. When reviewing form responses collected on a past version of a form, it's important to open that form response using the version of the form it was collected on. When filling out a form response, it helps to think of the form response as a clear plastic sheet that you are writing on over the paper copy of the form. If the questions on that underlying physical form are removed, moved, or new questions are added, the clear plastic sheet you filled out previous form responses on no longer overlays correctly on the updated paper copy of that form. The consequence of not using Form Versions on a form that changes over time is that when reviewing past data, if 1a question was removed in a future version of a form, it will appear that data collected in the past are now missing that data. There are other scenarios where a form version should be created which we will cover in later sections, but first a simple example.

    "},{"location":"editor/form-versions/#example","title":"Example","text":""},{"location":"editor/form-versions/#first-release","title":"First Release","text":"

    forms.json:

    [\n  {\n    \"id\" : \"form-x\",\n    \"title\" : \"Form X\",\n    \"src\" : \"./assets/form-x/form.html\",\n  }\n]\n

    ./assets/form-x/form.html:

    <tangy-form id=\"form-x\" title=\"Form X\">\n  <tangy-input label=\"Question A\" name=\"a\"></tangy-input>\n  <tangy-input label=\"Question B\" name=\"b\"></tangy-input>\n</tangy-form>\n

    "},{"location":"editor/form-versions/#second-release","title":"Second Release","text":"
    {\n  \"id\" : \"form-x\",\n  \"title\" : \"Form X\",\n  \"src\" : \"./assets/form-x/form.html\",\n  \"formVersionId\": \"2\",\n  \"formVersions\": [\n    {\n      \"id\": \"1\",\n      \"src\" : \"./assets/form-x/1.html\"\n    },\n    {\n      \"id\": \"2\",\n      \"src\" : \"./assets/form-x/2.html\"\n    }\n  ]\n}\n

    ./assets/form-x/form.html:

    <tangy-form id=\"form-x\" title=\"Form X\">\n  <tangy-input label=\"Question A\" name=\"a\"></tangy-input>\n</tangy-form>\n

    ./assets/form-x/1.html:

    <tangy-form id=\"form-x\" title=\"Form X\">\n  <tangy-input label=\"Question A\" name=\"a\"></tangy-input>\n  <tangy-input label=\"Question B\" name=\"b\"></tangy-input>\n</tangy-form>\n

    ./assets/form-x/2.html:

    <tangy-form id=\"form-x\" title=\"Form X\">\n  <tangy-input label=\"Question A\" name=\"a\"></tangy-input>\n</tangy-form>\n

    "},{"location":"editor/form-versions/#third-release","title":"Third Release","text":"
    {\n  \"id\" : \"form-x\",\n  \"title\" : \"Form X\",\n  \"src\" : \"./assets/form-x/form.html\",\n  \"formVersionId\": \"3\",\n  \"formVersions\": [\n    {\n      \"id\": \"1\",\n      \"src\" : \"./assets/form-x/1.html\"\n    },\n    {\n      \"id\": \"2\",\n      \"src\" : \"./assets/form-x/2.html\"\n    },\n    {\n      \"id\": \"3\",\n      \"src\" : \"./assets/form-x/3.html\"\n    }\n  ]\n}\n

    ./assets/form-x/form.html:

    <tangy-form id=\"form-x\" title=\"Form X\">\n  <tangy-input label=\"Question A\" name=\"a\"></tangy-input>\n  <tangy-input label=\"Question C\" name=\"c\"></tangy-input>\n</tangy-form>\n

    ./assets/form-x/1.html:

    <tangy-form id=\"form-x\" title=\"Form X\">\n  <tangy-input label=\"Question A\" name=\"a\"></tangy-input>\n  <tangy-input label=\"Question B\" name=\"b\"></tangy-input>\n</tangy-form>\n

    ./assets/form-x/2.html:

    <tangy-form id=\"form-x\" title=\"Form X\">\n  <tangy-input label=\"Question A\" name=\"a\"></tangy-input>\n</tangy-form>\n

    ./assets/form-x/3.html:

    <tangy-form id=\"form-x\" title=\"Form X\">\n  <tangy-input label=\"Question A\" name=\"a\"></tangy-input>\n  <tangy-input label=\"Question C\" name=\"c\"></tangy-input>\n</tangy-form>\n

    "},{"location":"editor/form-versions/#when-should-i-create-a-new-form-version","title":"When should I create a new Form Version?","text":"

    Situations when a new Form Version should be created include:

    1. New question
    2. Removed question
    3. Options for a question added or removed.
    4. New page
    5. Removed page
    6. Reordered pages

    Situations when a new Form Version can be skipped: 1. Label of a question has changed. 2. Variable marked as required.

    "},{"location":"editor/form-versions/#future-tooling-proposals","title":"Future Tooling proposals","text":"

    Proposals for tools to help in managing form versions:

    • Linter idea - cli that checks if 2 form versions share the same path - makes sure there are no duplicate revision src paths. Check for dupes in formVersion.id and formVersion.src.
    • start new dir in tangerine dir called cli - this would be the first subcommand of a new cli. This is different from the server cli. tangerine-preview is another command that could be integrated into this new cli. Example - generate-new-form creates the scaffolding for a new form and could implement/facilitate the revisions feature.
    • Version incrementor - used for releases

    Proposed Testing version support:

    What are the different use-cases that the software must implement to fully support versions? The following list will list each case and the correct source for the form:

    • Using the tangerine-preview app and must use the most recent version: formInfo.src
    • Viewing a record created in a legacy group with no formVersionId and no formVersions: formInfo.src
    • viewing a record created in a legacy group with no formVersionId but does have formVersions using the legacyOriginal flag: legacyVersion.src
    • viewing a record created in a legacy group with no formVersionId but does have formVersions without legacyOriginal flag: lawd have mercy! formInfo.src
    • viewing a record created in a new group using formVersionId and has formVersions: formVersion.src
    • viewing a record created in a new group using formVersionId and does not have formVersions : formInfo.src
    "},{"location":"editor/password-policy/","title":"Password Policy","text":"

    The default password policy is in config.defaults.sh. Although you can change it, it is recommended that you have a strong password policy.

    Relevant variables: - T_PASSWORD_POLICY - The policy, coded in the form of a regular expression. - T_PASSWORD_RECIPE - Description of the policy, to enable user to create a password that will pass the policy.

    Ideally a password policy should include the following specifications: - 8 characters or more - at least one upper case letters - at least one lower case letter - at least one special character - at least one numeral

    Each group can have a unique password policy. When a group is created, the default policy and recipe from config.sh are copied over to the passwordPolicy and passwordRecipe variables in app-config.json.

    For some groups, it may be more useful to have a simpler password policy on client than on editor. Here is an example:

    • \"passwordPolicy\": \"(?=.\\d)(?=.[a-z])(?=.*[A-Z]).{8,}\",
    • \"passwordRecipe\": \"Must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters\",

    Editor on the server uses the T_PASSWORD_POLICY and T_PASSWORD_RECIPE variables.

    "},{"location":"editor/password-policy/#tips","title":"Tips","text":"

    If the server's shell has problems interpreting any of the special characters when loading T_PASSWORD_POLICY from config.sh, you may need to add an escape \\ before the special character.

    The site https://www.regextester.com/ has been very helpful in testing out password policies.

    "},{"location":"editor/reserved-words/","title":"Reserved words in Tangerine","text":"

    The following words should not be used as field names in Tangerine because they will cause clashes with mysql export and other features: - buildChannel - buildId - caseDefinitionId - caseId - caseRoleId - caseEventId - collection - complete - dbRevision - deviceId - estimate - eventformid - formID_sanitized - groupId - inactive - participantId - required - startDate - startUnixtime - type - uploadDatetime

    "},{"location":"editor/translations/","title":"Translations","text":"

    There are two types of translations in Tangerine, Application Translations and Content Translations. Applications Translations are translations on Tangerine User Interface such as the \"next\" button on a form, or the \"Sync\" menu item in the top level tablet menu. Content Translations are the translations on forms such as the \"label\" and \"hint text\" of a question. The method of providing translations are different for the two.

    "},{"location":"editor/translations/#content-translations","title":"Content Translations","text":"

    Translations for specific languages are embedded in content, thus portable and specific to that content. The <t-lang> component (https://github.com/ICTatRTI/translation-web-component) is used to detect the language assigned to the HTML doc. In the following example, the label on the hello input will be \"Hello\" if English is set as the language, \"Bonjour\" if French is selected as the language.

        <tangy-input \n        name=\"hello\"\n        label=\"\n            <t-lang en>Hello</t-lang>\n            <t-lang fr>Bonjour</t-lang>\n        \"\n    >\n    </tangy-input>\n
    "},{"location":"editor/translations/#application-translations","title":"Application Translations","text":"

    By default, when you create a new Group in Tangerine, a set of default Application Translations are provided. Currently that includes English, French, Jordanian, Khmer, and Russian. When deploying, these languages are selectable on a per tablet basis under the Tangerine Settings menu.

    If you would like to add or modify translations for your group, currently we would recommend setting up your group with a Github Integration to allow editing of the content of your group's content. In your group's content folder you will find two types of files, the list of translations in translations.json, and then a file per translation such as translation.fr.json for French, translations.ru.json for Russian, etc. By adding to, or removing, or modifying entries in translations.json, this will modify what translations are available for a tablet user to select in settings.

    See the default translations.json file here and find the other default translation files here.

    "},{"location":"editor/advanced-form-programming/","title":"Overview","text":"

    Tangerine contains an extremely extensible foundational framework that allows the form developer to highly customize the core functionality, actions, and progression. The following section provides guidance and common examples for extending Tangerine.

    TODO: INSERT TABLE OF CONTENTS FOR THIS SECTION AND HIGH-LEVEL OVERVIEW

    "},{"location":"editor/advanced-form-programming/globals/","title":"Global Variables","text":"

    Tangerine-specific variables are available in the T global variable. These are exposed in app.component.ts. Example:

    this.window.T = {\n      form: {\n        Get: Get\n      },\n      router,\n      http,\n      user: userService,\n      lockBox: lockBoxService,\n      syncing: syncingService,\n      syncCouchdbService: syncCouchdbService, \n      sync: syncService,\n      appConfig: appConfigService,\n      update: updateService,\n      search: searchService,\n      device: deviceService,\n      tangyFormsInfo: tangyFormsInfoService,\n      tangyForms: tangyFormService,\n      formTypes: formTypesService,\n      case: caseService,\n      cases: casesService,\n      caseDefinition: caseDefinitionsService,\n      languages: languagesService,\n      variable: variableService,\n      classForm: classFormService,\n      classDashboard: dashboardService,\n      translate: window['t']\n    }\n

    Additional T properties may be added in other parts of the Tangerine codebase.

    "},{"location":"editor/advanced-form-programming/globals/#usage","title":"Usage","text":"

    Examples of T global usage are throughout these docs, but here are a few:

    To load and query the client database with options to get a specific revision:

    const db = await T.user.getUserDatabase()\ndb.get('foo',{rev:'4-uuid', latest:false})\n

    When writing queries or organizing the javascript logic to fetch the results, use the globally-exposed T.form.Get function to get the value of inputs; this will save you from having to wrote deeply nested code (doc.items[0].inputs[3].value[0].value)

    T.form.Get(doc, 'consent')

    // 3 ingredients are needed to set an Event Variable.\nconst eventId = '123'\nconst variableName = 'foo'\nconst variableValue = 'bar'\n\n// Set Event Variable.\nT.case.setVariable(`${eventId}-${variableName}`, variableValue)\n\n// Get Event Variable.\nconst shouldBeValueOfBar = T.case.getVariable(`${eventId}-${variableName}`)\n

    There is an older document, Tangerine globals, that describes some of the functions that are now attached to the T global.

    "},{"location":"editor/advanced-form-programming/kiosk-or-fullscreen-modes/","title":"Kiosk or Fullscreen modes","text":""},{"location":"editor/advanced-form-programming/kiosk-or-fullscreen-modes/#kiosk","title":"Kiosk","text":"

    Kiosk mode is enabled app-wide by adding \"kioskMode\": true to the group's app-config.json file. This enables the 'Kiosk Mode' item in the menu. Clicking this item sets kioskModeEnabled to true and removes the top toolbar.

    The app-config.json property - exitClicks - enables admin to set number of clicks to exit kioskMode. Default is 5. User must click the top of the screen 5 times within 2 seconds.

    "},{"location":"editor/advanced-form-programming/kiosk-or-fullscreen-modes/#fullscreen-mode","title":"Fullscreen mode","text":"

    Fullscreen mode is activated at the form level by setting \"fullscreen\": true in tangy-form editor. The current code employs a workaround to deal with a bug in APK's that prevents exit fullscreen from working by using a listener for 'enter-fullscreen' or 'exit-fullscreen' to set this.kioskModeEnabled = true or false, which removes the top bar.

    "},{"location":"editor/advanced-form-programming/local-content-development/","title":"Local content development","text":""},{"location":"editor/advanced-form-programming/local-content-development/#local-content-development-with-tangerine-preview","title":"Local content development with Tangerine Preview","text":"

    Tangerine Preview is a command line tool for previewing the Tangerine content you are working on your local computer. It work on Windows, Mac, and Linux.

    "},{"location":"editor/advanced-form-programming/local-content-development/#install","title":"Install","text":"

    Before you install tangerine-preview, make sure to install node.js.

    If you are on macOS, you will need to set permissions to allow for global installs by running the following command.

    sudo chown -R `whoami` /usr/local/lib/node_modules\n

    For all platforms, open a command line terminal and run Tangerine Preview install command.

    npm install -g tangerine-preview\n

    "},{"location":"editor/advanced-form-programming/local-content-development/#preview-your-content","title":"Preview your content","text":"

    Open a command prompt, change directory to your content that you would like to preview, then run the tangerine-preview command.

    cd your-project\ntangerine-preview\n

    Lastly, open Google Chrome to http://localhost:3000

    As you make content changes, they will be synced to the app. Reload your web browser and you'll see the changes.

    "},{"location":"editor/advanced-form-programming/local-content-development/#update-tangerine-preview","title":"Update tangerine-preview","text":"

    When new releases come out for tangerine, tangerine-preview will also be updated. To update, open a command prompt and run the install command again.

    If you have installed tangerine-preview in the past, you'll need to uninstall it first.

    npm install -g tangerine-preview\n

    The following will install tangerine-preview at the most recent version.

    npm install -g tangerine-preview\n

    If you need a specific version of tangerine-preview, you can specify the version when installing. For example...

    npm install -g tangerine-preview@3.18.5\n
    "},{"location":"editor/advanced-form-programming/local-content-development/#check-your-currently-install-version","title":"Check your currently install version","text":"
    npm list -g tangerine-preview\n
    "},{"location":"editor/advanced-form-programming/local-content-development/#set-up-vs-code-with-syntax-highlighting-for-on-open-on-change-etc","title":"Set up VS Code with Syntax Highlighting for on-open, on-change, etc.","text":"

    Ideally we would have a VS Code plugin for you to install. Until then, this is our workaround. If you are interested in helping with the development of a VS Code plugin, feel free to reach out to us via the issue queue.

    "},{"location":"editor/advanced-form-programming/local-content-development/#step-1","title":"Step 1","text":"

    Open Visual Studio configuration file. If you have the VS Code CLI installed, run the following in a terminal.

    code /Applications/Visual\\ Studio\\ Code.app/Contents/Resources/app/extensions/html/syntaxes/html.tmLanguage.json\n

    "},{"location":"editor/advanced-form-programming/local-content-development/#step-2","title":"Step 2","text":"

    Find on(s(c and replace with on-open|on-change|on-submit|skip-if|hide-if|dont-skip-if|disable-if|valid-if|discrepancy-if|warn-if|on(s(c.

    "},{"location":"editor/advanced-form-programming/local-content-development/#step-3","title":"Step 3","text":"

    Restart Visual Studio.

    "},{"location":"editor/case-module/","title":"Case Module","text":"

    Case Module allows us to define Case Definitions for different purposes such as following a participant in a drug trial over the course of many events, where each event may require many forms to be filled out. In order to create and find cases, you will need to configure the \"case-home\" as the \"homeUrl\" value in app-config.json.

    "},{"location":"editor/case-module/#configuring-cases","title":"Configuring Cases","text":"

    Case Module allows us to define Case Definitions for different purposes such as following a participant in a drug trial over the course of many events, where each event may require many forms to be filled out.

    To configure cases, there are four files to modify.

    First add a reference to the new Case Definition in the case-definitions.json. Here is an example of a case-definitions.json file that references two Case Definitions.

    File: case-definitions.json

    [\n  {\n    \"id\": \"case-definition-1\",\n    \"name\": \"Case Definition 1\",\n    \"src\": \"./assets/case-definition-1.json\"\n  },\n  {\n    \"id\": \"case-definition-2\",\n    \"name\": \"Case Definition 2\",\n    \"src\": \"./assets/case-definition-2.json\"\n  }\n]\n

    Then create the corresponding Case Definition file...

    File: case-definition-1.json

    {\n  \"id\": \"case-definition-1\",\n  \"formId\": \"case-definition-1-manifest\",\n  \"name\": \"Case Definition 1\",\n  \"description\": \"Description...\",\n  \"startFormOnOpen\": {\n    \"eventId\": \"event-definition-1\",\n    \"eventFormId\": \"event-form-1\"\n  },\n  \"eventDefinitions\": [\n   {\n      \"id\": \"event-definition-1\",\n      \"name\": \"Event Definition 1\",\n      \"description\": \"Description...\",\n      \"repeatable\": false,\n      \"required\": true,\n      \"eventFormDefinitions\": [\n        {\n          \"id\": \"event-form-definition-1\",\n          \"formId\": \"form-1\",\n          \"name\": \"Form 1\",\n          \"required\": true,\n          \"repeatable\": false\n        }\n      ]\n    }\n  ]\n}\n

    "},{"location":"editor/case-module/#case-definition-templates","title":"Case Definition Templates","text":"

    As a Data Collector uses the Client App, they navigate a Case's hierarchy of Events and Forms. Almost every piece of information they see can be overriden to display custom variables and logic by using the Case Definition's templates. This section describes the templates available and what variables are available. Note that all templates are evaluated as Javascript Template Literals. There are many good tutorials online about how to use Javascipt Template Literals, here are a couple of Javascript Template Literals examples that we reference often for things like doing conditionals and loops.

    "},{"location":"editor/case-module/#schedule","title":"Schedule","text":"

    templateScheduleListItemIcon default:

    \"templateScheduleListItemIcon\": \"${caseEvent.status === 'CASE_EVENT_STATUS_COMPLETED' ? 'event_note' : 'event_available'}\"\n

    templateScheduleListItemPrimary default:

    \"templateScheduleListItemPrimary\": \"<span>${caseEventDefinition.name}</span> in Case ${caseService.case._id.substr(0,5)}\"\n

    templateScheduleListItemSecondary default:

    \"templateScheduleListItemSecondary\": \"<span>${caseInstance.label}</span>\"\n

    Variables available: - caseService: CaseService - caseDefinition: CaseDefinition - caseEventDefinition: CaseEventDefinition - caseInstance: Case - caseEvent: CaseEvent

    "},{"location":"editor/case-module/#debugging-case-definition-templates","title":"Debugging Case Definition Templates","text":""},{"location":"editor/case-module/#configuring-formsjson","title":"Configuring forms.json","text":"

    The case references a Form in the formId property of the Case Definition. Make sure there is a form with that corresponding Form ID listed in forms.json with additional configuration for search.

    File: forms.json

    [\n  {\n    \"id\" : \"case-definition-1-manifest\",\n    \"type\" : \"case\",\n    \"title\" : \"Case Definition 1 Manifest\",\n    \"description\" : \"Description...\",\n    \"listed\" : true,\n    \"src\" : \"./assets/case-definition-1-manifest/form.html\",\n    \"searchSettings\" : {\n      \"primaryTemplate\" : \"${searchDoc.variables.status === 'Enrolled' ? `Participant ID: ${searchDoc.variables.participant_id} &nbsp; &nbsp; &nbsp; Enrollment Date: ${(searchDoc.variables.enrollment_date).substring(8,10) + '-' + (searchDoc.variables.enrollment_date).substring(5,7)+ '-' + (searchDoc.variables.enrollment_date).substring(0,4)}` : `Screening ID: ${searchDoc._id.substr(0,6)}  &nbsp; &nbsp; &nbsp; Screening Date: ${searchDoc.variables.screening_date ? searchDoc.variables.screening_date : 'N/A' }` }\",\n      \"shouldIndex\" : true,\n      \"secondaryTemplate\" : \"${searchDoc.variables.status === 'Enrolled' ? `Name: ${searchDoc.variables.first_name} ${searchDoc.variables.last_name}  &nbsp; &nbsp; &nbsp; Location: ${searchDoc.variables.location}  &nbsp; &nbsp; &nbsp; Status: Enrolled &nbsp; &nbsp; &nbsp;` : `Status: Not enrolled  &nbsp; &nbsp; &nbsp;` }\",\n      \"variablesToIndex\" : [\n        \"first_name\",\n        \"last_name\",\n        \"status\",\n        \"location\",\n        \"participant_id\",\n        \"enrollment_date\",\n        \"screening_date\"\n      ]\n    },\n  }\n]\n

    The properties in forms.json often change; check the Content Sets for current examples.

    "},{"location":"editor/case-module/#configuring-text-search","title":"Configuring Text Search","text":"

    The variables listed in variablesToIndex will be available for searching on with the Tablet level search as well as in Editor. You also must set \"shouldIndex\" : true.

    All forms that should not be searched must also have the searchSettings configured. Here is an example that must be implemented:

        \"searchSettings\" : {\n      \"shouldIndex\" : false,\n      \"primaryTemplate\" : \"\",\n      \"secondaryTemplate\" : \"\",\n      \"variablesToIndex\" : [\n      ]\n    },\n
    "},{"location":"editor/case-module/#configuring-qr-code-search","title":"Configuring QR code search","text":"

    To configure the QR code search on Tablets, in your app-config.json, there is a \"barcodeSearchMapFunction\" property. This is a map function for receiving the value of the Search Scan for you to parse out and return the value that should be used for search. A data variable is passed in for you to parse, and the return value is what ends up in the search bar as a text search.

    Example:

    if ((JSON.parse(data)).participant_id) { \n  return (JSON.parse(data)).participant_id\n} else { \n  throw 'Incorrect format'\n} \n

    "},{"location":"editor/case-module/#configuring-two-way-sync","title":"Configuring two-way sync","text":"

    Because you may need to share cases across devices, configuring two-way sync may be necessary. See the Two-way Sync Documentation for more details. Note that you sync Form Responses, and it's the IDs of that you'll want to sync in the \"formId\" of the Case Definition in order to sync cases.

    "},{"location":"editor/case-module/#configuring-the-schedule","title":"Configuring the Schedule","text":"

    One of the two tabs that Data Collectors see when they log into Tangerine is a \"Schedule\" tab. This schedule will show Case Event's on days where they are have an estimated day, scheduled day, and/or occurred on day. You can set these three dates on an event using the following APIs.

    caseService.setEventEstimatedDay(idOfEvent, timeInUnixMilliseconds)\ncaseService.setEventOccurredOn(idOfEvent, timeInUnixMilliseconds)\ncaseService.setEventScheduledDay(idOfEvent, timeInUnixMilliseconds)\n
    "},{"location":"editor/case-module/case-service-api/","title":"API: caseService","text":"

    Warning

    Use of the caseService Class is only available when the case module is enabled on the server.

    TODO: Link to the page that describes how to enable the case service

    The caseService class allows the form developer to programmatically interact with the case from within the form. The Case Service is activated in Tangerine when the user opens a view associated with a Case. Given a case's unique identifier, the Case Service loads into memory both the case document and the case definition document associated with the case. The Case Service also sets some reference variables for easy access to the most used case items like the participants, events and event forms.

    Unless otherwise noted, the Case Service APIs that operate on the version of the case in memory. When programming within forms, any changes to the case document are saved to couchdb when the on-submit logic completes. In more advanced programming workflows, the load() and save() APIs can be used to specify when case documents are loaded into memory or saved to couchdb.

    "},{"location":"editor/case-module/case-service-api/#case-apis","title":"Case APIs","text":""},{"location":"editor/case-module/case-service-api/#id","title":"id","text":"

    Returns the unique identifier of the currently loaded case.

    "},{"location":"editor/case-module/case-service-api/#participants","title":"participants","text":"

    Returns the list of participants associated with the case or an empty array if none exist.

    "},{"location":"editor/case-module/case-service-api/#events","title":"events","text":"

    Returns the list of events associated with the case or an empty array if none exist.

    "},{"location":"editor/case-module/case-service-api/#forms","title":"forms","text":"

    Returns the list of forms for all Case Events associated with the case or an empty array if none exist.

    "},{"location":"editor/case-module/case-service-api/#roledefinitions","title":"roleDefinitions","text":"

    Returns the list of Case Roles associated with this case from the Case Definition.

    "},{"location":"editor/case-module/case-service-api/#eventformdefinitions","title":"eventFormDefinitions","text":"

    Returns the list of Event Form Definitions associated with this case type form the Case Definition.

    "},{"location":"editor/case-module/case-service-api/#caseeventdefinitions","title":"caseEventDefinitions","text":"

    Returns the list of Case Event Definitions associated with this case type form the Case Definition.

    "},{"location":"editor/case-module/case-service-api/#changelocation","title":"changeLocation","text":"

    Change the location of a Case. This also changes the location information on all related Form Responses.

    "},{"location":"editor/case-module/case-service-api/#parameters","title":"Parameters","text":"Param Type Description location object An object where the properties are the levels and the values are the location node IDs"},{"location":"editor/case-module/case-service-api/#example","title":"Example","text":"
    • video
    • example code
    "},{"location":"editor/case-module/case-service-api/#setvariable","title":"setVariable","text":"

    Set a Case level variable in a Case.

    "},{"location":"editor/case-module/case-service-api/#parameters_1","title":"Parameters","text":"Param Type Description variableName string The variable name. value any The value for the variable."},{"location":"editor/case-module/case-service-api/#example_1","title":"Example","text":"

    caseService.setVariable('participant_id', getValue('participant_id'))\ncaseService.setVariable('first_name', getValue('first_name'))\ncaseService.setVariable('last_name', getValue('last_name'))\n
    - example code

    "},{"location":"editor/case-module/case-service-api/#getvariable","title":"getVariable","text":"

    Get a Case level variable in a Case.

    "},{"location":"editor/case-module/case-service-api/#parameters_2","title":"Parameters","text":"Param Type Description variableName string The variable name."},{"location":"editor/case-module/case-service-api/#returns","title":"Returns","text":"

    The value requested. May be any data type that was set.

    "},{"location":"editor/case-module/case-service-api/#example_2","title":"Example","text":"

    if (!caseService.getVariable('status')) {\n  caseService.setVariable('status', 'screening')\n}\n
    - example code

    "},{"location":"editor/case-module/case-service-api/#getcurrentcaseeventid","title":"getCurrentCaseEventId","text":"

    Returns the unique identifier for the Case Event currently loaded in memory. This is a useful function for safely getting the Case Event Id for use in other APIs that take caseEventId as a parameter.

    "},{"location":"editor/case-module/case-service-api/#parameters_3","title":"Parameters","text":"

    None

    "},{"location":"editor/case-module/case-service-api/#returns_1","title":"Returns","text":"

    The unique identifier for the currently loaded Case Event. Returns undefined if a Case Event is not currently in view.

    "},{"location":"editor/case-module/case-service-api/#example_3","title":"Example","text":"
    if (caseService.getCurrentCaseEventId()) {\n  caseService.setEventEstimatedDay(caseService.getCurrentCaseEventId(), moment())\n}\n
    "},{"location":"editor/case-module/case-service-api/#getcurrenteventformid","title":"getCurrentEventFormId","text":"

    Returns the unique identifier for the Case Event currently loaded in memory. This is a useful function for safely getting the Event Form Id for use in other APIs that take eventFormId as a parameter.

    "},{"location":"editor/case-module/case-service-api/#parameters_4","title":"Parameters","text":"

    None

    "},{"location":"editor/case-module/case-service-api/#returns_2","title":"Returns","text":"

    The unique identifier for the currently loaded Event Form. Returns undefined if an Event Form is not currently in view.

    "},{"location":"editor/case-module/case-service-api/#example_4","title":"Example","text":"
    if (caseService.getCurrentCaseEventId() && caseService.getCurrentEventFormId()) {\n  caseService.markEventFormRequired(caseService.getCurrentCaseEventId(), caseService.getCurrentEventFormId())\n}\n
    "},{"location":"editor/case-module/case-service-api/#case-event-api","title":"Case Event API","text":""},{"location":"editor/case-module/case-service-api/#createevent","title":"createEvent","text":"

    Dynamically create an instance of an event (defined in the case json) and add it to the current case

    "},{"location":"editor/case-module/case-service-api/#parameters_5","title":"Parameters","text":"Param Type Description eventDefinitionId string Event Definition ID from the Case Definition Document createRequiredEventForms (Optional) boolean (Default: False) Instantiate any required forms within the Event"},{"location":"editor/case-module/case-service-api/#returns_3","title":"Returns","text":"

    CaseEvent object

    "},{"location":"editor/case-module/case-service-api/#example_5","title":"Example","text":"
    const event1 = caseService.createEvent('event-definition-cf58ca')\nconst event2 = caseService.createEvent('event-definition-682ca6', true)\n
    "},{"location":"editor/case-module/case-service-api/#seteventname","title":"setEventName","text":"

    Set a custom name for the Case Event to be displayed in the Case Event list for the current case. The string value passed as the name parameter can be resolved from any javascript that returns a string.

    "},{"location":"editor/case-module/case-service-api/#parameters_6","title":"Parameters","text":"Param Type Description eventId string (UUID) Event Instance ID from the Case name string Name for the event"},{"location":"editor/case-module/case-service-api/#example_6","title":"Example","text":"
    caseService.setEventEstimatedDay(caseService.getCurrentCaseEventId(), \"First Visit\")\n
    "},{"location":"editor/case-module/case-service-api/#seteventestimatedday","title":"setEventEstimatedDay","text":"

    Set the estimated day of expected date completion. This is used by the calendar for displaying events. TODO: Improve Description

    "},{"location":"editor/case-module/case-service-api/#parameters_7","title":"Parameters","text":"Param Type Description eventId string (UUID) Event Instance ID from the Case timeInMs number Unix timestamp for the event date/time"},{"location":"editor/case-module/case-service-api/#example_7","title":"Example","text":"
    caseService.setEventEstimatedDay(event1.id, now)\ncaseService.setEventEstimatedDay(caseService.getCurrentCaseEventId(), 1592359411)\n
    "},{"location":"editor/case-module/case-service-api/#seteventscheduledday","title":"setEventScheduledDay","text":"

    Set the scheduled day of expected date completion. This is used by the calendar for displaying events. TODO: Improve Description

    "},{"location":"editor/case-module/case-service-api/#parameters_8","title":"Parameters","text":"Param Type Description eventId string (UUID) Event Instance ID from the Case timeInMs number Unix timestamp for the event date/time"},{"location":"editor/case-module/case-service-api/#example_8","title":"Example","text":"
    caseService.setEventScheduledDay(event1.id, now)\ncaseService.setEventScheduledDay(caseService.getCurrentCaseEventId(), 1592359411)\n
    "},{"location":"editor/case-module/case-service-api/#seteventwindow","title":"setEventWindow","text":"

    Set the expected event completion. TODO: Improve Description

    "},{"location":"editor/case-module/case-service-api/#parameters_9","title":"Parameters","text":"Param Type Description eventId string (UUID) Event Instance ID from the Case windowStartDayTimeInMs number Unix timestamp for the event start date/time windowEndDayTimeInMs number Unix timestamp for the event end date/time"},{"location":"editor/case-module/case-service-api/#example_9","title":"Example","text":"
    caseService.setEventWindow(event1.id, 1592359411, 1592618611)\ncaseService.setEventWindow(caseService.getCurrentCaseEventId(), 1592359411, 1592618611)\n
    "},{"location":"editor/case-module/case-service-api/#seteventoccurredon","title":"setEventOccurredOn","text":"

    Set the date in which the event occurred TODO: Improve Description

    "},{"location":"editor/case-module/case-service-api/#parameters_10","title":"Parameters","text":"Param Type Description eventId string (UUID) Event Instance ID from the Case timeInMs number Unix timestamp for the event completion date/time"},{"location":"editor/case-module/case-service-api/#example_10","title":"Example","text":"
    caseService.setEventOccurredOn(event1.id, now)\ncaseService.setEventOccurredOn(caseService.getCurrentCaseEventId(), 1592359411)\n
    "},{"location":"editor/case-module/case-service-api/#disableeventdefinition","title":"disableEventDefinition","text":"

    Prevent creation of an via the new event menu.

    "},{"location":"editor/case-module/case-service-api/#parameters_11","title":"Parameters","text":"Param Type Description eventDefinitionId string (UUID) Event Definition ID from the Case Definition"},{"location":"editor/case-module/case-service-api/#example_11","title":"Example","text":"
    caseService.disableEventDefinition('event-definition-1')\n
    "},{"location":"editor/case-module/case-service-api/#activatecaseevent","title":"activateCaseEvent","text":"

    The inactive flag on Case Event controls its appearance in the Case Event list. When a Case Event is created, the inactive flag is not set on the Case Event. This API adds the inactive flag to the Case Event and marks the flag as false. The inverse API deactivateCaseEvent sets the flag to true. Case Event with no inactive flag or having the flag set to false will appear in the Case Event list.

    "},{"location":"editor/case-module/case-service-api/#parameters_12","title":"Parameters","text":"Param Type Description eventId string (UUID) Event Instance ID from the Case"},{"location":"editor/case-module/case-service-api/#example_12","title":"Example","text":"
    // All currently hidden (inactive) case events in the case with the event-definition of 'optional-event-dev' will be shown in the Case Event list\nfor (let caseEvent in caseService.case.caseEvents.filter(event => event.caseEventDefinitionId === 'optional-event-def')) {\n  if (caseEvent.inactive) {\n    caseService.activateCaseEvent(caseEvent.id)\n  }\n}\n
    "},{"location":"editor/case-module/case-service-api/#deactivatecaseevent","title":"deactivateCaseEvent","text":"

    The inactive flag on Case Event controls its appearance in the Case Event list. When a Case Event is created, the inactive flag is not set on the Case Event. This API adds the inactive flag to the Case Event and marks the flag as true. The inverse API activateCaseEvent sets the flag to false. Case Event with the flag set to true will appear in the Case Event list.

    "},{"location":"editor/case-module/case-service-api/#parameters_13","title":"Parameters","text":"Param Type Description eventId string (UUID) Event Instance ID from the Case"},{"location":"editor/case-module/case-service-api/#example_13","title":"Example","text":"
    // All case events in the case with the event-definition of 'optional-event-dev' will be hidden in the Case Event list\nfor (let caseEvent in caseService.case.caseEvents.filter(event => event.caseEventDefinitionId === 'optional-event-def')) {\n  caseService.deactivateCaseEvent(caseEvent.id)\n}\n
    "},{"location":"editor/case-module/case-service-api/#event-form-api","title":"Event Form API","text":""},{"location":"editor/case-module/case-service-api/#createeventform","title":"createEventForm","text":"

    Dynamically create an instance of an event form (defined in the case json) and add it to the current Case.

    "},{"location":"editor/case-module/case-service-api/#parameters_14","title":"Parameters","text":"Param Type Description caseEventId string Event ID of the Event you want to add this Form to eventFormDefinitionId string Event Form Definition ID of the Event Form ou want to create participantId (optional) string ID of the Participant this Event Form is for."},{"location":"editor/case-module/case-service-api/#returns_4","title":"Returns","text":"

    EventForm object

    "},{"location":"editor/case-module/case-service-api/#example_14","title":"Example","text":"
    const eventForm = caseService.startEventForm(caseEvent.id, 'event-form-definition-fdkai3', participant.id)\n
    • code example
    • video demo
    "},{"location":"editor/case-module/case-service-api/#deleteeventform","title":"deleteEventForm","text":"

    Dynamically delete an instance of an event form.

    "},{"location":"editor/case-module/case-service-api/#parameters_15","title":"Parameters","text":"Param Type Description caseEventId string Event ID of the Event you want to add this Form to eventFormId string Event Form ID of the Event Form you want to delete"},{"location":"editor/case-module/case-service-api/#example_15","title":"Example","text":"
    // Find a Case Event and corresponding Event Form instance to delete.\nconst caseEvent = caseService\n  .case\n  .events\n  .find(event => event.eventDefinitionId === 'event-definition-1')\nconst eventForm = caseEvent\n  .eventForms\n  .find(eventForm => eventForm.eventFormDefinitionId === 'event-form-definition-1')\n// Delete the EventForm.\ncaseService.deleteEventForm(caseEvent.id, eventForm.id)\n
    "},{"location":"editor/case-module/case-service-api/#seteventformdata","title":"setEventFormData","text":"

    Set a custom piece of data for an event form. Note this is a separate data collection than the data on the Form Response related to the Event Form.

    "},{"location":"editor/case-module/case-service-api/#parameters_16","title":"Parameters","text":"Param Type Description caseEventId string Event ID of the Event the Event Form lives in eventFormId string Event Form ID of the Event Form you want to set data on variableName string Variable name you are setting value any Value you want to set"},{"location":"editor/case-module/case-service-api/#example_16","title":"Example","text":"
    caseService.setEventFormData(caseEvent.id, eventForm.id, 'foo', 'bar')\n
    "},{"location":"editor/case-module/case-service-api/#geteventformdata","title":"getEventFormData","text":"

    Get a custom piece of data for an event form. Note this is a separate data collection than the data on the Form Response related to the Event Form.

    "},{"location":"editor/case-module/case-service-api/#parameters_17","title":"Parameters","text":"Param Type Description caseEventId string Event ID of the Event the Event Form lives in eventFormId string Event Form ID of the Event Form you want to get data of variableName string Variable name you are getting"},{"location":"editor/case-module/case-service-api/#example_17","title":"Example","text":"
    const fooData = caseService.getEventFormData(caseEvent.id, eventForm.id, 'foo')\n
    "},{"location":"editor/case-module/case-service-api/#markeventformrequired","title":"markEventFormRequired","text":"

    Mark an Even Form instance as being required. You might do this on a form that was optional, but it has come to lite that it must be filled out before the event is marked as complete.

    "},{"location":"editor/case-module/case-service-api/#parameters_18","title":"Parameters","text":"Param Type Description caseEventId string Event ID of the Event the Event Form lives in eventFormId string Event Form ID of the Event Form you want to mark required"},{"location":"editor/case-module/case-service-api/#example_18","title":"Example","text":"
    // Create an EventForm in the current event. You could also find one.\nconst form2 = caseService.createEventForm(caseEvent.id, 'event-form--mark-form-as-required-example-2', participant.id)\n// Mark EventForm as required.\ncaseService.markEventFormRequired(caseEvent.id, form2.id)\n
    • example code
    • video
    "},{"location":"editor/case-module/case-service-api/#markeventformnotrequired","title":"markEventFormNotRequired","text":"

    Mark and event form as not required.

    "},{"location":"editor/case-module/case-service-api/#parameters_19","title":"Parameters","text":"Param Type Description caseEventId string Event ID of the Event the Event Form lives in eventFormId string Event Form ID of the Event Form you want to mark not required"},{"location":"editor/case-module/case-service-api/#example_19","title":"Example","text":"
    // Mark EventForm as required.\ncaseService.markEventFormRequired(caseEvent.id, eventForm.id)\n
    "},{"location":"editor/case-module/case-service-api/#markeventformcomplete","title":"markEventFormComplete","text":"

    Mark an event form as complete. In some case workflows a form may no longer need to be filled. This API marks an Event Form as complete so that the data collector does does not attempt to complete the form.

    "},{"location":"editor/case-module/case-service-api/#parameters_20","title":"Parameters","text":"Param Type Description caseEventId string Event ID of the Event the Event Form lives in eventFormId string Event Form ID of the Event Form you want to mark not required"},{"location":"editor/case-module/case-service-api/#example_20","title":"Example","text":"
    // Mark EventForm as required.\ncaseService.markEventFormRequired(caseEvent.id, eventForm.id)\n
    "},{"location":"editor/case-module/case-service-api/#activateeventform","title":"activateEventForm","text":"

    The inactive flag on Event Form controls its appearance in the Event Form list. When a Event Form is created, the inactive flag is not set on the Event Form. This API adds the inactive flag to the Event Form and marks the flag as false. The inverse API deactivateEventForm sets the flag to true. Event Form with no inactive flag or having the flag set to false will appear in the Event Form list.

    "},{"location":"editor/case-module/case-service-api/#parameters_21","title":"Parameters","text":"Param Type Description caseEventId string Event ID of the Event the Event Form lives in eventFormId string Event Form ID of the Event Form you want to mark active"},{"location":"editor/case-module/case-service-api/#example_21","title":"Example","text":"
    caseService.activateEventForm(caseEventId, eventFormId)\n
    "},{"location":"editor/case-module/case-service-api/#deactivateeventform","title":"deactivateEventForm","text":"

    The inactive flag on Event Form controls its appearance in the Event Form list. When a Event Form is created, the inactive flag is not set on the Event Form. This API adds the inactive flag to the Event Form and marks the flag as true. The inverse API activateEventForm sets the flag to false. Event Form with the flag set to true will appear in the Event Form list.

    "},{"location":"editor/case-module/case-service-api/#parameters_22","title":"Parameters","text":"Param Type Description caseEventId string Event ID of the Event the Event Form lives in eventFormId string Event Form ID of the Event Form you want to mark inactive"},{"location":"editor/case-module/case-service-api/#example_22","title":"Example","text":"
    caseService.deactivateEventForm(caseEventId, eventFormId)\n
    "},{"location":"editor/case-module/case-service-api/#participant-api","title":"Participant API","text":""},{"location":"editor/case-module/case-service-api/#createparticipant","title":"createParticipant","text":"

    Create a participant in a Case.

    "},{"location":"editor/case-module/case-service-api/#parameters_23","title":"Parameters","text":"Param Type Description caseRoleId string A Case Role ID as defined in the Case Role Definitions"},{"location":"editor/case-module/case-service-api/#returns_5","title":"Returns","text":"

    Participant object

    "},{"location":"editor/case-module/case-service-api/#example_23","title":"Example","text":"
    const participantRole1 = caseService.createParticipant('role-1')\n
    • example code
    "},{"location":"editor/case-module/case-service-api/#setparticipantdata","title":"setParticipantData","text":"

    Set some data for a specific Participant.

    "},{"location":"editor/case-module/case-service-api/#parameters_24","title":"Parameters","text":"Param Type Description participantId string ID of the participant variableName string Variable name you are setting value any Value you want to set"},{"location":"editor/case-module/case-service-api/#example_24","title":"Example","text":"

    Create a participant and set some data from the current form...

    const participantRole1 = caseService.createParticipant('role-1')\ncaseService.setParticipantData(participantRole1.id, 'participant_id', getValue('participant_id'))\ncaseService.setParticipantData(participantRole1.id, 'first_name', getValue('first_name'))\ncaseService.setParticipantData(participantRole1.id, 'last_name', getValue('last_name'))\n

    • example code

    Set data on participant whom this Event Form is assigned to using the global participant object that is active when an EventForm is open that has an assigned Participant...

    caseService.setParticipantData(participant.id, 'first_name', getValue('first_name'))\ncaseService.setParticipantData(participant.id, 'last_name', getValue('last_name'))\n

    • example code

    Find a participant by role and set some data from the current form...

    const participantRole1 = caseService.case.participants.find(participant => paritipcant.caseRoleId === 'role-1')\ncaseService.setParticipantData(participantRole1.id, 'participant_id', getValue('participant_id'))\ncaseService.setParticipantData(participantRole1.id, 'first_name', getValue('first_name'))\ncaseService.setParticipantData(participantRole1.id, 'last_name', getValue('last_name'))\n

    "},{"location":"editor/case-module/case-service-api/#getparticipantdata","title":"getParticipantData","text":"

    Get some data for a specific Participant.

    "},{"location":"editor/case-module/case-service-api/#parameters_25","title":"Parameters","text":"Param Type Description participantId string ID of the participant variableName string Variable name you are getting"},{"location":"editor/case-module/case-service-api/#returns_6","title":"Returns","text":"

    This function returns the value of the variable you requested which may consist of any data type you set it to.

    "},{"location":"editor/case-module/case-service-api/#example_25","title":"Example","text":"

    Get participant data of the form whom is assigned to the current form...

    caseService.getParticipantData(participant.id, 'first_name')\n

    • example code
    "},{"location":"editor/case-module/case-service-api/#notification-api","title":"Notification API","text":"

    Notifications appear in the Case view to provide instructions or extra information to the users who are filling out forms. The programmer uses the following APIs to create notifications in teh Case interface. Notifications can be persistant or dismisable depending on the use case.

    "},{"location":"editor/case-module/case-service-api/#createnotification","title":"createNotification","text":"

    Create a notificaiton and display it in the Case view

    "},{"location":"editor/case-module/case-service-api/#parameters_26","title":"Parameters","text":"Param Type Description label string Short text used for the title description string Longer text description link string Url link internal or external icon string Text name of a system icon color string Hexadecimal value of a color (e.g. #CCC) enforceAttention boolean If true, change focus to the notification when it is displayed persist boolean If true, notification can only be dismissed programatically"},{"location":"editor/case-module/case-service-api/#example_26","title":"Example","text":"
    caseService.createNotification('Alert: Case Needs you attention', 'The Case needs review with a supervisor.', '', 'notification_important', '#CCC', true, false)\n
    "},{"location":"editor/case-module/case-service-api/#opennotification","title":"openNotification","text":"

    Sets the status of a notificaiton to Open so it will display to the user.

    "},{"location":"editor/case-module/case-service-api/#parameters_27","title":"Parameters","text":"Param Type Description notificationId string Short text used for the title"},{"location":"editor/case-module/case-service-api/#example_27","title":"Example","text":"

    This code re-opens any Closed notifications that have a label that starts with 'Alert'.

    if (case.notifications) {\n  const notifications = case.notifications.filter(n => n.label.startsWith('Alert') && n.status === NotificationStatus.Closed)\n  for (let notification in notifications) {\n    caseService.openNotificaiton(notification.id)\n  }\n}\n

    "},{"location":"editor/case-module/case-service-api/#closenotification","title":"closeNotification","text":"

    Sets the status of a notificaiton to Closed so it will be hidden from the user.

    "},{"location":"editor/case-module/case-service-api/#parameters_28","title":"Parameters","text":"Param Type Description notificationId string Short text used for the title"},{"location":"editor/case-module/case-service-api/#example_28","title":"Example","text":"

    This code closes notifications that have a label that starts with 'Alert'.

    if (case.notifications) {\n  const notifications = case.notifications.filter(n => n.label.startsWith('Alert') && n.status === NotificationStatus.Open)\n  for (let notification in notifications) {\n    caseService.closeNotificaiton(notification.id)\n  }\n}\n

    "},{"location":"editor/case-module/custom-case-reports/","title":"Custom Case Reports","text":"

    The Custom Case Reports features enables the developer to create custom reports or a dashboard. This feature is accessible using a tab when viewing a case. To enable this feature, add showCaseReports: true to the group's app-config.json.

    "},{"location":"editor/case-module/custom-case-reports/#demo-and-description-of-this-features-assets","title":"Demo and description of this feature's assets","text":"

    This demo is based on the case module content set; therefore, you might want to install that in order to follow the demo.

    There are three files used for custom reports, which must be placed in the group directory.

    1. queries.js - has the map/reduce queries used to index the docs. Note that the sample queries.js file has a function called registrationResults with a map and a reduce query. The map function is explained above; the reduce uses Couchdb's built-in _sum keyword.

    The queries in this doc are installed when the app is installed. Support for updating these queries is not yet implemented.

    1. reports.js - runs the queries and fills out the content for reports.html. The reduce function is used to provide the counts in report2. The options in report2 options = {reduce: true} activate the reduce function.

    2. reports.html - provides a basic html frame for the data.

    "},{"location":"editor/case-module/custom-case-reports/#helper-functions","title":"Helper functions","text":"

    When writing queries or organizing the javascript logic to fetch the results, use the globally-exposed T.form.Get function to get the value of inputs; this will save you from having to wrote deeply nested code (doc.items[0].inputs[3].value[0].value)

    T.form.Get(doc, 'consent')

    Other helper function are available:

    • T.user: userService
    • T.case: caseService
    "},{"location":"editor/case-module/custom-case-reports/#development-in-tangerine-preview","title":"Development in Tangerine Preview","text":"

    Developers can use Tangerine-Preview as a test environment when developing custom case reports. When updating the version of a query in the queries.js file, run the following commands in the Chrome Dev Console to make Tangerine-Preview run the documents through the new version of the query:

    var userdb = await T.user.getUserDatabase()\nawait T.update.updateCustomViews(userDb)\n
    "},{"location":"editor/case-module/developing-custom-reports/","title":"Develop custom reports for Client","text":"
    • LitElement Documentation
    • LitElement Examples
    • Web Components
    "},{"location":"editor/case-module/get-and-set-event-data/","title":"Get and set event data","text":"

    For lack of a T.case.setEventData()/T.case.getEventData() API, use T.case.setVariable and T.case.getVariable with variable names that are namespaced using the Event ID.

    // 3 ingredients are needed to set an Event Variable.\nconst eventId = '123'\nconst variableName = 'foo'\nconst variableValue = 'bar'\n\n// Set Event Variable.\nT.case.setVariable(`${eventId}-${variableName}`, variableValue)\n\n// Get Event Variable.\nconst shouldBeValueOfBar = T.case.getVariable(`${eventId}-${variableName}`)\n

    There is an example in the case-module Content Set which consists of three parts:

    • Event Definition with Event Form Definition referencing a Form that sets Event Data: https://github.com/Tangerine-Community/Tangerine/blob/master/content-sets/case-module/case-type-1.json#L159
    • Form that sets Event Data: https://github.com/Tangerine-Community/Tangerine/blob/master/content-sets/case-module/template-event-listing/form.html#L5
    • Template using Event Data: https://github.com/Tangerine-Community/Tangerine/blob/master/content-sets/case-module/case-type-1.json#L21
    "},{"location":"editor/case-module/how-to-create-a-workflow-for-changing-case-location/","title":"How to create a workflow for changing a Case's location","text":"

    Using a combination of a <tangy-location> input on a form and some logic in the same form's on-submit hook, you can empower users to reassign a Case to a new location. In the Case Module's Content Set we have a \"Change Location of Case\" Case Event Definition, a \"Change Location of Case\" Event Form, and corresponding \"Change Location of Case\" Form. We'll use that Content Set as our example.

    The form could be in any event, or even it's own event as it is in the Case Module content set. The important part is in the form you place the <tangy-location> for selecting a new locatoin to assign the case to. The following shows the markup of a form that uses the caseService.changeLocation() API to change the location of a Case given the value selected in the <tangy-location> input.

    <tangy-form title=\"Change location of case\" id=\"change-location-of-case\"\n  on-submit=\"\n    caseService.changeLocation(inputs.new_location_assignment.value)\n  \"\n>\n  <tangy-form-item id=\"item1\">\n    <tangy-location label=\"Choose a location to assign this case to.\" name=\"new_location_assignment\" required></tangy-location>\n  </tangy-form-item>\n</tangy-form>\n
    "},{"location":"editor/case-module/how-to-pass-data-between-forms-in-a-case/","title":"How to pass data between Forms in a Case","text":"

    Let's say you had two forms in a Case, Form X and Form Y. In Form X, you collect the respondent's first name and last name, meanwhile in Form Y you want to confirm the data they entered on Form X.

    "},{"location":"editor/case-module/how-to-pass-data-between-forms-in-a-case/#form-x","title":"Form X","text":"

    In Form X, we bubble up the first_name and last_name variables on Form X to the first_name and last_name variables at the Case level.

    <tangy-form\n  id=\"form-x\"\n  title=\"Form X\"\n  on-submit=\"\n    T.case.setVariable('first_name', getValue('first_name'))\n    T.case.setVariable('last_name', getValue('last_name'))\n  \"\n>\n  <tangy-form-item id=\"item1\">\n    <tangy-input name=\"first_name\" label=\"First name\" required>\n    <tangy-input name=\"last_name\" label=\"Last name\" required>\n  </tangy-form-item>\n</tangy-form>\n
    "},{"location":"editor/case-module/how-to-pass-data-between-forms-in-a-case/#form-y","title":"Form Y","text":"

    In Form Y, we get the Case level variables of first_name and last_name and set them on the first_name and last_name inputs. This results in the form loading with the previously entered first and last names already filled out.

    <tangy-form id=\"form-y\" title=\"Form Y\">\n  <tangy-form-item\n    id=\"item1\"\n    on-open=\"\n      inputs.first_name.value = T.case.getVariable('first_name')\n      inputs.last_name.value = T.case.getVariable('last_name')\n    \"\n  >\n    <tangy-input name=\"first_name\" label=\"Confirm your first name\" required>\n    <tangy-input name=\"last_name\" label=\"Confirm your last name\" required>\n  </tangy-form-item>\n</tangy-form>\n
    "},{"location":"editor/case-module/how-to-use-form-response-data-in-an-event-form-listing/","title":"How to use Form Response data in an Event Form Listing","text":"

    The following tutorial uses content from the case-module Content Set found here.

    "},{"location":"editor/case-module/how-to-use-form-response-data-in-an-event-form-listing/#step-1-on-submit-of-a-form-capture-data-as-a-case-level-variable","title":"Step 1: On submit of a Form, capture data as a Case Level variable","text":"

    In Step 2, we'll template out data for the Event Form listing, but before we can do that we need to transfer some data from a form up to the Event Form data in the Case using the T.case.setEventFormData API.

    File: ./template-event-form-listing/form.html

    <tangy-form\n  id=\"template-event-form-listing\"\n  title=\"Template Event Form Listing\"\n  on-submit=\"\n    T.case.setEventFormData(caseEvent.id, eventForm.id, 'title', getValue('title'))\n  \"\n>\n  <tangy-form-item id=\"item-1\">\n    <tangy-input type=\"text\" name=\"title\" label=\"Set the custom title for this Event Form.\"></tangy-input>\n  </tangy-form-item>\n</tangy-form>\n

    "},{"location":"editor/case-module/how-to-use-form-response-data-in-an-event-form-listing/#step-2-use-templatecaseeventlistitemprimary-property-in-the-case-definition-to-print-the-case-variable-in-the-event-listing","title":"Step 2: Use templateCaseEventListItemPrimary property in the Case Definition to print the Case variable in the Event Listing","text":"

    After a user has submitted the Event Form mentioned above, we can now use the Event Form data for templating out Event listings. In the Case Definition, we add ternary to check if the variable exists, if it does then print it out in the listing, else show the name of the Event Form Definition.

    Section of File: ./case-definition-1.json

      \"templateEventFormListItemPrimary\": \"<span>${eventForm?.data?.title ? eventForm.data.title : eventFormDefinition.name}</span>\",\n

    "},{"location":"editor/case-module/how-to-use-form-response-data-in-an-event-listing/","title":"How to use Form Response data in an Event Listing","text":"

    The following tutorial uses content from the case-module Content Set found here.

    "},{"location":"editor/case-module/how-to-use-form-response-data-in-an-event-listing/#step-1-on-submit-of-a-form-capture-data-as-a-case-level-variable","title":"Step 1: On submit of a Form, capture data as a Case Level variable","text":"

    In Step 2, we'll template out data for the event listing, but before we can do that we need to transfer some data from a form up to the Case level variable. In the example below, we bubble up the title variable in the form to a title variable on the Case. However, note how we prepend the current Event ID on the case level title variable. This is important to do if you are going to bubble up variables that may have the same name across events. By prepending the current Event ID, we guarantee that any other Event that bubbles up a title variable will not overwrite the title variable for this Event.

    File: ./template-event-listing/form.html

    <tangy-form\n  id=\"template-event-listing\"\n  title=\"Template Event Listing\"\n  on-submit=\"\n    T.case.setVariable(`${caseEvent.id}-title`, getValue('title'))\n  \"\n>\n  <tangy-form-item id=\"item-1\">\n    <tangy-input type=\"text\" name=\"title\" label=\"Set the custom title for this event.\"></tangy-input>\n  </tangy-form-item>\n</tangy-form>\n

    "},{"location":"editor/case-module/how-to-use-form-response-data-in-an-event-listing/#step-2-use-templatecaseeventlistitemprimary-property-in-the-case-definition-to-print-the-case-variable-in-the-event-listing","title":"Step 2: Use templateCaseEventListItemPrimary property in the Case Definition to print the Case variable in the Event Listing","text":"

    After a user has submitted the Event Form mentioned above, we can now get that Case level variable when templating out Event listings. In the Case Definition, we add ternary to check if the variable exists, if it does then print it out in the listing, else show the name of the Event Definition. Note how we are again referencing the variable name by preprending the Event ID on the variable name. This ensures we are getting the title variable for that specific event and not accidentally overriding other/all event listings.

    Section of File: ./case-definition-1.json

      \"templateCaseEventListItemPrimary\": \"<span>${T.case.getVariable(`${caseEvent.id}-title`) ? T.case.getVariable(`${caseEvent.id}-title`) : caseEventDefinition.name}</span>\",\n

    "},{"location":"editor/case-module/on-device-data-corrections-using-issues/","title":"On Device Data Corrections using Issues","text":""},{"location":"editor/case-module/on-device-data-corrections-using-issues/#setup","title":"Setup","text":""},{"location":"editor/case-module/on-device-data-corrections-using-issues/#app-configuration","title":"App Configuration","text":"

    In your group's app-config.json (and app-config.defaults.json) add the following JSON settings. The first will make the list of Issues appear as a tab on the homescreen, the second will make a \"New Issue\" button appear when viewing submitted Event Forms.

    {\n  \"showIssues\": true,\n  \"allowCreationOfIssues\": true\n}\n
    "},{"location":"editor/case-module/on-device-data-corrections-using-issues/#template-the-issue-title-and-description","title":"Template the Issue Title and Description","text":"

    When creating an Issue on a Device, users are not allowed the opportunity to add define a Title and Description for the Issue they are creating. For this reason, you will find it helpful to develop templates for the Issue Title and Descriptions to give them context. These templates are built on a per Case Definition basis and are top level properties in any Case Definition.

    {\n  \"templateIssueTitle\": \"Issue for ${caseDefinition.name} with Participant ID of ${T.case.getVariable('participant_id')}, by ${userName}\",\n  \"templateIssueDescription\": \"\"\n}\n
    "},{"location":"editor/case-module/on-device-data-corrections-using-issues/#usage","title":"Usage","text":"

    When configured, Data Collectors will have the opportunity to create Issues where they propose changes to forms. After clicking \"New Issue\", they will fill out the form with any changes they see fit. When submitting the form will save the proposed changes in the Issue. The Issue will then be synced up to the server for a Data Manager to review. If the Data Manager approves, they may merge the proposed changes from the server. When proposed changes are merged, the status of the Issue will change to closed and that status change of the Issue will replicate down to the Device on that Device's next sync. Note however the merged changes to the form will not be replicated down to the Device unless two-way sync is set up for that form.

    "},{"location":"editor/case-module/prepare-form-logic-for-issues/","title":"Preparing form logic for compatibility with Issues","text":"

    Issues is a server level feature in Tangerine that allows privileged users to make proposals on changes to Form Responses. When a user is proposing changes, the system unlocks an already completed form. Form Developers must be careful when writing logic they only expect to be ran once in a form.

    For example, setting a \"caseOpenedOn\" variable in the on-open of a form. When a form is reopened in an Issue, the logic could potentially overwrite the \"caseOpenedOn\" variable using the current date of the proposed change.

    <tangy-form\n    on-open=\"\n        T.case.setVariable('caseOpenedOn', moment.now())\n    \"\n>\n

    This can be remedied by using the T.case.isIssueContext() helper function to ensure our variable setting does not happen in a reopen in the issue context.

    <tangy-form\n    on-open=\"\n        if (!T.case.isIssueContext()) {\n            T.case.setVariable('caseOpenedOn', moment.now())\n        }\n    \"\n>\n

    The same is true if you have any <tangy-form-item> level on-open or on-change logic that should not run in when being modified in an Issue. However, writing <tangy-form> logic for on-submit is a little different because on-submit code will not run when in the Issue context. Instead, if some code does need to run, place it in the on-resubmit code for a <tangy-form>.

    Note in the following example how we only set caseOpenedOn in on-submit. This ensure this variable will only be set once. However, note how put the setting of firstName and lastName in the on-resubmit. This ensures that if the proposed change to this Form Response changes the firstName or lastName values, the Case will be updated to reflect this proposed change.

    <tangy-form\n    on-submit=\"\n        T.case.setVariable('caseOpenedOn', moment.now())\n        T.case.setVariable('firstName', getValue('firstName'))\n        T.case.setVariable('lastName', getValue('firstName'))\n    \"\n    on-resubmit=\"\n        T.case.setVariable('firstName', getValue('firstName'))\n        T.case.setVariable('lastName', getValue('firstName'))\n    \"\n>\n

    There are also many input types such as GPS and Signature where it often does not make sense to recollect data upon submitting a proposal on an Issue. Consider adding disable-if=\"T.case.isIssueContext()\" to these inputs.

    <tangy-gps name=\"gps\" disable-if=\"T.case.isIssueContext()\"></tangy-gps>\n<tangy-signature name=\"signature\" disable-if=\"T.case.isIssueContext()\"></tangy-signature>\n
    "},{"location":"editor/case-module/role-base-access/","title":"Role Based Access","text":"

    A Device Users' Role can be used restrict access to specific Case Events, Event Forms, and inputs on Forms.

    • Getting started with using Device User Roles
    • Demo: Device User role based access to Event Forms
    • Demo: Device user role based permissions for Case Events

    To retrict access to an input on a form by role, use the T.user.getRoles() function to get the roles of the currently logged in user.

    <tangy-input\n  name=\"example\"\n  label=\"Example\"\n  show-if=\"T.user.getRoles().includes('admin')\"\n>\n</tangy-input>\n
    "},{"location":"editor/form-developers-cookbook/","title":"The Tangerine Form Developers' Cookbook","text":"

    Examples of various recipes for Tangerine Forms collected throughout the years. To create your own example, remix the example on glitch.com.

    "},{"location":"editor/form-developers-cookbook/#skip-a-question-based-on-input-in-another-question","title":"Skip a question based on input in another question","text":"

    In the following example we ask an additional question about tangerines if the user indicates that they do like tangerines.

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#skip-sections-based-on-input","title":"Skip sections based on input","text":"

    In the following example, wether or not you answer yes or no to the question, you will end up on a different item.

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#valid-by-number-of-decimal-points","title":"Valid by number of decimal points","text":"

    In the following example, we validate user input by number of decimal points.

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#valid-if-greater-or-less-than-other-input","title":"Valid if greater or less than other input","text":"

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#allowed-date-range-based-on-today","title":"Allowed date range based on today","text":"

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#flag-choice-as-discrepancy-andor-warning-and-show-or-hide-content-depending","title":"Flag choice as discrepancy and/or warning and show or hide content depending","text":"

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#indicate-a-mutually-exclusive-option-in-a-checkboxes-group-such-as-none-of-the-above","title":"Indicate a mutually exclusive option in a checkboxes group such as \"None of the above\"","text":"

    In the following example when you make a selection of a fruit and then choose one of the mutually exclusive options, your prior selections will be deselected.

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#capture-and-show-local-date-and-time","title":"Capture and show local date and time","text":"

    Sometimes we want to show the user the local date and time to ensure their time settings are correct.

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#show-a-timer-in-an-item","title":"Show a timer in an item","text":"

    Let's say you want to show a timer of how long someone has been on a single item. This calculates the time since item open and displays number of seconds since then in a tangy-box.

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#capture-the-time-between-two-items","title":"Capture the time between two items","text":"

    Sometimes we want to know how much time passed between two points in a form. This example captures, the start_time variable on the first item, then end_time on the last item. Lastly it calculates the length of time.

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#hard-checks-vs-soft-checks","title":"Hard checks vs. soft checks","text":"

    A \"hard check\" using \"valid if\" will not allow you to proceed. However a \"soft check\" using \"warn if\" will allow you to proceed after confirming.

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#set-selected-value-in-radio-buttons","title":"Set selected value in radio buttons","text":"

    In the following example we set the value of a <tangy-radio-buttons>.

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#dynamically-prevent-proceeding-to-next-section","title":"Dynamically prevent proceeding to next section","text":"

    In the following example hide the next button given the value of some user input.

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#proactive-input-validation","title":"Proactive input validation","text":"

    In the following example we validate an input after focusing on the next input. This approach is more proactive than running the validation logic when clicking next or submit.

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#content-box-with-tabs","title":"Content Box with Tabs","text":"

    In the following example we display content in a set of tabs.

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#dynamic-changing-of-text-color","title":"Dynamic Changing of Text Color","text":"

    In the following example we change the color of text depending on a user's selection.

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#use-skip-if-to-reference-variable-inside-tangy-inputs-group","title":"Use skip-if to reference variable inside tangy-inputs-group","text":"

    In the following example a skip-if refers to an other variable local to the group itself is in. The trick is using backticks around the variable name (not quotes) you are referencing and prepending the variable name you are referencing with ${context.split('.')[0]}.${context.split('.')[1]}..

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#use-valid-if-to-reference-variable-inside-tangy-inputs-group","title":"Use valid-if to reference variable inside tangy-inputs-group","text":"

    In the following example a valid-if refers to an other variable local to the group itself is in. The trick is using backticks around the variable name (not quotes) you are referencing and prepending the variable name you are referencing with ${input.name.split('.')[0]}.${input.name.split('.')[1]}.. Watch out for the gotcha of not using input.name instead of context like we do in a skip-if.

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#dynamic-location-level","title":"Dynamic Location Level","text":"

    In the following example we empower the Data Collector to select which Location Level at which they will provide their answer. This example can also be used in a more advanced way to base the level of location required for entry given some other set of inputs.

    Run example - Open Editor - View Code

    "},{"location":"editor/form-developers-cookbook/#prevent-user-from-proceeding-during-asynchronous-logic","title":"Prevent user from proceeding during asynchronous logic","text":"

    Sometimes in a form the logic calls for running some code that is asynchronous such as database saves and HTTP calls. As this logic runs, we would like to prevent the user from proceeding in the form. This is a job for a <tangy-gate>. Tangy Gate is an input that by default will not allow a user to proceed in a form. The gate can only be \"opened\" by some form logic that set's that Tangy Gate's variable name's value to true. This gives your logic in your forms an opportunity to run asynchronously, blocking the user from proceeding, then when async code is done your code sets the the gate to open.

    Run example - Open Editor - View Code

    "},{"location":"editor/getting-started-editor/add-sections/","title":"Adding Sections and Question to a Form","text":"

    To add a new section to your instrument, hit \"ADD SECTION\".

    The interface allows a drag-and-drop feature which enables reordering of the sections already created. The order in which the sections are listed, is the same as the sequence of screens that will be shown on the tablet when the tablet user is filling in the form.

    "},{"location":"editor/getting-started-editor/add-sections/#section-editor","title":"Section Editor","text":"

    Upon adding a new section, or selecting to \"EDIT\" your instrument section, you will see the section editor screen below.

    If this is a new section, you might give it a new name from the section header. Click the pen icon on the right of the blue bar and overwrite the \"title\". Or any other of the configuration options. Then hit SUBMIT to save your edits.

    "},{"location":"editor/getting-started-editor/add-sections/#section-options","title":"Section Options","text":"

    Each one of the sections has some options that you can control:

    Show this section in the summary at the end -- mark only if this section is the last one, and if you have coded some summary/feedback otherwise leave unchecked.

    Hide the back button -- checking this option will remove the Back button from the section when rendered on the tablet.

    Hide the next button -- hides the Next button on a section. Sometimes advancing the page may depend on the selection of an item, just like it is on some EF inputs. Generally, you keep this unchecked.

    right-to-left orientation -- switches the position of the Back and Next buttons for RTL languages.

    Hide navigation labels -- Hides the label from the Back and Next buttons so that it becomes an arrow.

    Hide navigation icons -- Hides the arrow from the back and Next buttons. If both this and the above are checked you will only see an orange button without labels and text.

    Threshold: Number of incorrect answers before disabling remaining questions -- This option is used in conjunction with radio button questions only. Set it to the number of consecutive incorrect replies before the test is discontinued. You must mark an option in the radio button group as Correct for this to work. Only one correct option per question can be defined.

    To add an item to your instrument section, click

    This opens the item type selection interface.

    These elements are subdivided into groups of item types (e.g., inputs, location, lists, misc):

    "},{"location":"editor/getting-started-editor/add-sections/#inputs","title":"Inputs","text":"

    INPUT-DATE: This item type renders a calendar widget on the tablet

    INPUT-TEXT: This item type is a standard numbers and letters field

    INPUT-TIME: This item type displays a clock hour selection on the tablet

    INPUT-NUMBER: This item type opens up the number keyboard on the tablet and doesn't allow any other non-number characters to be inserted here

    "},{"location":"editor/getting-started-editor/add-sections/#location","title":"Location","text":"

    GPS: This item type automatically collects GPS coordinates of the tablet

    LOCATION: The location item type requires a list of locations, e.g. school names by district and region to be imported to the Tangerine editor. Check out the location list section

    "},{"location":"editor/getting-started-editor/add-sections/#lists","title":"Lists","text":"

    CHECKBOX: This item type allows for multiple answers to be selected from a list of options

    CHECKBOX GROUP: Allows for multiple answer options to be selected from a group of options

    DROPDOWN (select): This item type allows for a single answer selection for longer lists

    RADIO BUTTONS: This item type only allows for a single answer selection from a list of answer options

    "},{"location":"editor/getting-started-editor/add-sections/#miscellaneous","title":"Miscellaneous","text":"

    IMAGE: the image items allows you to select an image already uploaded by the media library and present it to the user on a particular section

    SIGNATURE: this input type allows you to capture a signature by the assessor.

    HTML CONTENT CONTAINER: This item type allows for flexible integration of headers, help text, or transition messages that do not require any user input or response.

    QR CODE SCANNER: This item allows scanning of a QR and Data Matrix codes. Tangerine will capture and save the target info (e.g. URL).

    EF TOUCH: This item type is to assess children's executive functions, including working memory, inhibitory control, and cognitive flexibility (requires RTI manual support to upload your images and sounds).

    TIMED GRID: This item type facilitates timed assessment approaches, e.g., to assess letter sound knowledge, oral reading fluency or math operations.

    UNTIMED GRID: This item type facilitates assessment approaches that are not timed, but require many items, e.g. oral counting, untimed reading comprehension tasks, etc.

    CONSENT: This item is a special function for participant consent. If the participants responds that no consent is given, the form will be closed and data saved accordingly.

    Depending on the element chosen, an interface for providing more detail on the item being rendered/created is presented in the Item Editor.

    "},{"location":"editor/getting-started-editor/create-new-form/","title":"Creating a New Instrument/Form","text":""},{"location":"editor/getting-started-editor/create-new-form/#prerequisite-existing-group-or-create-a-new-group","title":"Prerequisite: Existing group or create a new Group","text":"

    After creating a group or opening an existing one, you will be presented with various options. The first action that you may wish to take is to create a new Form

    From the main menu options, select Author and then click Forms. This will bring up a listing of all forms for this group.

    You will notice that there is also a User Profile form. This form represents the profile each user has to fill in on the tablet, after they create their user login details. All information that you require in the user profile is attached to each record in the CSV export file. The user profile represents your assessor's information.

    Click the plus icon to create a new form. A new form with a default name is created and a default section is placed into this form. To change the form's name, type in the new text and click Save.

    "},{"location":"editor/getting-started-editor/create-new-form/#form-actions","title":"Form Actions","text":"

    Beside each of the forms you will see some action available. From here you can Edit, Print, Copy, Delete, or Archive a form. We recommend that you archive your forms instead of removing them. This will ensure that you can export the data of a form that is no longer in use.

    Click on the pen icon to modify the form. Each form contains a number of sections that represent your form's pages

    Click on the print icon to open a new printable menu where you can select two of the print details. This printable view we often use to quality assure (QA) the instrument or to get a list of variables and their definition. You can also use the print screen to save paper copy of your form.

    Click on the copy icon to create a copy of the current instrument. You can copy an instrument to a different group or to the current one.

    Click on the trash icon to delete this form.

    Click on the archive icon to archive an assessment. All archived forms are moved to the bottom of the page

    If a form is archived, click the un-archive button to activate a form. Only active forms are displayed in tablet listing of forms to the assessor.

    "},{"location":"editor/getting-started-editor/create-new-group/","title":"Creating a New Group","text":"

    Upon logging into your Tangerine instance, you will see a screen listing your Tangerine groups. You might think of groups as discrete data collection efforts that might contain several instruments or forms. If you have, e.g., a baseline data collection and an endline data collection for the same project, you might make these two different groups. When packaging your instruments into the apk (.apk is the application installation file format for Android devices) for installation on an Android device, Tangerine packages all instruments in a group. Thus, you should set up groups and categorize instruments accordingly.

    On the main group listing page, click \"+\" button in the bottom right corner to create a new group

    A group can be configured to display a drop-down to display available content-sets for which to configure the new group.

    Enter a name for your group and click Submit

    WARNING: If you are using the free service you are not able to create new groups.

    If you are a Tangerine subscriber, or run Tangerine on your own server, the user1 account can be configured to be the only account with permissions to create new groups. If this is desired, please send a request for this configuration to support@tangerinehelp.zendesk.com Any Admin user can create a new group unless configured for the user1 account only

    "},{"location":"editor/getting-started-editor/downloading-your-data/","title":"Downloading your data (Download CSV)","text":"

    All collected and synced data is available for you to download in a CSV format from the server. Each file downloaded contains some metadata output plus all of you variables. To find out how your variables map to the values in the csv file, please inspect your meta data export first (accessible from the form printing action under the Author tab)

    To access the csv generation and download screen go to your group and then select Data. Depending on the permissions you have for your group, you will see Dashboard, Download Data/Export, and Uploads tabs available for selection. Under the Download Data tab there are 3 options:

    Request spreadsheet - here you can create a download link for your csv of bundle multiple export files in a zip Spreadsheet Requests - here you will find a historical of generated requests that contain the exported data for the selected forms, at the time of the request. Note that we delete this links after one month, meaning that here you will see active links for only the past month Manage Spreadsheet templates - this links allows you to name and create a template with only selected columns for your csv export

    Let's see how to generate a spreadsheet. Click the Request Spreadsheet button. On the next page you will see a header filter, where you can select the month and year for the export, or leave as is to export all data. You will also see a filter for \"Exclude PII\" - marking this option allows you to exclude personal information from the export (Note that only variables that have been configured as PII will be excluded). If you want you can also give an optional description to your Request.

    Under the form selection area, you will see that you can select multiple form results to be exported or all forms. If you have created a data template you can slo select it from the drop down list, otherwise leave as \"All data\"

    In the below example, we have 3 forms selected and no filters for month/year. Click the Submit Request button to start the export generation. Note that while the generation of the file is in progress you can mouse away and do other actions in Tangerine. We will see how to get to a previously generated request further down.

    While the files are being generated you will see their status updated.Below I see that there are 3 forms in the queue, the first one is starting, and the other two are queued. You can also see. on the top right the \"download all\" button is greyed out

    Once the files are generated and the request completed you can download them. On the below screenshot we see:

    We can download each of the files individually by clicking Download file link on the right side of each form name We can download all files in a zip but clicking the Download all button on the top right.

    Now let's go back to the Data tab. This time under the Data Download option click the Spreadsheet Request button. This will bring you to all historically generated spreadsheet requests. On the below screenshot I see that there are 12 pages of previous/historical requests. On the most recent page, which is the one that loads by default, I see that the top file is with status Available, meaning that I can download it and the rest of the requests on this page have already been removed, since they are older than 1 month. The status for those is File removed and there is no link. From this screen, you can hit the download button beside the request to get the zip file containing all form results. Or, you can click the More info link to go to the same screen that we saw earlier during the file generation and download the files individually.

    Warning

    To make use of the Exclude PII function, all of your personally identifiable inputs must be marked like such on the input edit page, or when inserting a new input/question on the insert new input page. All data must be collected prior to this configuration

    To correctly interpret your data you need to know each variable and the corresponding values for the answer option. Please use the form's metadata print function under the form listing (go to Author->Forms). Access the metadata by clicking the print icon next to the form name and then select metadata.

    Each CSV file includes all data from the form responses. We can think of these data as data and metadata. Each line of the data file represents one data entry for your instrument. The values displayed under each variable correspond to the values you have assigned to each response option when designing your instrument.

    For each variable in Tangerine you will see a column or columns in the csv file. Some outputs like checkboxes and grids spread across multiple columns. For checkboxes you will see a column named using a this pattern \"VariableName_Value\". This means that for each option value pair for the checkbox group there will be a column . Similarly grids provide columns named \"\"GridVariableName_ItemPosition\" meaning that for each item in the grid you will see a column with the item position.

    If the CSV generation was successful, the following screen will present the group name, Form id , Start time and progress of the CSV download. Click Download to download the file.

    Once the CSV has been downloaded, you can find it in your Downloads folder.

    "},{"location":"editor/getting-started-editor/downloading-your-data/#metadata-included-in-csv","title":"Metadata included in CSV","text":"

    To correctly interpret your data you need to know each variable and the corresponding values for the answer option. Please use the form's metadata print function under the form listing (go to Author->Forms). Access the metadata by clicking the print icon next to the form name and then select metadata.

    Each CSV file includes all data from the form responses. We can think of these data as data and metadata. Each line of the data file represents one data entry for your instrument. The values displayed under each variable correspond to the values you have assigned to each response option when designing your instrument.

    For each variable in Tangerine you will see a column or columns in the csv file. Some outputs like checkboxes and grids spread across multiple columns. For checkboxes you will see a column named using a this pattern \"VariableName_Value\". This means that for each option value pair for the checkbox group there will be a column . Similarly grids provide columns named \"\"GridVariableName_ItemPosition\" meaning that for each item in the grid you will see a column with the item position.

    Each csv output from data collected by Tangerine has a set of metadata variables that are automatically output. Here is the current list:

    Variable Name Meaning _id 32-digit uuid identifying the unique form taken formId text name of the form startUnixtime unix timestamp that the form was first opened endUnixtime unix timestamp that the form was completed lastSaveUnixtime unix timestamp that the form was last opened and saved buildId 32-digit uuid identifying the app version buildChannel type of build: will be either build or production deviceId 32-digit uuid identifying the registered device groupId identifyer of the Tangerine group to which this device is registered complete TRUE or FALSE, whether the form is complete tangerineModifiedByUserId 3 2-digit uuid identifying the registered user who filled the form caseId Only for Case module configuration,32-digit uuid identifying the Case the form is associated with eventId Only for Case module configuration, 32-digit uuid identifying the Event within the Case that the form is associated with eventFormId Only for Case module configuration, 32-digit uuid identifying the Form within the Event within the Case that the form is associated with participantId Only for Case module configuration, 32-digit uuid identifying the participant for whom the form was filled GridVar.duration For a grid variable GridVar, this indicates the time limit/duration for the grid GridVar.time_remaining For a grid variable GridVar, this indicates the time remaining on the grid GridVar.gridAutoStopped For a grid variable GridVar, this indicates if the grid stopped using the auto stop rule GridVar.autoStop For a grid variable GridVar, this indicates the number of items that trigger the auto stop GridVar.item_at_time For a grid variable GridVar, this indicates the item at the Xth second GridVar. time_intermediate_captured For a grid variable GridVar, indicates the time time of the intermediate capture. GridVar.number_of_items_correct For a grid variable GridVar, indicates the number of correct items on the grid GridVar.number_of_items_attempted For a grid variable GridVar, indicates the number of items attempted on the grid GridVar.items_per_minute For a grid variable GridVar, indicates the number of items per minute read by the child. SectionId_firstOpenTime For a section with ID \"sectionId\" this it eh time stamp when it was first opened. sr_classId For Teach module configuration, the Id of the class for this student sr_studentId For Teach module configuration, the Id of the student"},{"location":"editor/getting-started-editor/downloading-your-data/#user-profile-metadata","title":"User Profile Metadata","text":"

    The information for the user logged in to the Tangerine app is also included in the CSV outputs. This includes the metadata below. The location metadata is based on the location defined in the location list. The table below includes three location levels as an example.

    Variable Name Meaning user-profile._id 32-digit uuid identifying the registered user who filled the form user-profile.item-1_firstOpenTime unix timestamp of when the form was opened user-profile.item-1.first_name user's first name user-profile.item-1.last_name user's last name user-profile.item-1.gender user's gender user-profile.item-1.phone user's phone number user-profile.item-1.location.Region user's assigned Region user-profile.item-1.location.District user's assigned District user-profile.item-1.location.Village user's assigned Village"},{"location":"editor/getting-started-editor/downloading-your-data/#unix-timestamps-conversion","title":"UNIX Timestamps conversion","text":"

    To convert any of the unix timestamp inputs to readable dates, use this formula in Excel in a new column: =A2/(60*60*24000) +\"1/1/1970\" Replace the A column with the corresponding column containing the timestamp. Now select the entire column and format it as Date or date + time. This will give you the human readable date and time.

    What can I do with Tangerine\u2019s timestamps?

    Check precise duration of an assessment or subtests. If you notice your overall assessment time (per assessor or per group) is lengthy, you may wish to use the timestamps to provide data on which subtests are the most time consuming. Confirm when data was collected. If you have suspicions that your data collector may have manually changed the values on your Date/Time screen against your instructions, you can check for inconsistencies between the Date/Time subtest data and the timestamps, as these cannot be altered by the user unless s/he also alters the date and time settings on the device.

    "},{"location":"editor/getting-started-editor/downloading-your-data/#downlaoding-data-video","title":"Downlaoding data video","text":""},{"location":"editor/getting-started-editor/edit-form/","title":"Editing the Form","text":"

    Once you have selected to edit your new or existing instrument/form, you will see a screen like the one below. This view lists the different sections of your instrument. Each section can contain one or more items.

    On the tablet, items in any one section can be seen on a single screen. The user moves through items by \"scrolling\" down the screen. The user moves through sections by hitting \"Next\" or \"Back\" on the tablet.

    The form editor provides the interface for adding and editing instrument sections and items. This interface provides controls that make the following actions possible:

    EDIT HTML - Clicking this button shows you the HTML code behind the form. Please edit the HTML with care.

    PREVIEW -- This control enables you to have a preview of your form in the current state

    SAVE -- This control allows you to save the form in its current state

    ADVANCED -- This Control enables you to access the on-open logic and on-change logic. This logic is used for skipping an entire instrument section.

    ADD SECTION -- This allows you to add a section of items to your instrument.

    For each section there are two actions available.

    COPY SECTION -- This icon copies the section and autmatically renames all variables.

    EDIT -- This icon opens the interface to edit an instrument section (e.g., add items)

    DELETE -- This icon deletes this instrument section

    In this view, you can drag sections to reorder them.

    "},{"location":"editor/getting-started-editor/editor-overview/","title":"Overview","text":"

    Tangerine contains an extremely extensible foundational framework that allows the form developer to highly customize the core functioanlity, actions, and progression. The following section provides guidance and common examples for extending Tangerine.

    "},{"location":"editor/getting-started-editor/editor-overview/#what-is-a-tangerine-form","title":"What is a Tangerine form?","text":"
    • Create a New Group
    • Create New Form
    • Editing the Form
    • Adding Sections and Questions
    • Different Input Types
    • Location list import
    • Skip Logic
    • Validation
    • Why custom build forms?
    • Custom Form Building in JavaScript
    "},{"location":"editor/getting-started-editor/editor-overview/#tangerine-preview","title":"Tangerine Preview","text":"
    • Tangerine Preview
    "},{"location":"editor/getting-started-editor/editor-overview/#_1","title":"Overview","text":""},{"location":"editor/getting-started-editor/input-types/","title":"Different Input Types","text":""},{"location":"editor/getting-started-editor/input-types/#item-editor","title":"Item Editor","text":"

    The item editor screen is similar for many of the item types. It usually contains the following elements:

    Variable name: This name has to be unique for any instrument/form, as this will be used for the CSV data output as column header with each observation/child assessed/interview being a row. Avoid special characters and spaces, use lowercase only (e.g., \"age\").

    Label: This will be the item label/name that will be displayed to the user (e.g. \"How old are you?\")

    Question number: If you input a number in here, you will see that the entire questions is moved to the right and the question number stands out when looking at the page. Use this if you are looking for a visual effect like this.

    Hint Text: This field allows you to add text that acts as a hint for the user (e.g., \"Enter child's age or year of birth, if known\")

    Toggle (On/Off) settings per question

    Required: Selecting this checkbox marks the element as a required field. This ensures that users will enter a value before proceeding to other instrument sections or finalizing the instrument/form.

    Disabled: Selecting this checkbox marks the element as inactive. The item is visible to the user on the tablet, but its value cannot be changed.

    Hidden: Selecting this checkbox makes the element inactive AND invisible on the tablet.

    PII - Personally Identifiable Information. This setting marks the input as PII which lets you remove this field from the CSV export file when this option is selected. Easily share data by de-identifying it whn using this option.

    You will have access to two more tabs allowing you to add Skip Logic and Validation to your questions.

    Conditional Display tab

    Skip if - Use this field to define logic for the input to be omitted if the condition is true.

    Show if - Use this field to define logic for the input to be displayed when the condition is true.

    Find examples of skip logic here

    Validation tab

    Warn if - fail the user submission with a warning only once. This is to alert the user that perhaps they are entering something out of the defined boundaries. The warn if logic will allow the user to proceed if they click the Next/Submit button a second time.

    Warning Text - The warning text to be displayed to the user when the condition is triggered.

    Valid if - define logic to contain a valid definition of the input

    Error text - the text to be displayed when the above condition is not met.

    Find examples of validation logic here

    "},{"location":"editor/getting-started-editor/input-types/#gps-item","title":"GPS Item","text":"

    Use the GPS item to record the location (longitude & latitude) of the user while filling in the instrument/form.

    We suggest placing a GPS item always in its own section. Do not combine with other items.

    When selecting to add an item of the GPS type, Tangerine presents the below item editor screen.

    The following might be a way to configure this item:

    Variable name: Enter \"gps\".

    Hint Text: Leave blank

    Hit \"SUBMIT\" to see the below item added to the section editor.

    On the tablet this item will look like this:

    "},{"location":"editor/getting-started-editor/input-types/#location","title":"Location","text":"

    This item type offers a dropdown listing of predefined location information such as, e.g., region, district, and school name. Before you add this item to your form, you need to upload a location list and configure Tangerine:

    To see a video of how to do this Watch the video

    First, decide what levels you would like to show and prepare a CSV file that contains your locations and ids. Each column header will present a location level (e.g. column A header might be region; column B header might be district, etc.). Make sure each level/column header contains only a single word and no spaces.

    Second, define the location levels for Tangerine. Click Configure/Location Lists and add the desired levels using the '+' sign

    Click \"Create a New Location Level\".

    Enter the name of the \"highest\" location level under \"Label\" (e.g. region). Repeat this process for all other location levels, however, for each \"child\" level, select which is the parent level. E.g. in the case of district, the \"Parent Level\" would be \"region\", and so forth. Hit \"Submit\" to save.

    Warning

    You cannot delete location levels. Be careful and deliberate as you define them for your group. If you made a mistake or need to make changes, contact the Tangerine helpdesk.

    Next, click the Import tab and select \"Import CSV\". Double check that your CSV file contains only those columns that you have defined as levels and spelled exactly the same!

    Download a sample location list file with IDs:(https://drive.google.com/file/d/1y3X0aMJKRYx51--jPC_3OUkMpmkhyEn-/view)

    Once you selected the CSV, Tangerine will ask you to map the location levels you already defined to the column headers found in your csv file.

    Click on the small arrows to select the matching column header. For each ID field, select \"Map a column to a level and select \"AutoGeneratedID\" for the ID as shown in the example below.

    Then click \"Process CSV\" as shown in the screen above. Once processing is completed, you will receive a notification like this:

    Once you have successfully uploaded a location list and prepared Tangerine, you can add the location input item to your form. The following might be a way to configure this item, once you completed the above steps.

    If you think that your location list may change significantly, and you'd like to re-upload it at some point thus not implementing any changes manually, consider adding manual IDs to your location file. In the instruction above, you saw how to add the Autogenerated ID that Tangerine inserts. These IDs, however, are not persisted when you re-upload your location file. In such cases, where you know that you'd rather re-upload the entire file, we recommend that you insert an ID column and you preserve those IDs across versions of your location list. By doing that you ensure that any matching on location IDs (and not on Location labels) will be persisted.

    To upload a location file with IDs, first create those IDS in the Excel file. Then, on the Map location field instead of selecting Autogenerated ID, select the column representing the ID for the corresponding level.

    Warning

    We highly recommend that you create a location list export before importing any new data. Click the Export tab and export your current location list. ALways work with this export file to make sure your list is up-to-date

    Warning

    Upon upload you are wiping out the location list. All previous results collected will be missing the labels for those location and will contain only the old IDs; all data on the tablets under the Visits tab will show the ID rather than the label. This is why we highly recommend altering the location list manually or maintaining the IDs across different version of the location list in your Excel file.

    Check out this Excel file to see a location list with IDs that you can import in Tangerine. The formula for generating the IDs can be copied to your own file: Download a sample location list file with IDs: (https://drive.google.com/file/d/1y3X0aMJKRYx51--jPC_3OUkMpmkhyEn-/view)

    Variable name: Enter \"location\".

    Hint Text: Leave blank

    Show levels (ex. county,subcounty): Enter \"province,district,school\"

    Show meta-data: Leave blank

    Hit \"SUBMIT\" to see the below item added to the section editor.

    On the tablet this item will look like this:

    Warning

    Without a location list, no location will be displayed, and the item will be seen as \"loading\".

    "},{"location":"editor/getting-started-editor/input-types/#checkbox-group-checkbox-radio-buttons-or-dropdowns","title":"Checkbox group (Checkbox, Radio Buttons, or Dropdowns)","text":"

    This item type lets you define a checkbox item that lets a user pick one or more options.

    The following might be a way to configure this item:

    Variable name: Enter \"books\".

    Label: Enter \"What kind of books do you like to read?\"

    Hint Text: Enter \"Tick all that apply\"

    Value (answer option): Enter the data value for the first answer option, e.g., \"0\"

    Label (answer option): Enter the label for the first answer option, e.g., \"None\"

    Hit \"ADD ANOTHER\" to add additional answer options, e.g.:

    Value (answer option): \"1\"

    Label (answer option): \"Storybooks (fiction)\"

    Hit \"ADD ANOTHER\" to add additional answer options, e.g.:

    Value (answer option): \"2\"

    Label (answer option): \"Books about real things (non-fiction)

    When done adding all answer options, hit \"SUBMIT\".

    Warning

    The item type \"CHECKBOX\" only adds a single checkbox to the form, with the item label being the answer option label.

    On the tablet this single checkbox item will look like this:

    "},{"location":"editor/getting-started-editor/input-types/#radio-buttons","title":"Radio Buttons","text":"

    Radio buttons are an item type used for items that allow for only one answer. The configuration for radio buttons is the same as for checkbox group with one exception. You will see that radio button options have a check mark to indicate which answer is correct. This is used in conjunction with the Threshold defined in the section header.

    When you have a threshold defined as 4, and for each question there is only one question option defined as correct, Tangerine will discontinue (hide the questions) after 4 consecutive replies are given as not correct. You can use this in EGMA tasks or in any other scenario where this makes sense

    On the tablet the radio button item will look like this:

    "},{"location":"editor/getting-started-editor/input-types/#dropdown","title":"Dropdown","text":"

    Dropdown is an item type used for items that allow for only one answer to be picked from a dropdown list of items. This item type is convenient when there are many options to choose from. The configuration for a dropdown item is the same as for checkbox group.

    On the tablet the dropdown item will look like this:

    "},{"location":"editor/getting-started-editor/input-types/#timed-grid","title":"Timed Grid","text":"

    This item type facilitates timed assessment approaches, e.g., to assess letter sound knowledge, oral reading fluency or math operations. The following might be a way to configure this item:

    Variable name: Enter \"letter_sound\".

    Number of columns: Enter the number of columns by which you'd like to organize the items. Enter, e.g. \"4\" (Tip: choose less columns for larger items, like words or operation problems)

    Hint Text: Leave blank

    Auto Stop: The autostop field defines the number of consecutive incorrect items, starting from the first one, after which the test stops automatically. For example, with an autostop value of 10, if a child has the first 10 items all incorrect, the test will stop. If a child has the first 4 items correct and then the following 10 items incorrect, the test will not autostop.

    Mark entire rows: This option allows the user to mark and entire row of items as incorrect (e.g. if a child skipped an entire row of sounds in a letter sound assessment)

    Duration in seconds: Enter the time allowed to complete this assessment, e.g. \"60\" for 60 seconds or one minute.

    Options (each option separated by a space): Enter all grid items here. Separate each item by a space from the next; if you have extra spaces please remove them!

    When done adding all answer options, hit \"SUBMIT\".

    Warning

    For these kids of assessments there are usually instructions preceding the assessment items. Insert those instructions as a \"HTML CONTENT CONTAINER\" item first, as shown below, followed by the \"TIMED GRID\" in the same section. We recommend to only feature the instructions (HTML Content) and Timed Grid in any one section of your instrument/form

    On the tablet the timed grid item will look like this:

    "},{"location":"editor/getting-started-editor/input-types/#html-content-container","title":"HTML Content Container","text":"

    This item type allows for flexible integration of headers, help text, or transition messages that do not require any user input or response. You can treat this container as a variable and hide or show different instructional text upon the selection of different options.

    The following might be a way to configure this item:

    Variable name: Enter \"Assessor instructions\".

    Mark entire rows: This option allows the user to mark an entire row of items as incorrect (e.g. if a child skipped an entire row of sounds in a letter sound assessment)

    Rows 1-X: Insert assessor instructions, use html tags to insert line breaks or formatting (e.g. for a line break; text for bolding a piece of text, etc.).

    When done adding all answer options, hit \"SUBMIT\".

    On the tablet this HTML container item will look like this:

    "},{"location":"editor/getting-started-editor/input-types/#copying-items","title":"Copying Items","text":"

    If you have an element and/or content which is the same as a previous element (e.g., radio buttons) that you would like to insert into your instrument quickly, without having to click \"INSERT HERE\" again, there is a COPY feature that you can use to do this. First, enter your original content (e.g., variable name, labels, and values) and then click SUBMIT. Once the first step is complete, next you click on the icon. Doing so automatically creates a duplicate of all your original content, except the variable name, which you will need to edit, if desired. In the image below, you can see that all duplicates are auto-populated with the name \"widget,\" followed by an underscore, and a mix of letters and numbers (always different from the previous copy). If you would like to, you can edit all the content of the copy to fit your needs.

    "},{"location":"editor/getting-started-editor/skip-logic/","title":"Skip Logic","text":"

    Every instrument/form, section, and individual item provides an interface for adding logic, e.g. skip logic, that controls the interactivity and presentation of the instrument, section, or item.

    There are two types of skip logic that can be applied:

    • On form level - used to skip an entire section and implement logic that is applicable to the entire form
    • On section/page level
      • Most common case: You can implement those in the item's 'Skip If' field, or
      • Used for more complex conditions Implement the skip in the section's on-change logic

    The functions that we use for skip logic are:

    • getValue('name') - to check the value of input 'name'
      • Use this for Text, Number, Dates, Time, Radio buttons, or Drop down lists
    • getValue('name').includes('value') - to check if 'value' is in the selected items of 'name'
      • Use this call to check if a value is in the list of selected values of a checkbox group input.
    • grid specific functions - look at the end of this page for more information.

    Join skip logic conditions using the && (AND) and || (OR) operators getValue('repeatedgrade') == '1' && getValue('age') >= 1

    Negate a condition using the ! (NOT) operator getValue('repeatedgrade') != '1' Or !getValue('grades_taught').includes('1')

    "},{"location":"editor/getting-started-editor/skip-logic/#logic-at-question-input-level","title":"Logic at Question/ Input level","text":"

    When we tap the Conditional Display tab, we see that here we can enter logic to hide or show a question based on previous input on that or other previous sections.

    In the above screenshot we see that there is a \"Skip if\" input a \"Show if\" input. The skip if condition will skip the question if the condition is true. The show if condition will show the question if the condition is true. For example:

    To skip a question when the value of a previous question is not equal to 777, use the skip if condition To show a question when the value of a previous question is equal to 777, use the show if condition You can see how the two possible ways of skipping are opposites of one another. We can use either logic for each scenario but sometimes it is easier to think in a positive condition and other times it is easier to think in a negative condition.

    In all skip logic conditions, except for those based on grids (timed input), we use the getValue() function. To write the conditions from above in terms of skip logic we need:

    The value(s) to be used in the condition The variable name Here is how the above condition looks in skip logic:

    To skip a question when the value of question with variable homework is not equal 777 To show a question when the value of question with variable homework is equal 777

    This logic can be used to compare the values of Text, Number, Email, Radio Buttons, Dropdown select input types.

    Here is the actual skip logic.

    Skip if: getValue('homework') != '777' Show if: getValue('homework') == '777' You can see how similar the logic is. The == sign above means \"equal\" and the != sign means \"not equal\"

    You can enter only one Show-if or Skip-if condition per question. You can also use the && (AND) and || (OR) logical operators to combine conditions. We will not look into that here.

    The above use of the function applies to all input types except for Checkbox group, Timed Grid, Untimed Grid, and Location

    To build logic based on the answers of a Checkbox group question, we use the getValue function but in a different way. Here we check if one of the selected options is the desired one. To skip a question when ONE of the values of a Checkbox question with variable homework is not equal 777 To show a question when ONE of the values of a Checkbox question with variable homework is equal 777 Here is how this condition will look

    Skip if: !getValue('homework').includes('777') Show if: getValue('homework').includes('777') Note how above we are asking if one of the selected options is 777 or if it isn't 777 - this is done with the ! (NOT) operator in front of the function.

    Take a look at how these examples look in Tangerine:

    For radio button question similar to below, where we want to show up the Other Specify input only when Other is selected:

    Use this logic in the homework_other question

    For a checkbox group question similar to the one below

    Use this logic to show to show a question in the stu_language_other

    Warning

    The skip logic commands used in Tangerine are case-sensitive and space-sensitive. You must type precisely the name of the variables which you want to reference.

    Warning

    Use single straight quotation marks to demarcate variables names ', do NOT use single slanted quotation marks ' or double quotation marks \".

    "},{"location":"editor/getting-started-editor/skip-logic/#logic-at-instrumentform-level","title":"Logic at instrument/form level","text":"

    At the instrument/form level, accessing this logic editor is via advanced settings in the section editor.

    Click on ADVANCED to see the screen below with \"on-open\" and \"on-change\" entries.

    As outlined earlier, at the item level, such logic can be added in the \"Show if\" field in the item editor.

    On-open and on-change

    As the name suggest, on-open logic is only executed when the form is opened whereas on-change logic is always executed whenever a change happens in the whole form. When selecting on-open logic either at the instrument/form level or in the section editor, the following screen appears. The interface allows JavaScript logic to be incorporated into the instrument.

    "},{"location":"editor/getting-started-editor/skip-logic/#skip-logic-examples","title":"(Skip) Logic Examples","text":"

    You want to skip an entire section:

    Navigate to and select the \"on-change\" at the instrument/form level. This logic will not work if you insert it in a section (it must be defined on form level)

    In this example, the section gets skipped based on responses from a previous item, e.g., if the respondent answered negatively to a previous question \"Do you have children?\". Note that the sectionID is provided in Tangerine in the section details as shown below. Form level skip logic is used to present or hide an entire section page to the user. This is very useful when managing a workflow and you need to display some sections but hide others according to the selected option for a question. For example, you can show a certain section only for grade 1 and hide it if grade 2 is selected.

    if(getValue('children') == '1')\n{sectionEnable('item_1')}\nelse\n{sectionDisable('item_1')}\n

    You want to hide a set of items based on responses to an item in a previous section:

    Navigate to and select the \"on-open\" at the section level.

    In this example several items in this section are hidden based on the participant's response to the item about the child's schooling experience in a previous section.

    if(getValue('school') == '1')\n{itemShow('grade')\nitemShow('repeatedgrade')\nitemShow('dropout')}\nelse\n{itemHide('grade')\nitemHide('repeatedgrade')\nitemHide('dropout')}\n

    You want to hide a set of items based on responses to two items in a previous section:

    Navigate to and select the \"on-open\" at the section level.

    In this example the item \"teachers_name\" should only be shown if the participant's previous response to \"teacher_available\" was yes = 1 AND if the participant' previous response to \"class_selected\" was \"1\".

    if(getValue(' teacher_available') === '1' && getValue('class_selected') === '1' )\n{itemShow('teachers_name')}\nelse\n{itemHide('teachers_name')}\n

    The Logic interface offer syntax highlighting. This is handy when you have errors in your code. Below is an example of an error and sample message.

    "},{"location":"editor/getting-started-editor/skip-logic/#logic-at-section-level","title":"Logic at section level","text":"

    At the section level, the logic editor can be accessed by editing the Section Details clicking the pen icon on the right of the blue bar (where one can also rename the section).

    "},{"location":"editor/getting-started-editor/skip-logic/#skip-logic-with-grid-specific-functions","title":"Skip logic with grid specific functions","text":"

    You may be in the situation where you are required to perform a skip based on some results from a grid. We provide four functions that you can use in your skip logic to show or hide questions or sections based on the results of a grid.

    Showing a question based on the number of attempted items on a grid

    If you'd like to hide a question when the number of attempted items on a particular grid is over a certain threshold you can make use of the 'numberOfItemsAttempted(input)' function. If your grid variable is 'letter_sound' and the question you want to skip is 'Q_1' then in the question Q_1 I can insert the below skip logic(under Show If) to show it only when the number of attempted items on the grid is greater than 10

    numberOfItemsAttempted(inputs.letter_sound) > 10\n

    Showing a question based on the number of correct items of a grid

    Sometimes it may be the case where you want to show a question only if there are a certain N items on the grid answered correctly. In those cases, we make use of the 'numberOfCorrectItems(input)' function. If your grid variable is 'letter_sound' and the question you want to skip is 'Q_1' then in the question Q_1 I can insert the below skip logic(under Show If) to show this question only when the number of correct items on the grid is greater than 0

    numberOfCorrectItems(inputs.letter_sound) > 0\n

    Show a question only if the grid did not auto stop

    If you have set the autostop value of a grid with variable name 'letter_sound' and you want to show a question only when the grid did not discontinue due to a triggered auto stop, then you can insert the below logic into the question's Show If field:

    typeof inputs.letter_sound != 'undefined' && inputs.letter_sound.gridAutoStopped\n

    The use of the '!' gives us the opposite of the result returned by the function. If the grid stopped the result will be true. When we use the '!' in front of the function, it means that, when the grid did not stop we want a positive answer hence show the question.

    Show a question based on the words per minute read on a grid

    It may happen that you need to show a question only to advanced students. In those cases, we make use of the function 'itemsPerMinute(input)' This function returns the number of items per minute read by the student. We can use it, just as before, in the Show If input field of a question, like so:

    itemsPerMinute(inputs.letter_sound) > 35\n

    This call will force a question to be displayed only when the rate of reading was higher than 35 workds per minute.

    NOTE: All of the above functions can also be used to show or a hide an entire section page.

    "},{"location":"editor/getting-started-editor/validation/","title":"Validation","text":"

    Tangerine provides the option to check the validity of an input field. Navigate to the \"Valid if\" field in the Item Editor.

    When we tap the Validation tab, we see that here we can enter logic to validate a question based on previous or current input. For the number input type we can also directly enter min and max values. For all validation we also have the option to specify a default error message.

    Inputs that are common for all validation screens are:

    Warn-if: this input allows you to define logic to issue a warning if the condition is true. A warning validation means that clicking Next or Submit will fail the first time the user clicks it but will allow the user to continue on the second trial

    Warning Text: this is the text displayed to the user when a warning condition is triggered

    Valid if: this input allows you to define logic to issue an error if the condition is true. A valid if logic means that clicking Next or Submit will fail when the condition is met and the user is forced to correct the input

    Error text: this is the text displayed to the user when a validation error is triggered

    All warning and validation logic can make use of the getValue function but also you can access the current input's value by referring to input.value. In many cases we also use the parseInt function to convert the text input to a number.

    In the below example you see an input defined which will trigger a validation if the value is great then 5, Between 5 and 7 it is only a warning message, meaning that the user can proceed if they click Next/Submit again but if the value is greater then 7 the user has to correct the input.

    The easiest validation is for number type of inputs. There we can directly specify the minimum and maximum values for the number that we can accept.

    I will add an age input of type number. Then on the Validation tab i have defined min and max values, as well as error text.

    I want to add one more validation to my stu_number variable, making sure it is exactly 6 characters long. For this I will use input.value.length == 6

    Here is how this looks in student number input.

    "},{"location":"editor/getting-started-editor/validation/#question-input-validation-examples","title":"Question/ Input Validation Examples","text":"

    If, for example, the value entered into an \"INPUT-NUMBER\" field should have 9 or more characters, enter the following into \"Valid if\" for this item:

    input.value.length > 9\n

    Tangerine also allow you to compare the value entered in the current item to a value entered for another, earlier item. This might be the case, e.g. for attendance when observing a classroom. That is, when recording attendance, the number of children present should not exceed the number of children enrolled. Assume that a relevant variable name of the earlier item was \"boys_enrolled\" and the current items is about the boys present, this might be the validation logic to enter under \"Valid if\" for boys_present.

    parseInt(input.value) <= inputs.boys_enrolled.value\n

    If you want to validate that a number input is in between a particular range but also allow a 'No Reply' answer, use the below validation rule:

    input.value >= 0 \n&& input.value <= 10 \n|| input.value == 999\n

    Here we make sure that the user can only enter numbers between 0 and 10 but also 999 as a reply to this question.

    You can now use the Mutually Exclusive option on the checkbox edit page to achieve the same functionality.

    Deprecated

    If you have a list of checkboxes with the option No (value 0), NA (value 888), and some other options, and you'd like to make sure that the assessor cannot select the options No or the option NA along with other available options you need to implement a rule like the one below. The variable name in the below example is TQ1

    (!getValue('TQ1').includes('888') && !getValue('TQ1').includes('0')) \n|| (getValue('TQ1').includes('888') \n|| getValue('TQ1').includes('0')) \n&& getValue('TQ1').length == 1\n
    "},{"location":"editor/project_managment/class/","title":"Class module","text":""},{"location":"editor/project_managment/class/#setup","title":"Setup","text":"

    Add class to the T_MODULES property in config.sh. Create a new group via editor; this takes advantage of the class module which sets important class-related properties and file. If you /must/ use the create-group command, change homeUrl to dashboard and set uploadUnlockedFormReponses = true.

    "},{"location":"editor/project_managment/class/#feedback","title":"Feedback","text":"

    Feedback for each form item (subtask) can be entered using the Settings editor.

    Feedback is displayed if available on the Student grouping report. The following fields are available: - ${feedback.example} - ${feedback.skill} - ${feedback.assignment}

    The following code can be used to format feedback on the Student Grouping report:

    <div class='feedback-assignment'>${feedback.assignment}</div>\n
    <div class='feedback-example'>${feedback.example}.</div>\n

    Note that the use of these formatting commands are optional.

    Here is a sample feedback message that uses this formatting:

    These students are doing really well. Consider framing your feedback to these student as follows: <div class='feedback-example'>${feedback.example}.</div>\nReflect on these students results: why do you think did these students were particularly successful in ${feedback.skill}.\nWas there a specific ${feedback.skill} strategy or activity you used? Did they already know this content?\nIs there another strategy or activity they could do to extend their ${feedback.skill} skills?\nConsider giving these students supplementary story: <div class='feedback-assignment'>${feedback.assignment}</div> to read\nand make 3-5 inferential questions for them to answer. You may also consider engaging these students as peer mentors to\nothers as these other students do additional practice.\n
    "},{"location":"editor/project_managment/class/#scoring","title":"Scoring","text":"

    There are 3 options for scoring in Class: - Using a TANGY-TIMED grid - Using a hidden formId+_score field to store the calculated score value when the form is submitted using the on-change javascript - Using a score calculated at report run-time.

    The dashboard.service populaceTransformedResult function loops through the inputs; for each item type, it calculates the value, score, and max.

    It also keeps a running tally of the sum of all max values (totalMax).

    Here are the default rules for each input type: * TANGY-INPUT: * value: value field * score: value field * max: max field * TANGY-RADIO-BUTTONS: * value: loops through the options and uses the value from the non-empty option * score: value * max: Use value of the highest option. * TANGY-CHECKBOXES: * value: loops through the options and uses the value from the non-empty option * score: value * max: Use value of the highest option.

    For a TANGY-TIMED input, once the value and score have been calculated for each item and populated into an answeredQuestions array, we loop through this array and calculate aggregates for the tangy-form-item.

    • TANGY-TIMED:
    • value:
    • score: totalCorrect

    For tangy form items that use a _score field: Calculate the totalAnswers by subtracting 1 from the item.inputs.length (to account for the _score field) Use score for totalCorrect and totalAnswers for maxValueAnswer, unless the max value was assigned earlier.

    Finally, there is support for calculating the score at report-time by looping through answeredQuestions and summing the score and max values.

    "},{"location":"editor/project_managment/configuration/","title":"Configuration","text":""},{"location":"editor/project_managment/configuration/#app-configuration","title":"App Configuration","text":"

    app-config.json should have the following properties defined.

    • homeUrl:string The default route to load when no route is specified. Think of this as the root url
    • securityPolicy:string[]. This is an array of all the combinations of the security policies to be enforced in the app. NOTE: noPassword and password are mutually exclusive. Only one should be provided and not both.
      • password
      • noPassword
    • associateUserProfileMode: This is the mode that determines where a user profiles comes from after a user has created an account on a device. Note, a \"User\" is tied together across devices by a single \"User Profile\". The account on the device is simply a security mechanism for using the profiles.
    • remote: Selecting this will result in a user being promted to enter a \"code\" after they register an account. This code is the last 6 characters of their User Profile ID. Typically a Group Admin would create a User Profile doc on the server and then send this code to the person associated with the User Profile. When the user enters this code on the tablet, the tablet will reach out over the Internet and download the corresponding User Profile and any content associated with that User Profile. Because all content is downloaded for that user, it can also be used as a way to fully restore a user's data on a new or recovered tablet. However note that this data is mode is not compatible with using CouchDB sync settings on any form definitions' sync settings.
    • local-new: This option allows users who register an Account on a tablet to create a new User Profile. This is also the default if no option is selected.
    • local-exists: This option is useful when using devices are set up using the \"Centrally Managed Device\" setup which would result in a facility's User Profiles already being on that device. When this option is selected, a drop down of unclaimed user profiles appears when accounts are being registered.
    "},{"location":"editor/project_managment/editor-guide/","title":"Releasing updates to existing forms","text":""},{"location":"editor/project_managment/editor-guide/#gotchas","title":"Gotchas","text":"
    • If you remove an input from an item or move that input to another item, when a user resumes a form response that was created with the prior version, content for that input will appear to have dissappeared.
    • If you add remove an item from a form, when users resume form responses created on with the prior version, it will appear they have lost data since the item has been removed.
    "},{"location":"editor/project_managment/project_admin/","title":"Project Managment Overview","text":"
    • How do you manage a project in Tangerine?
    • What is a group?
    • Why do you need a group?
    • Creating a group
    • What is a role in Tangerine?
    • Creating roles
    • How to create new users and add them to a group?
    • How do you manage users?
    "},{"location":"editor/project_managment/project_admin/#mobile-device-use","title":"Mobile Device Use","text":"
    • What devices will Tangerine work with?
    • How do you manage devices?
    • How to manage Tangerine Updates?
    • Android Installation
    • Web Browser Installation
    • Tangerine Installation Decision Tree
    • Registration and Login
    • Administering Instruments
    • Resuming Instruments
    • Syncing Data
    • Location Data Management
    • Filter location data based on the user\u2019s profile location
    • Import a location list
    • Location list sample file with IDs
    "},{"location":"editor/project_managment/case-archive/case-archive/","title":"Case, Event and Form Archive and Unarchive","text":""},{"location":"editor/project_managment/case-archive/case-archive/#introduction","title":"Introduction","text":"

    We have released an update to Tangerine which allows for the archiving and un-archiving of both events, and forms within events. This is an extension of the already existing functionality by which an entire case can be archived. The purpose of this is to empower data management teams using Tangerine to \"clean up\" messy cases where extraneous data has been added to a case in error, or by a conflict situation. The purpose of this document is to summarize both the configuration to enable this, and to demonstrate the use of these functions. This functionality will only apply to the web-based version of Tangerine, and will not be available on tablets.

    "},{"location":"editor/project_managment/case-archive/case-archive/#enabling-access-to-archive-functions","title":"Enabling access to archive functions:","text":"

    Under \"Users and Roles\", either a new role needs to be created, or additional rights will need to be granted to an existing role to provide access to the archive and unarchive functionality. To access this, login to Tangerine, and go to configure for the specific group that you are working with.

    By default, the admin role will NOT have this functionality. You can either update the role, or create a new role as needed. Note that the USER1 account will have already have access to this functionality. We suggest that all users of the web interface be given their own accounts for better tracking of activity within Tangerine, and that you do not normally use the USER1 account. Under roles, you can either modify the admin account to add this access, or create a new role with access, and assign that role to users. Note that a single user can be assigned to more than one role. By clicking on the configure icon () next to the trash can icon, you can edit an existing role (such as the admin role above).

    You will see that there are 4 separate permissions related to archiving. You can actively apply these as needed for your project team. There are 2 archive permissions to add (can_archive_events, and can_archive_forms), and two additional unarchive permissions (can_unarchive_events, and can_unarchive_forms). These are in addition to the can_archive_cases and can_unarchive_cases permissions which previously existed. For each site, there may be reasons to manage these each independently.

    "},{"location":"editor/project_managment/case-archive/case-archive/#archiving-and-unarchiving-of-forms-and-events","title":"Archiving and unarchiving of forms and events:","text":"

    Once the correct roles have been applied to a user, they will be able to use the archiving/un-archiving functionality.

    In the upper right corner, there is a kabob menu ( ) that can access the archive and delete functions at the case level. Note that we suggest NEVER using the delete function, as it is much harder to restore a deleted case.

    Select the \"Archive\" option, and then click \"OK\" to confirm the archiving of a case.

    If you need to review an archived case, you can select the \"View Archived Cases\" checkbox on the cases screen.

    When you re-open an archived case, it shows with an indication that it is archived, and the menu will provide you with the option to un-archive it.

    Selecting Unarchive will restore the case and all forms back to a normal (unarchived) status.

    Additionally, this functionality will cascade down to the event and form level. If you have a duplicate event, or an event that was added in error. You can archive the event, and all the forms within the event by clicking on the \"archive\" icon ( ) to the right of the event.

    Click the icon and then confirm that you wish to archive the event by clicking \"OK.\"

    If you need to visualize an archived event, you can check the checkbox for \"Show Archived Events.\" The archived event will show as greyed out, with an \"Unarchive\" icon next to it ( ). If you select an archived event, the forms within it will also be archived, but similarly are viewable by checking the \"Show Archived Forms\" box. The forms will be greyed out and show the unarchive button to the right.

    Clicking on the unarchive button for an event will prompt you to confirm if you want to unarchive the event.

    Unarchiving an event will cascade down, and automatically unarchive any associated forms for that event. If you need to unarchive an event, and archive SOME forms within that event, you can unarchive, and then go into the event to archive individual forms within that event as needed.

    "},{"location":"editor/project_managment/case-module/case-data-model/","title":"Case Management Data Model","text":""},{"location":"editor/project_managment/case-module/case-data-model/#case-entities-and-relationships","title":"Case Entities and Relationships:","text":"

    Entities: Participant, Case, CaseEvent, EventForm, FormResponse

    Relationships:

    • A Case is related to many Participants.
    • A Case is related to many CaseEvents.
    • A CaseEvent is related to many EventForms.
    • An EventForm is related to one FormResponse.
    • An EventForm is related to one Participant.

    Then there are definition Entities that are not in the data:

    Entities: ParticipantDefinition, CaseDefinition, CaseEventDefinition, EventFormDefinition, FormDefinition

    "},{"location":"editor/project_managment/case-module/case-data-model/#how-the-case-entities-and-relationships-are-expressed-in-tangerine","title":"How the Case Entities and Relationships are expressed in Tangerine","text":"

    A typical Tangerine Case will feature: - one document (type = case) that has all of the Case-related meta-data mentioned below, and - multiple documents with forms data. These forms are linked by formResponseId in the case's eventForms array.

    There is not a 1-to-1 mapping between Tangerine entities and data persisted to the server. Records are saved in Tangerine as a TangyFormResponse doc, identified by \"collection\": \"TangyFormResponse\" in the Couchdb document.

    A TangyFormResponse is a very generic container for data; it does not by default manage any of its relationships. Most of the Case-related entities are saved in a single TangyFormResponse as \"type\": \"case\" and explicitly manages these relationships inside the eventForms array:

    {\n  \"_id\": \"8744ff38-4c3e-487d-814d-ddcb916a41d5\",\n  \"collection\": \"TangyFormResponse\",\n  \"type\": \"case\",\n  \"eventForms\": [\n    {\n      \"id\": \"c7b6ee21-793a-11ea-9144-710703689c79\",\n      \"complete\": true,\n      \"caseId\": \"c7b23330-793a-11ea-9144-710703689c79\",\n      \"participantId\": \"\",\n      \"caseEventId\": \"c7b6ee20-793a-11ea-9144-710703689c79\",\n      \"eventFormDefinitionId\": \"enrollment-screening-form\",\n      \"formResponseId\": \"c7b6ee22-793a-11ea-9144-710703689c79\"\n    },\n    {\n      \"id\": \"c7b6ee23-793a-11ea-9144-710703689c79\",\n      \"complete\": true,\n      \"caseId\": \"c7b23330-793a-11ea-9144-710703689c79\",\n      \"participantId\": \"8a46e841-d80c-4038-857c-7ae43c1d42cf\",\n      \"caseEventId\": \"c7b6ee20-793a-11ea-9144-710703689c79\",\n      \"eventFormDefinitionId\": \"mnh-sociodemographic-form\",\n      \"formResponseId\": \"c7b6ee24-793a-11ea-9144-710703689c79\"\n    }\n  ]\n}\n
    Any other documents related to a case save only form data and a small amount of meta-data.

    "},{"location":"editor/project_managment/case-module/case-data-model/#how-relationships-are-mapped-in-an-eventform","title":"How relationships are mapped in an EventForm","text":"

    Let's first look at the Case hierarchy: A Case has a collection of CaseEvents.

    A CaseEvent has a collection of EventForms, which manage the relationship between : - the CaseEvent (stored as _id in the CaseEvent and caseId in the CaseEvent eventForms array ) - Participant (stored in the CaseEvent's particiaptns array and also linked via participantId in the CaseEvent's eventForms's array) - CaseEvent (stored as caseEventId in the CaseEvent's events array) - TangyFormResponse (stored as formResponseId and available externally in a separate document)

    class EventForm {\n  id:string;\n  participantId:string\n  complete:boolean = false\n  caseId:string; \n  caseEventId:string;\n  eventFormDefinitionId:string;\n  formResponseId:string;\n  data?:any;\n  constructor() {\n\n  }\n}\n

    The formResponseId links to a TangyFormResponse, which contains the data filled out in a form.

    "},{"location":"editor/project_managment/case-module/case-management-group/","title":"Case Management Group","text":"

    Case Management allows us to define Case Definitions for different purposes such as following a participant in a drug trial over the course of many events, where each event may require many forms to be filled out. In order to create and find cases, you will need to configure the \"case-home\" as the \"homeUrl\" value in app-config.json.

    "},{"location":"editor/project_managment/case-module/case-management-group/#configuring-cases","title":"Configuring Cases","text":"

    Case Management allows us to define Case Definitions for different purposes such as following a participant in a drug trial over the course of many events, where each event may require many forms to be filled out.

    To configure cases, there are four files to modify.

    First add a reference to the new Case Definition in the case-definitions.json. Here is an example of a case-definitions.json file that references two Case Definitions.

    File: case-definitions.json

    [\n  {\n    \"id\": \"case-definition-1\",\n    \"name\": \"Case Definition 1\",\n    \"src\": \"./assets/case-definition-1.json\"\n  },\n  {\n    \"id\": \"case-definition-2\",\n    \"name\": \"Case Definition 2\",\n    \"src\": \"./assets/case-definition-2.json\"\n  }\n]\n

    Then create the corresponding Case Definition file...

    File: case-definition-1.json

    {\n  \"id\": \"case-definition-1\",\n  \"formId\": \"case-definition-1-manifest\",\n  \"name\": \"Case Definition 1\",\n  \"description\": \"Description...\",\n  \"startFormOnOpen\": {\n    \"eventId\": \"event-definition-1\",\n    \"eventFormId\": \"event-form-1\"\n  },\n  \"eventDefinitions\": [\n   {\n      \"id\": \"event-definition-1\",\n      \"name\": \"Event Definition 1\",\n      \"description\": \"Description...\",\n      \"repeatable\": false,\n      \"required\": true,\n      \"eventFormDefinitions\": [\n        {\n          \"id\": \"event-form-definition-1\",\n          \"formId\": \"form-1\",\n          \"name\": \"Form 1\",\n          \"required\": true,\n          \"repeatable\": false\n        }\n      ]\n    }\n  ]\n}\n

    "},{"location":"editor/project_managment/case-module/case-management-group/#case-definition-templates","title":"Case Definition Templates","text":"

    As a Data Collector uses the Client App, they navigate a Case's hierarchy of Events and Forms. Almost every piece of information they see can be overriden to display custom variables and logic by using the Case Definition's templates. This section describes the templates available and what variables are available. Note that all templates are evaluated as Javascript Template Literals. There are many good tutorials online about how to use Javascipt Template Literals, here are a couple of Javascript Template Literals examples that we reference often for things like doing conditionals and loops.

    "},{"location":"editor/project_managment/case-module/case-management-group/#schedule","title":"Schedule","text":"

    templateScheduleListItemIcon default:

    \"templateScheduleListItemIcon\": \"${caseEvent.status === 'CASE_EVENT_STATUS_COMPLETED' ? 'event_note' : 'event_available'}\"\n

    templateScheduleListItemPrimary default:

    \"templateScheduleListItemPrimary\": \"<span>${caseEventDefinition.name}</span> in Case ${caseService.case._id.substr(0,5)}\"\n

    templateScheduleListItemSecondary default:

    \"templateScheduleListItemSecondary\": \"<span>${caseInstance.label}</span>\"\n

    Variables available: - caseService: CaseService - caseDefinition: CaseDefinition - caseEventDefinition: CaseEventDefinition - caseInstance: Case - caseEvent: CaseEvent

    "},{"location":"editor/project_managment/case-module/case-management-group/#debugging-case-definition-templates","title":"Debugging Case Definition Templates","text":""},{"location":"editor/project_managment/case-module/case-management-group/#configuring-search","title":"Configuring search","text":"

    The case references a Form in the formId property of the Case Definition. Make sure there is a form with that corresponding Form ID listed in forms.json with additional configuration for search.

    File: forms.json

    [\n  {\n     \"id\" : \"case-definition-1-manifest\",\n     \"type\" : \"case\",\n     \"title\" : \"Case Definition 1 Manifest\",\n     \"description\" : \"Description...\",\n     \"listed\" : true,\n     \"src\" : \"./assets/case-definition-1-manifest/form.html\",\n     \"searchSettings\" : {\n        \"primaryTemplate\" : \"Participant ID: ${searchDoc.variables.participant_id}\",\n        \"shouldIndex\" : true,\n        \"secondaryTemplate\" : \"Enrollment Date: ${searchDoc.variables.enrollment_date}, Case ID: ${searchDoc._id}\",\n        \"variablesToIndex\" : [\n           \"participant_id\",\n           \"enrollment_date\"\n        ]\n     }\n  }\n]\n

    "},{"location":"editor/project_managment/case-module/case-management-group/#configuring-two-way-sync","title":"Configuring two-way sync","text":"

    Because you may need to share cases across devices, configuring two-way sync may be necessary. See the Two-way Sync Documentation for more details. Note that you sync Form Responses, and it's the IDs of that you'll want to sync in the \"formId\" of the Case Definition in order to sync cases.

    "},{"location":"editor/project_managment/case-module/case-management-group/#configuring-the-schedule","title":"Configuring the Schedule","text":"

    One of the two tabs that Data Collectors see when they log into Tangerine is a \"Schedule\" tab. This schedule will show Case Event's on days where they are have an estimated day, scheduled day, and/or occurred on day. You can set these three dates on an event using the following APIs.

    caseService.setEventEstimatedDay(idOfEvent, timeInUnixMilliseconds)\ncaseService.setEventOccurredOn(idOfEvent, timeInUnixMilliseconds)\ncaseService.setEventScheduledDay(idOfEvent, timeInUnixMilliseconds)\n
    "},{"location":"editor/project_managment/case-module/case-module-cookbook/","title":"Case Module Cookbook","text":""},{"location":"editor/project_managment/case-module/case-module-cookbook/#get-data-from-participant-related-to-current-event-form","title":"Get data from participant related to current Event Form","text":"

    In the following example, from an on-change hook or on-open, we can look up the corresponding participant for the current form, then look the age variable that has been previously set on that participant.

    const currentEventId = window.location.hash.split('/')[5]\nconst currentFormId = window.location.hash.split('/')[6]\nconst participantId = caseService\n  .case\n  .events\n  .find(event => event.id === currentEventId)\n  .eventForms\n  .find(eventForm => eventForm.id === currentFormId)\n  .participantId\nconst age = caseService.getParticipantData(participantId, 'age')\n

    "},{"location":"system-administrator/","title":"System Administrator Guide","text":""},{"location":"system-administrator/#sync-protocols","title":"Sync Protocols","text":"

    sync-protocol-1 doc sync-protocol-2 doc

    "},{"location":"system-administrator/#configuration","title":"Configuration","text":"

    Configuration notes

    "},{"location":"system-administrator/automating-upgrades-of-tangerine/","title":"Automating Upgrades of Tangerine","text":"

    If you have a particularly complex upgrade of Tangerine that involves changing configurations, writing your own upgrade script and testing that on a QA server can be a way to ensure smooth upgrades when you go to production. Below you will find various tips and tricks we've discovered along the way of writing our own upgrade scripts.

    "},{"location":"system-administrator/automating-upgrades-of-tangerine/#update-a-groups-configuration-in-the-groups-database","title":"Update a group's configuration in the groups database","text":"

    In this example, we modify the xyz group's configuration to implement some csvReplacementCharacters. First we install into the container the jq utility for modifying JSON on the command line, then we modify the group's config doc in the second command. To use this example, replace the two xyz instances with the group's ID you want to modify.

    docker exec -it tangerine apt install -y jq\ndocker exec -it tangerine bash -c 'curl -s $T_COUCHDB_ENDPOINT/groups/xyz | jq \".csvReplacementCharacters = [[\\\",\\\",\\\"|\\\"],[\\\"\\n\\\",\\\"___\\\"]]\" | curl -s -T - -H \"Content-Type: application/json\" -X PUT $T_COUCHDB_ENDPOINT/groups/xyz'\n
    "},{"location":"system-administrator/automating-upgrades-of-tangerine/#updating-a-groups-content-repository","title":"Updating a group's content repository","text":"

    Given the upgrade of Tangerine, there may be associated content changes required. Set up an deploy key in your content repository on github and modify the following script to suit your needs.

    cd /path-to-tangerine/tangerine/data/groups/some-group-id/\nGIT_SSH_COMMAND='ssh -i /root/.ssh/id_github.pub' git fetch origin\ngit checkout v2.0.0\n
    "},{"location":"system-administrator/automating-upgrades-of-tangerine/#update-a-groups-app-configjson","title":"Update a group's app-config.json","text":"

    Best practice for ensuring that configuration in client/app-config.json is maintained over time is to first update the configuration you want to modify in your group's client/app-config.defaults.json and then retemplate the defaults to the client/app-config.json file. To use the following example, replace instances of group-xyz with the relevant Group ID and GROUP XYZ with the relevant Group Name.

    docker exec -it tangerine apt install -y jq\ndocker exec -it tangerine bash -c 'cat /tangerine/groups/group-xyz/client/app-config.defaults.json | jq \".serverUrl = \\\"$T_PROTOCOL://$T_HOST_NAME\\\"\" | jq \".groupId = \\\"group-xyz\\\"\" | jq \".groupName = \\\"Group XYZ\\\"\" > /tangerine/groups/group-xyz/client/app-config.json'\n
    "},{"location":"system-administrator/configuration-notes/","title":"Configuration Notes","text":"

    This page documents various configuration options available in Tangerine

    "},{"location":"system-administrator/configuration-notes/#new-group-configuration","title":"New Group configuration","text":"

    Modify ./content-sets/content-sets-example.json and rename to content-sets.json to enable the content-set dropdown. PR 3275: https://github.com/Tangerine-Community/Tangerine/pull/3275

    "},{"location":"system-administrator/configuring-aws-cloudwatch-for-logs/","title":"Configuring AWS Cloudwatch","text":""},{"location":"system-administrator/configuring-aws-cloudwatch-for-logs/#create-cloudwatchagentserverrole-role","title":"Create CloudWatchAgentServerRole role","text":"

    https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/create-iam-roles-for-cloudwatch-agent.html

    Follow the directions in the section marked \"To create the IAM role necessary for each server to run the CloudWatch agent\" which are summarized here:

    "},{"location":"system-administrator/configuring-aws-cloudwatch-for-logs/#to-create-the-iam-role-necessary-for-each-server-to-run-the-cloudwatch-agent","title":"To create the IAM role necessary for each server to run the CloudWatch agent","text":"
    • Sign in to the AWS Management Console and open the IAM console at https://console.aws.amazon.com/iam/.
    • In the navigation pane, choose Roles and then choose Create role. Under Select type of trusted entity, choose AWS service.
    • Immediately under Common use cases, choose EC2,and then choose Next: Permissions.
    • In the list of policies, use the search box to find the CloudWatchAgentServerPolicy and select its checkbox.
    • To use Systems Manager to install or configure the CloudWatch agent, select the box next to AmazonSSMManagedInstanceCore. This AWS managed policy enables an instance to use Systems Manager service core functionality. If necessary, use the search box to find the policy. This policy isn't necessary if you start and configure the agent only through the command line.
    • Choose Next: Tags. (If needed)
    • Choose Next: Review. For Role name, enter a name for your new role, such as CloudWatchAgentServerRole or another name that you prefer.
    • Confirm that CloudWatchAgentServerPolicy and optionally AmazonSSMManagedInstanceCore appear next to Policies.
    • Choose Create role. The role is now created.

    Diregard the directions marked \"To create the IAM role for an administrator to write to Parameter Store\"

    "},{"location":"system-administrator/configuring-aws-cloudwatch-for-logs/#attach-iam-role-cloudwatchagentserverrole-to-instance","title":"Attach IAM role CloudWatchAgentServerRole to instance","text":"

    https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/iam-roles-for-amazon-ec2.html#attach-iam-role

    To attach an IAM role to an instance - Open the Amazon EC2 console at https://console.aws.amazon.com/ec2/. - In the navigation pane, choose Instances. - Select the instance, choose Actions, Security, Modify IAM role. - Select the IAM role to attach to your instance, and choose Save.

    "},{"location":"system-administrator/configuring-aws-cloudwatch-for-logs/#installing-or-updating-ssm-agent","title":"Installing or updating SSM Agent","text":"

    https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-install-ssm-agent.html

    Agent is already installed in Ubuntu Server 16.04, 18.04, and 20.04 - To check status sudo systemctl status snap.amazon-ssm-agent.amazon-ssm-agent.service - To start agent sudo snap start amazon-ssm-agent

    "},{"location":"system-administrator/configuring-aws-cloudwatch-for-logs/#download-and-configure-the-cloudwatch-agent","title":"Download and configure the CloudWatch agent","text":"

    https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/download-CloudWatch-Agent-on-EC2-Instance-SSM-first.html

    • To download the CloudWatch agent using Systems Manager, Open the Systems Manager console at https://console.aws.amazon.com/systems-manager/.
    • In the navigation pane, choose Run Command. -or- If the AWS Systems Manager home page opens, scroll down and choose Explore Run Command.
    • Choose Run command. In the Command document list, choose AWS-ConfigureAWSPackage.
    • In the Targets area, choose the instance to install the CloudWatch agent on. If you don't see a specific instance, you probably don't have the correct Policy associated with your instance
    • In the Action list, choose Install. In the Name field, enter AmazonCloudWatchAgent.
    • Keep Version set to latest to install the latest version of the agent.
    • Choose Run.
    • Optionally, in the Targets and outputs areas, select the button next to an instance name and choose View output. Systems Manager should show that the agent was successfully installed.
    "},{"location":"system-administrator/configuring-aws-cloudwatch-for-logs/#configure-the-cloudwatch-agent","title":"Configure the CloudWatch agent","text":"

    The agent configuration file is a JSON file that specifies the metrics and logs that the agent is to collect, including custom metrics. You can create it by using the wizard or by creating it yourself from scratch. You could also use the wizard to initially create the configuration file and then modify it manually.

    Create and modify the agent configuration file - You can configure the agent on your AWS EC2 instance. Run the following command: /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-config-wizard - When the wizard runs, choose to install statsD and CollectD (default is yes), the 'Advanced' level of metrics. - The defaults are usually fine, but decline when it asks \"Do you want to monitor any log files?\" and also \"Do you want to store the config in the SSM parameter store?\" - The configuration file config.json is stored in /opt/aws/amazon-cloudwatch-agent/bin/config.json

    https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/create-cloudwatch-agent-configuration-file.html

    "},{"location":"system-administrator/configuring-aws-cloudwatch-for-logs/#to-restart-the-cloudwatch-agent","title":"To restart the CloudWatch agent","text":"

    https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/install-CloudWatch-Agent-on-EC2-Instance-fleet.html#start-CloudWatch-Agent-EC2-fleet

    sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c file:/opt/aws/amazon-cloudwatch-agent/bin/config.json

    If you see an error about collectd, install it: apt install collectd

    https://github.com/awsdocs/amazon-cloudwatch-user-guide/issues/54#issuecomment-696844909

    "},{"location":"system-administrator/configuring-aws-cloudwatch-for-logs/#modifying-the-dashboard","title":"Modifying the dashboard","text":""},{"location":"system-administrator/configuring-aws-cloudwatch-for-logs/#create-a-cloudwatch-alarm-based-on-a-static-threshold","title":"Create a CloudWatch alarm based on a static threshold","text":"

    https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/ConsoleAlarms.html

    "},{"location":"system-administrator/configuring-aws-cloudwatch-for-logs/#custom-namespaces","title":"Custom namespaces","text":"

    The default namespace for metrics collected by the CloudWatch agent is CWAgent, although you can specify a different namespace when you configure the agent. CWAgent provides useful data such as disk utilization which can be used on a dashboard or for an alert.

    Go to CloudWatch -> Metrics -> All Metrics and scroll to Custom namespaces to see the CWAgent namespace.

    List of data collected by CWAgent here: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/metrics-collected-by-CloudWatch-agent.html

    "},{"location":"system-administrator/configuring-aws-cloudwatch-for-logs/#sns-notifications","title":"SNS Notifications","text":"

    To create a topic: https://eu-west-1.console.aws.amazon.com/sns/v3/home?region=eu-west-1#/topics

    "},{"location":"system-administrator/couchdb/","title":"CouchDB","text":""},{"location":"system-administrator/couchdb/#upgrade","title":"Upgrade","text":"

    If there has been a security update to CouchDB 2.x, all you must do is rerun the start.sh command and the new image for CouchDB will be downloaded and run.

    "},{"location":"system-administrator/couchdb/#change-password","title":"Change password","text":"
    # Shutdown tangerine and couchdb\ndocker stop tangerine couchdb\n# Remove the docker files that cached the password for CouchDB.\nrm -r data/couchdb/local.d\n# Update the T_COUCHDB_USER_ADMIN_PASS variable.\nvim config.sh\n# update the cached password in the reporting worker configuration.\nvim data/reporting-worker-state.json\n# If using the mysql module, update the passwords in each group's mysql state file.\nvim data/mysql/state/<groupId>.ini\n# start.sh will rebuild the couchdb and tangerine containers which will update the password in all necessary places.\n./start.sh <version>\n
    "},{"location":"system-administrator/install-on-aws/","title":"Installing Tangerine on AWS","text":""},{"location":"system-administrator/install-on-aws/#creating-the-aws-instance","title":"Creating the AWS instance","text":"

    Login to AWS and Launch a new instance with Ubuntu 18.04 using a t2.medium server with 4 GiB memory.

    Volume should be larger than the 8GB default. 24GB would be useful, but if you're planning to test different Tangerine images, go for 64GB.

    "},{"location":"system-administrator/install-on-aws/#security","title":"Security","text":"

    Make sure to assign a security group to your instance that allows you to access port 80 via a web browser and port 22 via ssh.

    • HTTP: TCP 80 0.0.0.0/0
    • SSH TCP 22 0.0.0.0/0
    "},{"location":"system-administrator/install-on-aws/#set-up-ssl","title":"Set up SSL","text":"

    Prerequisites:

    • An SSL Certificate. If you don't yet have one, we recommend using AWS's Certificate Manager (found under \"Security, Identity, and Compliance\").

    Create and Configure an Elastic Load Balancer (ELB):

    • Go to EC2, click \"Load Balancers\" in the left column, click \"Create Load Balancer\", and then select \"Classic Load Balancer\".
    • Step 1: Define Load Balancer
    • Set a Load Balancer name to what you want.
    • Set \"Load Balancer Protocol\" on the left most column to \"HTTPS\".
    • Set \"Instance Protocol\" in the third column to \"HTTP\".
    • Click \"Add\".
    • In the new row set \"Load Balancer Protocol\" to \"HTTP\" and \"Instance Protocol\" to \"HTTP\".
    • Click \"Next\".
    • Step 2: Assign Security Groups
    • Select \"Create a new security group\".
    • Set rules for both HTTP and HTTPS. If you only do HTTPS, anyone who goes to http://yourdomain.com will get an Access Denied message. Allow them to access the site with HTTP, the software will forward them to HTTPS automatically.
    • Click \"Next\".
    • Step 3: Configure Security Settings
    • If you have an SSL certificate, you can upload that here. Otherwise select \"Choose an existing certificate from AWS Certificate Manager (ACM)\".
    • If you have not requested a certificate for your domain yet, you will need to click \"Request a new certificate from ACM\" and follow those instructions before proceeding.
    • Step 4: Configure Health Check
    • Ping Protocol: HTTP
    • Ping Port: 80
    • Ping Path: /app/tangerine/index.html
    • Response Timeout: 5 seconds
    • Interval: 10 seconds
    • Unhealthy threshold: 10
    • Healthy threshold: 2
    • Step 5: Add EC2 Instances
    • Select the EC2 instance running Tangerine.
    • Step 6: Add Tags
    • No tags are required for Tangerine.
    • Step 7: Review
    • If everything looks good, go ahead and create it!
    • Now proceed to your Load Balancers dashboard, click on your load balancer, click on the Instances tab, and now wait for your EC2 instance to be listed as \"InService\".
    • Configure your domain's DNS to point to this load balancer by clicking on the load balancer's Description tab and using the \"DNS name\" given to configure your Domain's DNS.
    "},{"location":"system-administrator/install-on-aws/#ssh-login-to-server","title":"SSH Login to Server","text":"

    Once your server is created, login with your key:

     ssh -i ~/.ssh/iyour_key -l ubuntu\u00a0<your EC2 instance's IP address>\n

    Now you may continue to step 2 in the installation instructions of Tangerine's README.md, then pick back up here.

    "},{"location":"system-administrator/install-on-aws/#configure-logs","title":"Configure Logs","text":"

    Send logs to AWS CloudWatch for building alarms and saving disk space.

    1. Create IAM user with programattic access and AWSAppSyncPushToCloudWatchLogs policy. Keep open credentials screen for reference.
    2. Install aws-cli with sudo apt-get install awscli.
    3. aws configure and give the credentials for the IAM user.
    4. Go to AWS Console -> IAM -> Access Management -> Roles -> Create Role, create a role called aws-cloudwatch-logs with an attached policy of AWSOpsWorksCloudWatchLogs.
    5. Go to AWS Console -> EC2 -> Instances -> <select your instance> -> Actions -> Security -> Modify IAM role and add the aws-cloudwatch-logs role to the EC2 instance.
    6. Go to AWS Console -> CloudWatch -> Logs -> Actions -> Create log group.
    7. Create the Log Group named after the instance name (ie. example-v3).
    8. Write the configuration to /etc/docker/daemon.json. Change awslogs-region to the \"less specific\" region name (eu-central-1 as opposed to eu-central-1b) and replace example-v3 in tag and awslogs-group to reflect the EC2 instance name.
    9. Then run systemctl restart docker. If containers were already running, you may need to recreate them for settings to take hold. For Tangerine, that just means running ./start.sh again.
    10. After setting up Tangerine, navigate in your browser to AWS Console -> CloudWatch -> Logs and select your instance's log group. There you will find two streams, one for the tangerine container the other for couchdb container using the \"tag\" pattern you configured in daemon.json.
    {\n    \"log-driver\": \"awslogs\",\n    \"log-opts\": {\n        \"awslogs-region\": \"eu-central-1\",\n        \"awslogs-group\": \"example-\",\n        \"tag\": \"example-{{.Name}}\"\n    }\n}\n
    "},{"location":"system-administrator/install-on-aws/#configure-alarm","title":"Configure Alarm","text":"

    With Docker logs being sent to AWS CloudWatch, you can configure an alarm to detect if Tangerine is down. The following directions explain how to send an automated email if a Tangerine heartbeat log message is not heard for 15 minutes.

    • Navigate in your browser to AWS Console -> CloudWatch -> Logs.
    • Open your server's log group.
    • Open the stream for Tangerine. If your tag pattern in /etc/docker/daemon.json is example-{{.Name}}, then your stream name will be example-tangerine.
    • In the Filter events text box, type heartbeat and press enter. This will filter the logs to all heartbeat messages.
    • With the filter still applied, click the \"Create Metric Filter\" button.
    • Fill out \"Metric\" form as follows:
      • Filter name: heartbeat
      • Filter pattern: heartbeat
      • Metric namespace: tangerine
      • Metric name: heartbeat
      • Metric value: 1
      • Default value: 0
      • Unit: leave blank
    • Navigate to your log group and click the \"Metric filter\" tab, click the checkbox in your Metric's box, then click \"Create alarm\" button.
    • Fill out the form:
      • Metric name: heartbeat
      • Statistic: Sum
      • Period: 15 minutes
      • Threshold type: Static
      • Whenever heartbeat is...: Lower
      • than...: 1
      • Additional configuration
      • Datapoints to alarm: 1 out of 1
      • Missing data treatment: Treat missing data as bad (breaching the threshold)
    • Fill out \"Notification\" form as follows:
      • Alarm state trigger: In alarm
      • Select an SNS topic: Create new topic
      • Create a new topic...: <server name>-tangerine-heartbeat
      • Email endpoints that will receive the notification...: <your email address>
    • Click \"Create Topic\" button, then \"Next\" button.
    • Fill out \"Name and description\" form as follows then click \"Next\" button:
      • Alarm name: <server name>-tangerine-heartbeat
    • Now on the \"Preview and create\" screen, click \"Create Alarm\"
    • Check your email to confirm subscription to SNS Topic.
    "},{"location":"system-administrator/integrate-group-with-github/","title":"Integrate a group's content with a repository on Github","text":""},{"location":"system-administrator/integrate-group-with-github/#step-1","title":"Step 1","text":"

    Create a group in the Editor. Note the ID in the URL starting with group-.

    "},{"location":"system-administrator/integrate-group-with-github/#step-2","title":"Step 2","text":"

    Create a repository on Github for your group's content.

    "},{"location":"system-administrator/integrate-group-with-github/#step-3","title":"Step 3","text":"

    SSH into the server and create a \"deploy key\" the server will use to authenticate to Github with. When running ssh-keygen, do not password protect the key file. When it prompts you for a password, just hit enter.

    ssh <your server>\nsudo su\nssh-keygen -t rsa -b 4096 -C \"root@domain_of_server\"\ncat /root/.ssh/id_rsa.pub\n

    Change the key permissions if necessary.

    Copy the key contents that we just \"cat'ed\" to the screen. Then go to your Repository on Github and click on Settings -> Deploy keys -> Add deploy key and paste that key in the key contents, enable \"Allow write access\" and save.

    "},{"location":"system-administrator/integrate-group-with-github/#step-4","title":"Step 4","text":"

    Now we push our group's initial content to our github repository with the following commands...

    cd tangerine/data/client/content/groups/<group id>\ngit init\ngit add .\ngit commit -m \"first commit\"\ngit remote add origin <githut repository SSH URL>\ngit push origin master\n

    You should now see on your Github Repository code page a list of files pushed from the server.

    "},{"location":"system-administrator/integrate-group-with-github/#step-5","title":"Step 5","text":"

    We'll now configure your server to periodically pull content changes from Github.

    ssh <your server>\nsudo su\ncrontab -e\n
    Enter the following onto a new line. Replace <group id> with appropriate Group ID.
    * * * * * cd /home/ubuntu/tangerine/data/client/content/groups/<group id> && GIT_SSH_COMMAND='ssh -i /root/.ssh/id_rsa' git pull origin master && git add . && git commit -m 'auto-commit' && GIT_SSH_COMMAND='ssh -i /root/.ssh/id_rsa' git push origin master\n

    "},{"location":"system-administrator/managing-data-conflicts/","title":"Managing Data Conflicts","text":"

    When using Sync Protocol 2 we can sync data down to Devices. Because of this it is possible for two Devices to edit the same data between syncs. This causes a \"Data Conflict\". When a Device is syncing and a conflict is detected.

    To manage Data Conflicts you will need to know the IP Address of your server and have the CouchDB Admin credentials found in config.sh.

    "},{"location":"system-administrator/managing-data-conflicts/#step-1-find-documents-with-conflicts","title":"Step 1: Find Documents with Conflicts","text":"

    Go to <serverIpAddress>:5984/_utils/#database/<groupId>/_design/shared_conflicts/_view/shared_conflicts. This is a list of Documents with conflicts. Click the first one in the list which will open the Document with conflicts. Note this doc has link in the second bar from the top on the right with label of \"Conflicts\".

    "},{"location":"system-administrator/managing-data-conflicts/#step-2-merge-conflict-revisions-contents-into-doc","title":"Step 2: Merge Conflict Revisions contents into Doc","text":"

    With the Document open, in Fauxton, copy the URL and open another window with the same URL side by side. On the window on the left, click the \"Conflicts\" link in the top right of that window. Note the \"Conflicting Revisions\" drop down in the Conflicts Browser. These Conflicting Revisions are the contents of Documents that were the \"losing revision\" when in a conflict. Selecting a \"Conflict Rev\" in the Conlict Revisions dropdown will result in the difference between the Conflicting Revision's contents and the current revision's contents. JSON highlighted in green belongs to the Conflict Rev.

    Cycle through each of the Conflict Revs migrating JSON highlighted in green over to the current Document edit view in your browser on the right. When all contents in the Conflict Revs have been migrated (AKA \"merged\") into the current Document, save the current doc with the changes and proceed to the next step.

    "},{"location":"system-administrator/managing-data-conflicts/#step-3-archive-conflict-revisions","title":"Step 3: Archive Conflict Revisions","text":"

    Use the pouchdb-couchdb-archive-conflicts CLI tool to archive the conflicts in the Document. This will result in removing the Document for the conlist list in step 1 and save a copy of each conflict revision into `<serverIpAddress>:5984/_utils/#database/<groupId>-conflict-revs/.

    Install the tool. Requires Node.js (https://nodejs.org/).

    npm install -g pouchdb-couchdb-archive-conflicts\n

    archive-conflicts http://<username>:<password>@<serverIp>:5984/<groupId> <docId> \n

    Now return to Step 1 and pick the next Document in the list.

    "},{"location":"system-administrator/managing-data-conflicts/#reviewing-archived-conflict-revisions","title":"Reviewing Archived Conflict Revisions","text":"

    To review a Documents archived conflict revisions, add a byConflictDocId view to the database.

    function (doc) {\n  emit(doc.conflictDocId, doc.conflictRevId);\n}\n

    When viewing this view you can then click \"Options\" and enter the follwing under \"By Keys\" [\"<docId>\"].

    "},{"location":"system-administrator/managing-legacy-conflict-issues/","title":"Managing Legacy Conflict Issues","text":"

    The following describes a process for managing data conflicts using a deprecated feature known as \"Auto-merge\" and \"Conflict Issues\".

    When using Sync Protocol 2 we can sync data down to Devices. Because of this it is possible for two Devices to edit the same data between syncs. This causes a \"Data Conflict\". When a Device is syncing and a conflict is detected, the Tangerine software will try to resolve that conflict by merging the two versions. When Tangerine does this, it creates a \"Conflict Issue\" which stores the two versions and the merged version for safe keeping (in Issue this is refered to as A, B, and Merged). These are important to review in the Issue queue to ensure that the merge is satisfactory. Sometimes a merge will not be possible and the data will be left in a Conflict state according to CouchDB's definition of Conflict.

    To manage Data Conflicts you will need to know the IP Address of your server and CouchDB Admin credentials found in config.sh.

    "},{"location":"system-administrator/managing-legacy-conflict-issues/#monitor-how-many-conflict-issues-there-are-by-document-id","title":"Monitor how many Conflict Issues there are by Document ID","text":"
    1. Go to <serverIpAddress>:5984/_utils/#/database/<groupId>/_design/issuesOfTypeConflictByConflictingDocTypeAndConflictingDocId/_view/issuesOfTypeConflictByConflictingDocTypeAndConflictingDocId
    2. Click wrench for view on left hand column, make sure \"Reduce: Count\" is enabled on the view.
    3. Click \"Options\", check \"Reduce\" and then set \"Group Level\" of 2.
    "},{"location":"system-administrator/managing-legacy-conflict-issues/#get-a-list-of-all-conflict-issues-in-a-group","title":"Get a list of all conflict issues in a group","text":"

    <serverIpAddress>/<groupId>/_design/issuesOfTypeConflictByConflictingDocTypeAndConflictingDocId/_view/issuesOfTypeConflictByConflictingDocTypeAndConflictingDocId?reduce=false

    "},{"location":"system-administrator/managing-legacy-conflict-issues/#check-a-single-document-for-conflicts","title":"Check a single document for conflicts","text":""},{"location":"system-administrator/managing-legacy-conflict-issues/#get-a-list-of-all-conflict-issues-for-a-document","title":"Get a list of all conflict issues for a document","text":"

    If you know the document id for a case, you can get a list of its conflict issues.

    In the following example, the first parameter in the key is set to 'case' to signify that it is a case document (type:'case'); however, in the future conflicts may also betype:'response'.

    Replace <docId> and enter the following into your browser:

    <serverIpAddress>/<groupId>/_design/issuesOfTypeConflictByConflictingDocTypeAndConflictingDocId/_view/issuesOfTypeConflictByConflictingDocTypeAndConflictingDocId?reduce=false&key=[\"case\",\"<docId>\"]

    "},{"location":"system-administrator/managing-legacy-conflict-issues/#get-the-number-of-conflicts-in-a-document","title":"Get the number of conflicts in a document","text":"

    If you know the document id for a case, replace <docId> and enter the following into your browser:

    <serverIpAddress>/<groupId>/_design/issuesOfTypeConflictByConflictingDocTypeAndConflictingDocId/_view/issuesOfTypeConflictByConflictingDocTypeAndConflictingDocId?reduce=true&group_level=2&key=[\"case\",\"<docId>\"]

    "},{"location":"system-administrator/managing-legacy-conflict-issues/#monitor-active-database-conflicts-in-couchdb","title":"Monitor active Database Conflicts in CouchDB","text":"

    Go to <serverIpAddress>:5984/_utils/#database/<groupId>/_design/shared_conflicts/_view/shared_conflicts.

    To view the number of conflicts per document, click the table tab.

    To resolve a conflict, click on the row to open the Doc in conflict. Then click the \"Conflicts\" tab to resolve the Conflict.

    "},{"location":"system-administrator/managing-legacy-conflict-issues/#viewing-the-history-of-documents-in-the-database","title":"Viewing the history of Documents in the database","text":"

    Sometimes it helps to look back at the history of a Case. When a Case is open in Tangerine Editor, you can run the following in the Chrome Devtools Console.

    await T.case.getCaseHistory()\n

    To look at the full picture that CouchDB server is aware of, use the following URL structure...

    <serverIpAddress>:5984/<groupId>/<docId>?revs_info=true\n
    You will find the response contains a _revs_info property with an array of revisions. If a revisions has \"status\" of \"unavailable\", then that revision is on the Device it came from. You can find out which Device likely has that revision by finding the next available revision, getting the doc at that revision, and inspecting the modifiedByDeviceId property. This is the version of the doc that was uploaded to the server.

    <serverIpAddress>:5984/<groupId>/<docId>?rev=<revId>\n
    "},{"location":"system-administrator/mysql-js/","title":"MySQL-JS Module","text":""},{"location":"system-administrator/mysql-js/#enabling-mysql-js-module","title":"Enabling MySQL-JS module","text":"

    Important! If reenabling the mysql module, remove the mysql folder: rm -r data/mysql

    "},{"location":"system-administrator/mysql-js/#step-1","title":"Step 1","text":"

    Ensure the variables from the MySQL section are in your customized config.sh file.

    "},{"location":"system-administrator/mysql-js/#mysql","title":"Mysql","text":"
    • T_MYSQL_CONTAINER_NAME=\"mysql\" # Either the name of the mysql Docker container or the hostname of a mysql server or AWS RDS MySQL instance.
    • T_MYSQL_USER=\"admin\" # Username for mysql credentials
    • T_MYSQL_PASSWORD=\"password\" # Password for mysql credentials
    • T_USE_MYSQL_CONTAINER=\"true\" # If using a Docker container, set to true. This will automatically start a mysql container when using a Tangerine launch script.
    "},{"location":"system-administrator/mysql-js/#step-2","title":"Step 2","text":"

    Ensure the T_MYSQL_PASSWORD variable is set to a sufficiently secure string. Failure to properly secure this password will without a doubt result in ransomware bots hacking your database.

    "},{"location":"system-administrator/mysql-js/#step-3","title":"Step 3","text":"

    Add the mysql module to T_MODULES_ENABLED in config.sh.

    For example:

    T_MODULES=\"['csv','mysql-js']\"\n

    "},{"location":"system-administrator/mysql-js/#step-4","title":"Step 4","text":"

    Run the start script to load in new configuration. Do this even if your server is already running. Note that restarting the container will not work, we have to run ./start.sh to recreate the container with the new configuration.

    ./start.sh <version>\n

    Note: Upgrading an older version of Tangerine may require running docker exec tangerine push-all-groups-views after to enable indexes used for mysql

    "},{"location":"system-administrator/mysql-js/#step-5","title":"Step 5","text":"

    Clear reporting cache to start generating a MySQL database for each group.

    docker exec tangerine reporting-cache-clear\n

    You can check in on the progress of generating the mysql database using the mysql-report command. (Warning The mysql-report command creates a heavy workload to an instance so do not use it when mysql is trying to process a lot of data from couchdb. See the \"Troubleshooting\" section below.) It will return for each kind of case data and form, how many records are in the source database vs. how many have made it over to mysql. Note that if your system is under heavy load during the processing of this, this command may stress it out even more so it may be best to wait until you see a load of less than one using a tool like top or htop.

    docker exec -it tangerine bash \nmysql-report <groupId> | json_pp\n
    "},{"location":"system-administrator/mysql-js/#step-6","title":"Step 6","text":"

    In the reboot instruction in crontab that to starts Tangerine on reboot, add mysql container to the containers that start before tangerine and increase the sleep command to 60 seconds. Failure to implement this will result in tangerine failing to start on reboot.

    @reboot docker start couchdb mysql && sleep 60 && docker start tangerine\n

    Also add a cron job to run mysql-report at 1 a.m every day - this will keep the mysql indexes current.

    # Run mysql-report at 1 a.m every day:\n# 0 1 * * * docker exec tangerine mysql-report group-479f455e-b1bd-481b-8bd7-0d985a07431c\n
    "},{"location":"system-administrator/mysql-js/#step-7","title":"Step 7","text":"

    The most basic way to access MySQL would be to use the MySQL CLI.

    docker exec -it tangerine bash\nmysql -u\"$T_MYSQL_USER\" -p\"$T_MYSQL_PASSWORD\" -hmysql\n

    On the mysql command line, list the available databases using show databases;. Note how the database names are similar to the Group ID's these correspond with except with dashes removed. For example, if the group ID was group-abc-123, the corresponding MySQL database would be groupabc123. To select a database, type use <database ID>; then show tables; to list out the available tables.

    "},{"location":"system-administrator/mysql-js/#step-8","title":"Step 8","text":"

    To set up remote encrypted connections to mysql, three options:

    1. TLS: In the tangerine/data/mysql/databases folder you will find files ca.pem, client-cert.pem, and client-key.pem. Distribute those files to your MySQL users so they may connnect to your server's IP addres port 3306 using these certificates. For example, mysql -u admin -p\"you-mysql-password\" --ssl-ca=ca.pem --ssl-cert=client-cert.pem --ssl-key=client-key.pem.
    2. SSH: For each person using MySQL, they will need SSH access to the server. When granted, they may use tunneling of mysql port 3306 over SSH to access mysql at 127.0.0.1:3306. For example, to set up an SSH port forwarding on Mac or Linux, run ssh -L 3306:your-server:3306 your-server.
    3. VPN: If you connect to MySQL via the IP address of the server, using a VPN will ensure that communication with MySQL is encrypted. Note however that the traffic will be visible to those also on your VPN so make sure it's a trusted VPN only used by those who have permission to access the data.
    "},{"location":"system-administrator/mysql-js/#resetting-mysql-databases","title":"Resetting MySQL databases","text":"

    If you need to reset the mysql database, do the following: - stop the mysql docker instance: docker stop mysql - delete ./data/mysql - remove 'mysql-js' from T_MODULES - Run ./start.sh or ./develop.sh. This will remove the mysql-js module from enabledModules in the app couch database's modules doc. See \"Disabling modules: mysql-js\" in the console to confirm. - add 'mysql-js' to T_MODULES - this will init the mysql databases. - Run ./start.sh or ./develop.sh. This will add the mysql-js module from enabledModules in the app couch database's modules doc and create the databases. See \"Enabling modules: mysql-js\" in the console to confirm.

    "},{"location":"system-administrator/mysql-js/#configuration","title":"Configuration","text":"
    • You may add configuration options to ./server/src/mysql-js/conf.d/config-file.js.
    • If you are using the mysql container and are having errors with very large forms, the new settings in ./server/src/mysql-js/conf.d/config-file.js should help. You will need to completely rebuild the mysql database. Stop the Tangerine and mysql containers. Delete (or -rename) the ./data/mysql directory. Then restart Tangerine using the ./start.sh or develop.sh script.
    • Important: If you already have a mysql instance running and don't want to rebuild the mysql database, delete the innodb-page-size=64K line from ./server/src/mysql-js/conf.d/config-file.js; otherwise, your mysql instance will not start.
    • If making changes to the innodb-page-size option, you must delete the ./data/mysql directory.
    "},{"location":"system-administrator/mysql-js/#troubleshooting","title":"Troubleshooting","text":""},{"location":"system-administrator/mysql-js/#issue-data-on-the-mysql-db-is-far-behind-the-couchdb","title":"Issue: Data on the Mysql db is far behind the Couchdb.","text":"

    This scenario can happen when replicating data from a Production database on another server instance. Step to triage and resolve this issue:

    1. run docker ps -a to see if the tangerine and couchdb instances are up
    2. Bring back up those instance by using the start.sh script.
    3. Confirm using docker logs -f tangerine that the docker containers are back up and processing data correctly.
    4. If the server must catch up more than a day's worth of documents, use the wedge pre-warm-views at the end of the day to hit all views in the couchdb to pre-warm them (i.e. index those views).
    5. After the indexes have been built, use the mysql-report groupID command to see if the mysql and couchdb databases are caught up.
    "},{"location":"system-administrator/mysql-module/","title":"MySQL Module (Legacy)","text":""},{"location":"system-administrator/mysql-module/#enabling-mysql-module","title":"Enabling MySQL module","text":"

    This module is the legacy module starting with the release of Tangerine v3.26.0. If you are running a version prior to v3.26.0, you should consider switching to the new MySQL-JS Module for improved performance.

    Important! If reenabling the mysql module, remove the mysql folder: rm -r data/mysql

    "},{"location":"system-administrator/mysql-module/#step-1","title":"Step 1","text":"

    Ensure the variables from the MySQL section in config.defaults.sh are in your customized config.sh file.

    "},{"location":"system-administrator/mysql-module/#step-2","title":"Step 2","text":"

    Ensure the T_MYSQL_PASSWORD variable is set to a sufficiently secure string. Failure to properly secure this password will without a doubt result in ransomware bots hacking your database.

    "},{"location":"system-administrator/mysql-module/#step-3","title":"Step 3","text":"

    Add the mysql module to T_MODULES_ENABLED in config.sh.

    For example:

    T_MODULES=\"['csv','mysql']\"\n

    "},{"location":"system-administrator/mysql-module/#step-4","title":"Step 4","text":"

    Run the start script to load in new configuration. Do this even if your server is already running. Note that restarting the container will not work, we have to run ./start.sh to recreate the container with the new configuration.

    ./start.sh <version>\n

    Note: Upgrading an older version of Tangerine may require running docker exec tangerine push-all-groups-views after to enable indexes used for mysql

    "},{"location":"system-administrator/mysql-module/#step-5","title":"Step 5","text":"

    Clear reporting cache to start generating a MySQL database for each group.

    docker exec tangerine reporting-cache-clear\n

    You can check in on the progress of generating the mysql database using the mysql-report command. (Warning The mysql-report command creates a heavy workload to an instance so do not use it when mysql is trying to process a lot of data from couchdb. See the \"Troubleshooting\" section below.) It will return for each kind of case data and form, how many records are in the source database vs. how many have made it over to mysql. Note that if your system is under heavy load during the processing of this, this command may stress it out even more so it may be best to wait until you see a load of less than one using a tool like top or htop.

    docker exec -it tangerine bash \nmysql-report <groupId> | json_pp\n
    "},{"location":"system-administrator/mysql-module/#step-6","title":"Step 6","text":"

    In the reboot instruction in crontab that to starts Tangerine on reboot, add mysql container to the containers that start before tangerine and increase the sleep command to 60 seconds. Failure to implement this will result in tangerine failing to start on reboot.

    @reboot docker start couchdb mysql && sleep 60 && docker start tangerine\n

    Also add a cron job to run mysql-report at 1 a.m every day - this will keep the mysql indexes current.

    # Run mysql-report at 1 a.m every day:\n# 0 1 * * * docker exec tangerine mysql-report group-479f455e-b1bd-481b-8bd7-0d985a07431c\n
    "},{"location":"system-administrator/mysql-module/#step-7","title":"Step 7","text":"

    The most basic way to access MySQL would be to use the MySQL CLI.

    docker exec -it tangerine bash\nmysql -u\"$T_MYSQL_USER\" -p\"$T_MYSQL_PASSWORD\" -hmysql\n

    On the mysql command line, list the available databases using show databases;. Note how the database names are similar to the Group ID's these correspond with except with dashes removed. For example, if the group ID was group-abc-123, the corresponding MySQL database would be groupabc123. To select a database, type use <database ID>; then show tables; to list out the available tables.

    "},{"location":"system-administrator/mysql-module/#step-8","title":"Step 8","text":"

    To set up remote encrypted connections to mysql, three options:

    1. TLS: In the tangerine/data/mysql/databases folder you will find files ca.pem, client-cert.pem, and client-key.pem. Distribute those files to your MySQL users so they may connnect to your server's IP addres port 3306 using these certificates. For example, mysql -u admin -p\"you-mysql-password\" --ssl-ca=ca.pem --ssl-cert=client-cert.pem --ssl-key=client-key.pem.
    2. SSH: For each person using MySQL, they will need SSH access to the server. When granted, they may use tunneling of mysql port 3306 over SSH to access mysql at 127.0.0.1:3306. For example, to set up an SSH port forwarding on Mac or Linux, run ssh -L 3306:your-server:3306 your-server.
    3. VPN: If you connect to MySQL via the IP address of the server, using a VPN will ensure that communication with MySQL is encrypted. Note however that the traffic will be visible to those also on your VPN so make sure it's a trusted VPN only used by those who have permission to access the data.
    "},{"location":"system-administrator/mysql-module/#troubleshooting","title":"Troubleshooting","text":""},{"location":"system-administrator/mysql-module/#issue-data-on-the-mysql-db-is-far-behind-the-couchdb","title":"Issue: Data on the Mysql db is far behind the Couchdb.","text":"

    This scenario can happen when replicating data from a Production database on another server instance. Step to triage and resolve this issue:

    1. run docker ps -a to see if the tangerine and couchdb instances are up
    2. Bring back up those instance by using the start.sh script.
    3. Confirm using docker logs -f tangerine that the docker containers are back up and processing data correctly.
    4. If the server must catch up more than a day's worth of documents, use the wedge pre-warm-views at the end of the day to hit all views in the couchdb to pre-warm them (i.e. index those views).
    5. After the indexes have been built, use the mysql-report groupID command to see if the mysql and couchdb databases are caught up.
    "},{"location":"system-administrator/rescuing-a-full-Tangerine-tablet/","title":"Rescuing a Full Tangerine Tablet","text":"

    If you have a tablet that is no longer able to sync because the disk is full, you can pull files off it and install on a clean tablet.

    This is relevant only to tablets that are runing sqlite/sqlCypher.

    "},{"location":"system-administrator/rescuing-a-full-Tangerine-tablet/#listing-files-in-private-database-dir","title":"Listing files in private database dir","text":"

    Android databases are stored in a defined location. You may list them using the adb shell command.

    adb shell \"run-as org.rti.tangerine ls -lsa -R /data/data/org.rti.tangerine/databases/\"\n

    If the package name of your Tangerine differs, substitute 'org.rti.tangerine' with your package name.

    This command should return something like the following:

    4 -rw-------  1 u0_a647 u0_a647       0 2021-11-04 08:15 shared-user-database\n4 -rw-------  1 u0_a647 u0_a647       0 2021-11-04 08:15 shared-user-database-index\n500 -rw-------  1 u0_a647 u0_a647  507904 2021-11-04 08:08 shared-user-database-mrview-058020864d38b2f7c7203401485e2f6d\n3180 -rw-------  1 u0_a647 u0_a647 3248128 2021-11-04 08:10 shared-user-database-mrview-0b8e5d92430db6d71d1379d7c9862b79\n500 -rw-------  1 u0_a647 u0_a647  507904 2021-11-04 08:06 shared-user-database-mrview-0c1a03125cc747770c5ae321753f4ec2\n3156 -rw-------  1 u0_a647 u0_a647 3223552 2021-11-04 08:10 shared-user-database-mrview-34a25e481c98744e67427811b36567c4\n2900 -rw-------  1 u0_a647 u0_a647 2961408 2021-11-04 08:13 shared-user-database-mrview-3d495638a28b32b444ca7a9452a9ec5e\n2924 -rw-------  1 u0_a647 u0_a647 2985984 2021-11-04 08:12 shared-user-database-mrview-918a54f957639ea4a8d24e16c6d93f50\n500 -rw-------  1 u0_a647 u0_a647  507904 2021-11-04 08:12 shared-user-database-mrview-a3b8826be74302784a7af55681039cac\n744 -rw-------  1 u0_a647 u0_a647  757760 2021-11-04 08:08 shared-user-database-mrview-d121af6c2693286feb260c1843a65996\n1344 -rw-------  1 u0_a647 u0_a647 1372160 2021-11-04 08:09 shared-user-database-mrview-e0db7a0ed91857c44dd6ad77ee5b37f4\n2588 -rw-------  1 u0_a647 u0_a647 2641920 2021-11-04 08:07 shared-user-database-mrview-e4d1230d206245fc83e75ee420b1f31e\n500 -rw-------  1 u0_a647 u0_a647  507904 2021-11-04 08:11 shared-user-database-mrview-fc95555b88f96506de12d279698695f2\n72 -rw-------  1 u0_a647 u0_a647   69632 2021-11-04 08:04 tangerine-variables\n

    In this example, the tablet was running with the \"encryptionPlugin\":\"CryptoPouch\" configuration in app-config.json. That switch configures the app to use CryptoPouch, which encrypts documents in the app's indexedb for storage and stores indexes using sqlCypher. This is why the shared-user-database is 0 but the indexes - such as shared-user-database-mrview-fc95555b88f96506de12d279698695f2 - are large.

    "},{"location":"system-administrator/rescuing-a-full-Tangerine-tablet/#pulling-the-files","title":"Pulling the files","text":"

    Once you list the files, you much change permissions on them and then pull them. Change the permissions (chmod 666) for each file you wish to transfer:

    adb shell \"run-as org.rti.tangerine chmod 666 /data/data/org.rti.tangerine/databases/shared-user-database-mrview-e4d1230d206245fc83e75ee420b1f31e\"\n

    Once the permissions is changed, you may transfer the files:

    adb exec-out run-as org.rti.tangerine cat databases/shared-user-database-mrview-e4d1230d206245fc83e75ee420b1f31e > shared-user-database-mrview-e4d1230d206245fc83e75ee420b1f31e\n

    After the files are transferred, you may reset the permissions:

    adb shell \"run-as org.rti.tangerine chmod 600 /data/data/org.rti.tangerine/databases/shared-user-database-mrview-e4d1230d206245fc83e75ee420b1f31e\"\n
    "},{"location":"system-administrator/rescuing-a-full-Tangerine-tablet/#restoring-a-database","title":"Restoring a database","text":"

    In the docs/system-administrator/restore-from-backup.md, there are instructions in the section \"Restoring backups onto a fresh Tangerine app installation\" on how to restore a database. Those instructions show how to use Android File Transfer to move the database files to the clean Android device and restore the app using a fresh Tangerine installation.

    "},{"location":"system-administrator/rescuing-a-full-Tangerine-tablet/#database-file-analysis","title":"Database file analysis","text":"

    In the docs/system-administrator/restore-from-backup.md, there are instructions in the section \"Viewing data from an encrypted backup\" that show how to compile and use sqlcipher to view and fix a corrupt SqlCypher database.

    "},{"location":"system-administrator/restore-from-backup/","title":"Restoring from a Backup","text":""},{"location":"system-administrator/restore-from-backup/#backing-up","title":"Backing up","text":"

    The Export Backup feature is available from the right-hand menu by selecting \"Export Data.\"

    Backups come in two types: - tabs with in-app scryption: a single file per database - tabs with device encryption: one directory per database, each of which contain many files.

    The Export Backup screen displays the backup location, and if the device uses device encryption, an input where the user may modify the \"Docs per backup file\" parameter.

    Press \"Export Data for all Users\" to initiate export.

    It will display a status message after each database backup is saved. The backup files will be saved in the Documents/Tangerine/backups directory. Use Android File transfer tool to transfer the files. Save all of the database backup files or directories. There should be four databases backed up:

    • shared-user-database
    • users
    • tangerine-lock-boxes
    • tangerine-variables

    The listing is similar when backing up an encrypted database; however, it backs up 4 files instead of 4 directories:

    "},{"location":"system-administrator/restore-from-backup/#restoring-backups-onto-a-fresh-tangerine-app-installation","title":"Restoring backups onto a fresh Tangerine app installation.","text":"

    This only works for sync-protocol-2.

    Connect the tablet to the pc with a USB cable. Use Android File transfer or Samsung Smart Switch to browse to the Documents/Tangerine/restore directory. Copy all database files or directories generated by the Export Data command from the pc to the restore directory.

    Install the Tangerine app on the tablet. DO NOT do the initial device setup (language selection/enter admin password/etc); instead, press the \"Restore Backup\" button to start the restore process.

    Read the instructions. When ready, press the \"Restore Backup\" button. It will display a confirmation prompt:

    If the device does not have a \"Documents\" directory in Internal Storage, it displays an error. The Troubleshooting section provides details about the error and its resolution:

    The restore feature logs the process for each database. After the databases have been restored, it initiates indexing of the databases:

    When restoring an encrypted database, indexing is not run due to the state of the application at this point of the installation. In this case, prepare to wait a few minutes or longer after restarting the app and logging in to allow it to index the home page. Once the home page is displayed, do a Sync to upload/download any updated files; this will also kick off indexing.

    After the restore process is complete, click the context button (|||) to close Tangerine and launch it again to load with the restored databases.

    "},{"location":"system-administrator/restore-from-backup/#restoring-form-history","title":"Restoring form history","text":"

    On a tablet, the history of every form response change is saved. An agglomeration of these changes is what is sync'd to the server. It is possible to view all of these changes using the tablet backup.

    After restoring the database, open the app in DevTools and in the javascript console enter the following commands:

    const db = await T.user.getUserDatabase()\n// docId is the document _id of the document you are trying to get the history from.\nlet docId = 'uuid'\nconst diffs = await T.tangyForms.getDocRevHistory(docId)\n// get diffs into copy buffer\ncopy(diffs)\n

    If there were any conflicts in the doc, they should be in _conflicts.

    You may wish to list all issues:

    const issues = (await db.query('byType', {key: 'issue', include_docs: true}))\n.rows\n.map(row => row.doc)\n.filter(issue => issue.resolveOnAppContexts && issue.resolveOnAppContexts.includes('CLIENT'))\n

    "},{"location":"system-administrator/restore-from-backup/#viewing-data-from-an-encrypted-backup","title":"Viewing data from an encrypted backup","text":"

    This is a deep dive - you probably don't need to do this.

    Ask user to go to Export Data and press \"Export Data for all Users\". The backup files will be saved in the Android/data/org.rti.tangerine/files directory. Transfer the shared-user-database file. Then ask the user to go to the About menu and read off the Device ID.

    In Fauxton, look up the device record in the group-uuid-devices database. Copy the value for the key property.

    Building SqlCipher on MacOSX:

    ```shell script git clone https://github.com/sqlcipher/sqlcipher.git cd sqlcipher/sqlcipher ./configure --enable-tempstore=yes CFLAGS=\"-DSQLITE_HAS_CODEC -I/usr/local/opt/openssl/include/\" LDFLAGS=\"/usr/local/opt/openssl/lib/libcrypto.a\"

    To use the compiled sqlcipher:\n\n```shell script\nsqlcipher/sqlcipher ~/Downloads/shared-user-database\n
    In the sqlcipher console, enter the key:

    PRAGMA key = 'secret-key-uuid-very-secret';\n

    To list tables:

    .tables\n
    Output: ```shell script attach-seq-store by-sequence local-store attach-store document-store metadata-store
    ## Recovering a corrupted database\n\nOpen the database:\n\n```shell script\nsqlcipher/sqlcipher ~/Downloads/shared-user-database\n
    Enter the key:

    PRAGMA key = 'secret-key-uuid-very-secret';\n

    Run a check on the database. It will probably return something like \"database disk image is malformed\", which is not terribly useful:

    sqlite>PRAGMA integrity_check;\n

    Run the following commands to dump the sql and build a new database (kudos: https://blog.niklasottosson.com/databases/sqlite-check-integrity-and-fix-common-problems/):

    sqlite>.output backup.db\nsqlite>.dump\nsqlite>.quit\n>sqlite3 database_fixed.db\nsqlite>.read backup.db\nsqlite>.quit\n
    If there were no errors, you should be able to query database_fixed.db. If not, open backup.db in a text editor and rummage through the sql statements.

    "},{"location":"system-administrator/sync-protocol-1/","title":"Sync Protocol 1","text":"

    Sync protocol 1 is deprecated as of 12-15-2022. We strongly recommend using Sync protocol 2, which is more secure.

    "},{"location":"system-administrator/sync-protocol-1/#background","title":"Background","text":"

    Sync protocol 1 was the original sync protocol for Tangerine that features a one-way push sync to a server.

    "},{"location":"system-administrator/sync-protocol-1/#configuration","title":"Configuration","text":""},{"location":"system-administrator/sync-protocol-1/#configsh","title":"config.sh","text":"
    • T_MODULES: Make sure that 'sync-protocol-2' is not listed in T_MODULES.
    • T_UPLOAD_TOKEN: The value for T_UPLOAD_TOKEN must match the value for 'uploadToken' in the group's app-config.json.
    • T_UPLOAD_WITHOUT_UPDATING_REV : A config.sh setting for use in high-load instances using sync-protocol-1. *** Using this setting COULD CAUSE DATA LOSS. *** This setting uses a different function to process uploads that does not do a GET before the PUT in order to upload a document. Please note that if there is a conflict it will copy the _id to originalId and POST the doc, which will create a new id. If that fails, it will log the error and not upload the document to the server, but still send an 'OK' status to client. The failure would result in data loss.
    "},{"location":"system-administrator/sync-protocol-1/#app-configjson","title":"app-config.json","text":"
    • uploadTokenThe value for 'uploadToken' must match the value for T_UPLOAD_TOKEN in config.sh.
    • uploadUnlockedFormReponses - when set to true this populates a list of doc_ids from responsesUnLockedAndNotUploaded view to be uploaded - even if doc.complete === false. This value is used mostly in projects Tangerine Class/Teach, where students are tested over time.
    "},{"location":"system-administrator/sync-protocol-1/#syncing","title":"Syncing","text":"

    Sync in sync-protocol-1 is very simple: if queries a view to check what docs satisfy this criteria: !doc.uploadDatetime || doc.lastModified > doc.uploadDatetime) If uploadUnlockedFormReponses is set, it includes docs where complete === false.

    "},{"location":"system-administrator/sync-protocol-2/","title":"Sync Protocol 2 (two-way sync)","text":""},{"location":"system-administrator/sync-protocol-2/#background","title":"Background","text":"

    When Tangerine was originally created, devices would only use a one-way sync from the tablet to the server sync-protocol-1 doc. Sync Protocol 2 was developed to enable two-way sync; typically, all data from the tablet is pushed to the server, and - in order to conserve bandwidth - only data from some forms is pulled to the tablet.

    The form responses that are synced depend on which forms are configured for sync and limited to a grouping by the \"location\" field in that users' profile.

    For example: An installation has two Forms, Form A and Form B. Only Form A is configured to sync. User A who has \"facility 1\" assigned to them in their user profile creates a form response for Form A and Form B then initiates a sync to find that two form responses have been pushed up. User B has \"facility 1\" assigned to them in their user profile and initiates a sync to find they pulled down one form response for Form A that originated on User A's device. If User B modifies this form response, it will be pushed on the next sync and then later User A would pull down the change. Let's say there is a User C who is assigned to \"facility 2\" in their user profile. When they initiate a sync, they will not receive any form responses from the server because the server only has form responses from User A who is assigned to \"facility 1\".

    "},{"location":"system-administrator/sync-protocol-2/#enabling-sync-protocol-2-for-new-groups","title":"Enabling Sync Protocol 2 for new Groups","text":"

    Note: Sync Protocol 2 is usually automatically enabled for new groups; however, these instructions show how to manually configure it.

    1. Enable Sync Protocol 2 before creating a new group by editing config.sh by adding \"sync-protocol-2\" to T_MODULES.
    2. Create a new group.
    3. Define location list levels and content in Config -> Location List.
    4. Create a new form in Author -> Forms.
    5. Go to Deploy -> Device Users and create new Device Users.
    6. Go to Deploy -> Devices and create new Devices.
    7. Go to Deploy -> Releases and release the app.

    \"syncProtocol\":\"2\" Enables a \"Device Setup\" process on first boot of the client application. This requires you set up a \"Device\" record on the server. When setting up a Device record on the server, it will give you a QR code to use to scan from the tablet in order to receive its device ID and token.

    "},{"location":"system-administrator/sync-protocol-2/#upgrade-an-existing-group-to-sync-protocol-2","title":"Upgrade an existing group to Sync Protocol 2","text":"

    If planning to use `\"syncProtocol\":\"2\" and a project already uses \"centrallyManagedUserProfile\" : true, remove \"centrallyManagedUserProfile\": true and configure the user profile's custom sync settings to push.

    "},{"location":"system-administrator/sync-protocol-2/#managing-data-conflicts","title":"Managing Data Conflicts","text":"

    Because we can sync data down to Devices, it's possible for two Devices to edit the same data between syncs. This causes a \"Data Conflict\". It's important for someone to monitor conflicts to ensure data integrity. Please refer to Managing Data Conflicts documentation.

    "},{"location":"system-administrator/sync-protocol-2/#modes-and-stages-of-sync-protocol-2","title":"Modes and stages of Sync Protocol 2.","text":"

    There are two different modes of sync:

    • Initial device setup: After a device has been registered (on tablet), the initial sync is executed. The initial sync has 3 stages:
    • Pull: It pulls documents from the server in batches, set by initialBatchSize from app-config.json (default: 1000). No documents are pushed, since no data collection has happened so far.
    • Status uploaded: Status of this sync process is uploaded.
    • Database optimization: After this initial sync, database indexes are created an optimized, which takes a little while. These indexes are critical to the app's performance.

    • Routine sync: After a short period of data collection, the user executes an Online sync. This sync has 4 stages:

    • Pull: Pull any new or updated documents from the server.
    • Push: Pushes any documents created on the tablet
    • Status uploaded: Status of this sync process is uploaded.
    • Database optimization: Indexes are updated.
    "},{"location":"system-administrator/sync-protocol-2/#sync-settings-in-app-configjson","title":"Sync settings in app-config.json","text":"

    Here are the settings that may be modified in app-config.json for sync: - initialBatchSize = (default: 1000) Number of documents downloaded in the first sync when setting up a device. - batchSize (default: 200) - Number of documents downloaded upon each subsequent sync. - writeBatchSize = (default: 50) - Number of documents written to the tablet during each sync batch. - changes_batch_size = (default: 50) - Enables support for reducing the number of documents processed in the changed feed when syncing. This setting will help sites that experience crashes when syncing or indexing documents. Using this setting will slow sync times.

    If the tablet user logs in as \"admin\", she may access the \"Admin Configuration\" menu. The \"Pull all docs from the server\" feature enables \"catching up\" any documents that were missed in previous syncs. This resets a placeholder variable (\"since\") to 0, causing the replication API to replicate any documents or updated documents that are not on the tablet. This feature uses the \"initialBatchSize\" setting to download larger batches of documents.

    If users report errors during sync, consider reducing these settings. The \"writeBatchSize\" is the most critical setting because it manages how many documents are written to the database at a time. If the batch is too large, the sync may fail.

    "},{"location":"system-administrator/sync-protocol-2/#security","title":"Security","text":"

    A big part of using Sync Protocol 2 is embracing device configuration into the workflow. Sync Protocol 2 is more than just sync: it also provides the structure for encrypting a database. A mobile device must be registered in Tangerine before it may sync. The \"Deploy / Devices\" page enables registration of a device. Device registration creates a key (for encrypting the db), token(for sync authentication with the server), and other identifiers associated with that device. The admin then scans a QR code for the device's registration that installs the device record (which includes the key) in the device's tangerine-lock-boxes IndexedDB database (see LockBoxService), and is used to encrypt the Tangerine databases. A user's username and password are used to decrypt the lockbox. - See db.factory to see how a key is passed in to encrypt a db. - See sync.service to see how deviceToken is used for authentication in the syncSessionUrl.

    "},{"location":"system-administrator/sync-strategies/","title":"Sync Strategies","text":""},{"location":"system-administrator/sync-strategies/#overview","title":"Overview","text":"

    The choice of sync strategy impacts how Tangerine syncs with the server. If you configure a form to use two-way sync, it uses CouchDB replication; otherwise, it uses custom sync. How are these two types of sync different? - CouchDB replication: -- If there is conflicting data on the server, the document update fails and it creates a log of the conflict on the uploaded document -- It currently does not notify the tablet user that there was a conflict. The data on the server displays the previous data, not the new, conflict data. See below how to view the new, conflict data. -- Uses more bandwidth - Custom sync -- If there is conflicting data on the server, it overwrites the document and does not make a log of the conflict. It uses the pouchdb-upsert plugin to do the write. -- Uses less bandwidth

    "},{"location":"system-administrator/sync-strategies/#how-to-tell-if-there-are-conflicts-when-using-couchdb-replication","title":"How to tell if there are conflicts when using CouchDB replication?","text":"

    Add \"conflicts=true\" to the url if checking view curl, or in your application, add {conflicts: true} option when you get() it. It will list the conflicts:

    _conflicts:[\n\"29-0003a0b8af090d907efecde3aa121416\",\n\"25-f712a217de615f44c66ddb16b1a53a19\",\n\"14-bad1258430d22ad41dc9ce4123283c4f\",\n\"5-3fcde4c45f910b7a0c541e837e4ffd3c\"\n]\n

    Query the form using \"rev\" in the querystring to view the conflicted version.

    http://localhost:5984/group-58093841-eaeb-4e51-8675-29757d71fd35/3cec5368-7b89-43cd-9c59-bcd1584dd4ea?rev=5-3fcde4c45f910b7a0c541e837e4ffd3c\n

    See https://pouchdb.com/guides/conflicts.html for more information.

    "},{"location":"system-administrator/tangerine-nginx-ssl/","title":"Configuring Nginx as SSL proxy server for Tangerine","text":""},{"location":"system-administrator/tangerine-nginx-ssl/#update","title":"Update","text":"

    Issue #3147 describes the start-ssl.sh script that automates installation of the SSL certificates as well as the letsencrypt-nginx-proxy-companion and nginx-proxy containers. The first time it runs it may error out - do a docker logs -f letsencrypt-nginx-proxy-companion to see error. If it does, restart the container (docker restart letsencrypt-nginx-proxy-companion).

    You may disregard the following notes - the new script supersedes them; however, they may have some useful information.

    "},{"location":"system-administrator/tangerine-nginx-ssl/#initial-configuration","title":"Initial configuration","text":"

    First open config.sh and change the port mapping of Tangerine

    T_PORT_MAPPING=\"-p 8080:80\"\n

    Rebuild the container by running ./start.sh

    "},{"location":"system-administrator/tangerine-nginx-ssl/#pull-nginx-docker-image-and-install-certbot-inside-the-nginx-container","title":"Pull nginx docker image and install certbot inside the nginx container","text":"

    If your container is not called tangerine adjust below. --link is to allow the ngin container to forward to the tangerine one

    docker pull nginx\ndocker run -p 80:80 -p 443:443 --link tangerine:tangerine --restart always --name nginx -d nginx\n

    Go into the nginx container and execute the below commands

    docker exec -it nginx bash\n\napt-get update && apt-get install certbot vim python3-certbot-nginx -y\n

    Open the config file

    vi /etc/nginx/conf.d/default.conf\n
    Adjust the server_name to your domain and the size of the client body

    server_name My.Domain.com;\nclient_max_body_size 0; \n

    Replace the location directive with the one below:

    location / {\n            # First attempt to serve request as file, then\n            # as directory, then fall back to displaying a 404.\n            proxy_pass_header  Server;\n            proxy_set_header   Host $http_host;\n            proxy_redirect     off;\n            proxy_set_header   X-Real-IP $remote_addr;\n            proxy_set_header   X-Scheme $scheme;\n            proxy_set_header X-Forwarded-Host $host:$server_port;\n            proxy_set_header X-Forwarded-Proto https;\n            proxy_pass         http://tangerine;\n        }\n
    Save the file and exit vi

    Now execute and follow the promtps for cerbot. It will fail but that's ok

    certbot --nginx\n
    The above will generate the certificate according to My.Domain.com name given. Select options 1. My.Domain.com

    Reload the config by running

    nginx -s reload\n

    Exit the nginx container and add some configuration for autmatic updates of certificates Execute crontab \u2013e and add the line below

     0 3 * * * docker exec -it nginx certbot renew --post-hook \"service nginx reload\"\n

    Optionally save your image to your docker images

    docker commit nginx nginx/nginx:configuredNginx\n

    "},{"location":"system-administrator/tangerine-nginx-ssl/#note-that-your-nginx-container-is-now-linked-to-your-tangerine-container-every-time-you-execute-startsh-for-tangerine-you-have-to-start-a-new-nginx-continer-to-udpate-the-link-to-recreate-using-the-saved-image-run-docker-run-p-8080-p-443443-link-tangerinetangerine-restart-always-name-nginx-d-nginxnginxconfigurednginx","title":"Note that your nginx container is now linked to your tangerine container. Every time you execute start.sh for tangerine you have to start a new nginx continer to udpate the link. TO recreate using the saved image run 'docker run -p 80:80 -p 443:443 --link tangerine:tangerine --restart always --name nginx -d nginx/nginx:configuredNginx'","text":"

    Point your DNS to the actual server

    "},{"location":"system-administrator/troubleshooting-android-devices/","title":"Troubleshooting Android Devices","text":""},{"location":"system-administrator/troubleshooting-android-devices/#modify-the-app-configjson-on-a-tangerine-apk-install","title":"Modify the app-config.json on a Tangerine APK Install","text":"

    If an Android device is having trouble, you may want to tweak the app-config.json settings on that specific device. Use the following commands to pull the app-config.json file off the device, modify it on your computer, and then push it back to the device.

    adb shell\nrun-as org.rti.tangerine\n# Discover what the path to the release is (will be a datetime), then use for subsequent paths.\nls files/cordova-hot-code-push-plugin/\ncd files/cordova-hot-code-push-plugin/2021.09.14-18.12.15/www/shell/assets\ncp app-config.json /sdcard/Download/\nexit\nadb pull /sdcard/Download/app-config.json\n# Time to modify app-config.json.\nvim app-config.json\nadb push app-config.json /sdcard/Download/\nadb shell\nrun-as org.rti.tangerine\n# Make sure to use that datetime path discovered earlier.\nmv /sdcard/Download/app-config.json /data/user/0/org.rti.tangerine/files/cordova-hot-code-push-plugin/2021.09.14-18.12.15/www/shell/assets/\n
    "},{"location":"system-administrator/upgrade-checklist/","title":"Upgrade checklist","text":""},{"location":"system-administrator/upgrade-checklist/#test-on-qa-server","title":"Test on QA server","text":"
    • Clone production server to QA server.
    • Remove all cronjobs that may be pushing to remote git repositories.
    • Remove all CouchDB replications that may be pushing data to remote CouchDBs.
    • Update T_HOSTNAME in tangerine/config.sh.
    • Run ./start.sh <currently used version of tangerine>. This puts the updated config in config.sh into the container.
    • Update serverUrl in all app-config.json files for each group in tangerine/data/group/<your-group-id>/client/app-config.json. Find and replace from the tangerine folder by modifying the following command: find ./data/groups/ -type f -name \"app-config.json\" -print0 | xargs -0 sed -i '' -e 's/production-server-hostname/qa-server-hostname/g'
    • Release all APKs and PWAs. This puts all updated app config into the APKs and PWAs.
    • Install and set up PWA/APKs for groups to test.
    • Upgrade the QA server following the server instructions for the release you are upgrading to in CHANGELOG.md (https://github.com/Tangerine-Community/Tangerine/blob/master/CHANGELOG.md). Note that you must upgrade incrementally between the versions. If you skip one you may miss important updates or they may not apply correctly and you risk corrupting your install without knowing it.
    • Test functionality on the server.
    • Release updated APKs and PWAs.
    • Upgrade test tablet and test functionality.
    "},{"location":"system-administrator/upgrade-checklist/#deploy-to-production","title":"Deploy to production","text":"
    • Make backup of production server.
    • Release PWAs/APKs on the test channels for all groups.
    • Install and set up PWA/APKs for groups to test using APK/PWA on the test channel.
    • Upgrade the QA server following the server instructions for the release you are upgrading to in CHANGELOG.md (https://github.com/Tangerine-Community/Tangerine/blob/master/CHANGELOG.md). Note that you must upgrade incrementally between the versions. If you skip one you may miss important updates or they may not apply correctly and you risk corrupting your install without knowing it.
    • Test functionality on the server.
    • Release updated APKs and PWAs on the test channel.
    • Upgrade test tablet and test functionality.
    • Release updated APKs and PWAs on the live channel.
    • Note the Build ID of the APK or PWA on the live channel, then in the content repository tag a release with a corresponding Build ID in git. If using Github, use the Releases feature and note the version of Tangerine upgraded to along with any other notes.
    • Terminate the QA server.
    "},{"location":"system-administrator/upgrade-instructions/","title":"Upgrade instructions","text":""},{"location":"system-administrator/upgrade-instructions/#server-upgrade-instructions","title":"Server upgrade instructions","text":"

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    Preparation

    Tangerine v3 images are relatively large, around 12GB. The server should have at least 20GB of free space plus the size of the data folder. Check the disk space before upgrading the the new version using the following steps:

    cd tangerine\n# Check the size of the data folder.\ndu -sh data\n# Check disk for free space.\ndf -h\n

    If there is less than 20 GB plus the size of the data folder, create more space before proceeding. Good candidates to remove are: older versions of the Tangerine image and data backups.

    # List all docker images.\ndocker image ls\n# Remove the image of the version that is not being used.\ndocker rmi tangerine/tangerine:<unused_version>\n# List all data backups.\nls -l data-backup-*\n# Remove the data backups that are old and unneeded.\nrm -rf ../data-backup-<date>\n

    Upgrade

    After ensuring there is enough disk space, follow the steps below to upgrade the server.

    1. Backup the data folder

      # Create a backup of the data folder.\ncp -r data ../data-backup-$(date \"+%F-%T\")\n

    2. Confirm there is no active synching from client devices

    Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like \"Created sync session\" for Devices that are syncing and \"login success\" for users logging in on the server.

    docker logs --since=60m tangerine\n
    1. Install the new version of Tangerine
      # Fetch the updates.\ngit fetch origin\n# Checkout a new branch with the new version tag.\ngit checkout -b <new_version> <new_version>\n# Run the start script with the new version.\n./start.sh <new_version>\n

    Clean Up

    After the upgrade, remove the previous version of the Tangerine image to free up disk space.

    docker rmi tangerine/tangerine:<previous_version>\n
    "}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000000..0f8724efd9 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 0000000000..80e8dfafa1 Binary files /dev/null and b/sitemap.xml.gz differ diff --git a/stylesheets/extra.css b/stylesheets/extra.css new file mode 100644 index 0000000000..710247ba94 --- /dev/null +++ b/stylesheets/extra.css @@ -0,0 +1,5 @@ +:root { + --md-primary-fg-color: #3c5b8d; + --md-primary-fg-color--light: #6c88bd; + --md-primary-fg-color--dark: #00325f; +} \ No newline at end of file diff --git a/system-administrator/assets/backup-encrypted-log.jpg b/system-administrator/assets/backup-encrypted-log.jpg new file mode 100644 index 0000000000..6a6cff9652 Binary files /dev/null and b/system-administrator/assets/backup-encrypted-log.jpg differ diff --git a/system-administrator/assets/backup-progress-and-ui.jpg b/system-administrator/assets/backup-progress-and-ui.jpg new file mode 100644 index 0000000000..70d2d9bd31 Binary files /dev/null and b/system-administrator/assets/backup-progress-and-ui.jpg differ diff --git a/system-administrator/assets/confirm-restore-prompt.jpg b/system-administrator/assets/confirm-restore-prompt.jpg new file mode 100644 index 0000000000..7ca8eeec00 Binary files /dev/null and b/system-administrator/assets/confirm-restore-prompt.jpg differ diff --git a/system-administrator/assets/home-restore-button.jpg b/system-administrator/assets/home-restore-button.jpg new file mode 100644 index 0000000000..52d058148f Binary files /dev/null and b/system-administrator/assets/home-restore-button.jpg differ diff --git a/system-administrator/assets/restore-encrypted.jpg b/system-administrator/assets/restore-encrypted.jpg new file mode 100644 index 0000000000..a798f5659c Binary files /dev/null and b/system-administrator/assets/restore-encrypted.jpg differ diff --git a/system-administrator/assets/restore-files-listing.jpg b/system-administrator/assets/restore-files-listing.jpg new file mode 100644 index 0000000000..099a538210 Binary files /dev/null and b/system-administrator/assets/restore-files-listing.jpg differ diff --git a/system-administrator/assets/restore-optimizing.jpg b/system-administrator/assets/restore-optimizing.jpg new file mode 100644 index 0000000000..0f79d55060 Binary files /dev/null and b/system-administrator/assets/restore-optimizing.jpg differ diff --git a/system-administrator/assets/restore-troubleshooting-error.jpg b/system-administrator/assets/restore-troubleshooting-error.jpg new file mode 100644 index 0000000000..170695b92e Binary files /dev/null and b/system-administrator/assets/restore-troubleshooting-error.jpg differ diff --git a/system-administrator/automating-upgrades-of-tangerine/index.html b/system-administrator/automating-upgrades-of-tangerine/index.html new file mode 100644 index 0000000000..794983c69e --- /dev/null +++ b/system-administrator/automating-upgrades-of-tangerine/index.html @@ -0,0 +1,8 @@ + Automating Upgrades of Tangerine - Tangerine Documentation

    Automating Upgrades of Tangerine

    If you have a particularly complex upgrade of Tangerine that involves changing configurations, writing your own upgrade script and testing that on a QA server can be a way to ensure smooth upgrades when you go to production. Below you will find various tips and tricks we've discovered along the way of writing our own upgrade scripts.

    Update a group's configuration in the groups database

    In this example, we modify the xyz group's configuration to implement some csvReplacementCharacters. First we install into the container the jq utility for modifying JSON on the command line, then we modify the group's config doc in the second command. To use this example, replace the two xyz instances with the group's ID you want to modify.

    docker exec -it tangerine apt install -y jq
    +docker exec -it tangerine bash -c 'curl -s $T_COUCHDB_ENDPOINT/groups/xyz | jq ".csvReplacementCharacters = [[\",\",\"|\"],[\"\n\",\"___\"]]" | curl -s -T - -H "Content-Type: application/json" -X PUT $T_COUCHDB_ENDPOINT/groups/xyz'
    +

    Updating a group's content repository

    Given the upgrade of Tangerine, there may be associated content changes required. Set up an deploy key in your content repository on github and modify the following script to suit your needs.

    cd /path-to-tangerine/tangerine/data/groups/some-group-id/
    +GIT_SSH_COMMAND='ssh -i /root/.ssh/id_github.pub' git fetch origin
    +git checkout v2.0.0
    +

    Update a group's app-config.json

    Best practice for ensuring that configuration in client/app-config.json is maintained over time is to first update the configuration you want to modify in your group's client/app-config.defaults.json and then retemplate the defaults to the client/app-config.json file. To use the following example, replace instances of group-xyz with the relevant Group ID and GROUP XYZ with the relevant Group Name.

    docker exec -it tangerine apt install -y jq
    +docker exec -it tangerine bash -c 'cat /tangerine/groups/group-xyz/client/app-config.defaults.json | jq ".serverUrl = \"$T_PROTOCOL://$T_HOST_NAME\"" | jq ".groupId = \"group-xyz\"" | jq ".groupName = \"Group XYZ\"" > /tangerine/groups/group-xyz/client/app-config.json'
    +
    \ No newline at end of file diff --git a/system-administrator/configuration-notes/index.html b/system-administrator/configuration-notes/index.html new file mode 100644 index 0000000000..da82a5397f --- /dev/null +++ b/system-administrator/configuration-notes/index.html @@ -0,0 +1 @@ + Configuration Notes - Tangerine Documentation
    \ No newline at end of file diff --git a/system-administrator/configuring-aws-cloudwatch-for-logs/index.html b/system-administrator/configuring-aws-cloudwatch-for-logs/index.html new file mode 100644 index 0000000000..b4f390017b --- /dev/null +++ b/system-administrator/configuring-aws-cloudwatch-for-logs/index.html @@ -0,0 +1 @@ + Configuring AWS Cloudwatch - Tangerine Documentation

    Configuring AWS Cloudwatch

    Create CloudWatchAgentServerRole role

    https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/create-iam-roles-for-cloudwatch-agent.html

    Follow the directions in the section marked "To create the IAM role necessary for each server to run the CloudWatch agent" which are summarized here:

    To create the IAM role necessary for each server to run the CloudWatch agent

    • Sign in to the AWS Management Console and open the IAM console at https://console.aws.amazon.com/iam/.
    • In the navigation pane, choose Roles and then choose Create role. Under Select type of trusted entity, choose AWS service.
    • Immediately under Common use cases, choose EC2,and then choose Next: Permissions.
    • In the list of policies, use the search box to find the CloudWatchAgentServerPolicy and select its checkbox.
    • To use Systems Manager to install or configure the CloudWatch agent, select the box next to AmazonSSMManagedInstanceCore. This AWS managed policy enables an instance to use Systems Manager service core functionality. If necessary, use the search box to find the policy. This policy isn't necessary if you start and configure the agent only through the command line.
    • Choose Next: Tags. (If needed)
    • Choose Next: Review. For Role name, enter a name for your new role, such as CloudWatchAgentServerRole or another name that you prefer.
    • Confirm that CloudWatchAgentServerPolicy and optionally AmazonSSMManagedInstanceCore appear next to Policies.
    • Choose Create role. The role is now created.

    Diregard the directions marked "To create the IAM role for an administrator to write to Parameter Store"

    Attach IAM role CloudWatchAgentServerRole to instance

    https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/iam-roles-for-amazon-ec2.html#attach-iam-role

    To attach an IAM role to an instance - Open the Amazon EC2 console at https://console.aws.amazon.com/ec2/. - In the navigation pane, choose Instances. - Select the instance, choose Actions, Security, Modify IAM role. - Select the IAM role to attach to your instance, and choose Save.

    Installing or updating SSM Agent

    https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-install-ssm-agent.html

    Agent is already installed in Ubuntu Server 16.04, 18.04, and 20.04 - To check status sudo systemctl status snap.amazon-ssm-agent.amazon-ssm-agent.service - To start agent sudo snap start amazon-ssm-agent

    Download and configure the CloudWatch agent

    https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/download-CloudWatch-Agent-on-EC2-Instance-SSM-first.html

    • To download the CloudWatch agent using Systems Manager, Open the Systems Manager console at https://console.aws.amazon.com/systems-manager/.
    • In the navigation pane, choose Run Command. -or- If the AWS Systems Manager home page opens, scroll down and choose Explore Run Command.
    • Choose Run command. In the Command document list, choose AWS-ConfigureAWSPackage.
    • In the Targets area, choose the instance to install the CloudWatch agent on. If you don't see a specific instance, you probably don't have the correct Policy associated with your instance
    • In the Action list, choose Install. In the Name field, enter AmazonCloudWatchAgent.
    • Keep Version set to latest to install the latest version of the agent.
    • Choose Run.
    • Optionally, in the Targets and outputs areas, select the button next to an instance name and choose View output. Systems Manager should show that the agent was successfully installed.

    Configure the CloudWatch agent

    The agent configuration file is a JSON file that specifies the metrics and logs that the agent is to collect, including custom metrics. You can create it by using the wizard or by creating it yourself from scratch. You could also use the wizard to initially create the configuration file and then modify it manually.

    Create and modify the agent configuration file - You can configure the agent on your AWS EC2 instance. Run the following command: /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-config-wizard - When the wizard runs, choose to install statsD and CollectD (default is yes), the 'Advanced' level of metrics. - The defaults are usually fine, but decline when it asks "Do you want to monitor any log files?" and also "Do you want to store the config in the SSM parameter store?" - The configuration file config.json is stored in /opt/aws/amazon-cloudwatch-agent/bin/config.json

    https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/create-cloudwatch-agent-configuration-file.html

    To restart the CloudWatch agent

    https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/install-CloudWatch-Agent-on-EC2-Instance-fleet.html#start-CloudWatch-Agent-EC2-fleet

    sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c file:/opt/aws/amazon-cloudwatch-agent/bin/config.json

    If you see an error about collectd, install it: apt install collectd

    https://github.com/awsdocs/amazon-cloudwatch-user-guide/issues/54#issuecomment-696844909

    Modifying the dashboard

    Create a CloudWatch alarm based on a static threshold

    https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/ConsoleAlarms.html

    Custom namespaces

    The default namespace for metrics collected by the CloudWatch agent is CWAgent, although you can specify a different namespace when you configure the agent. CWAgent provides useful data such as disk utilization which can be used on a dashboard or for an alert.

    Go to CloudWatch -> Metrics -> All Metrics and scroll to Custom namespaces to see the CWAgent namespace.

    List of data collected by CWAgent here: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/metrics-collected-by-CloudWatch-agent.html

    SNS Notifications

    To create a topic: https://eu-west-1.console.aws.amazon.com/sns/v3/home?region=eu-west-1#/topics

    \ No newline at end of file diff --git a/system-administrator/couchdb/index.html b/system-administrator/couchdb/index.html new file mode 100644 index 0000000000..51b5232c64 --- /dev/null +++ b/system-administrator/couchdb/index.html @@ -0,0 +1,13 @@ + CouchDB - Tangerine Documentation

    CouchDB

    Upgrade

    If there has been a security update to CouchDB 2.x, all you must do is rerun the start.sh command and the new image for CouchDB will be downloaded and run.

    Change password

    # Shutdown tangerine and couchdb
    +docker stop tangerine couchdb
    +# Remove the docker files that cached the password for CouchDB.
    +rm -r data/couchdb/local.d
    +# Update the T_COUCHDB_USER_ADMIN_PASS variable.
    +vim config.sh
    +# update the cached password in the reporting worker configuration.
    +vim data/reporting-worker-state.json
    +# If using the mysql module, update the passwords in each group's mysql state file.
    +vim data/mysql/state/<groupId>.ini
    +# start.sh will rebuild the couchdb and tangerine containers which will update the password in all necessary places.
    +./start.sh <version>
    +
    \ No newline at end of file diff --git a/system-administrator/index.html b/system-administrator/index.html new file mode 100644 index 0000000000..936bb436be --- /dev/null +++ b/system-administrator/index.html @@ -0,0 +1 @@ + System Administrator Guide - Tangerine Documentation
    \ No newline at end of file diff --git a/system-administrator/install-on-aws/index.html b/system-administrator/install-on-aws/index.html new file mode 100644 index 0000000000..cb7f7603f0 --- /dev/null +++ b/system-administrator/install-on-aws/index.html @@ -0,0 +1,10 @@ + Installing Tangerine on AWS - Tangerine Documentation

    Installing Tangerine on AWS

    Creating the AWS instance

    Login to AWS and Launch a new instance with Ubuntu 18.04 using a t2.medium server with 4 GiB memory.

    Volume should be larger than the 8GB default. 24GB would be useful, but if you're planning to test different Tangerine images, go for 64GB.

    Security

    Make sure to assign a security group to your instance that allows you to access port 80 via a web browser and port 22 via ssh.

    • HTTP: TCP 80 0.0.0.0/0
    • SSH TCP 22 0.0.0.0/0

    Set up SSL

    Prerequisites:

    • An SSL Certificate. If you don't yet have one, we recommend using AWS's Certificate Manager (found under "Security, Identity, and Compliance").

    Create and Configure an Elastic Load Balancer (ELB):

    • Go to EC2, click "Load Balancers" in the left column, click "Create Load Balancer", and then select "Classic Load Balancer".
    • Step 1: Define Load Balancer
    • Set a Load Balancer name to what you want.
    • Set "Load Balancer Protocol" on the left most column to "HTTPS".
    • Set "Instance Protocol" in the third column to "HTTP".
    • Click "Add".
    • In the new row set "Load Balancer Protocol" to "HTTP" and "Instance Protocol" to "HTTP".
    • Click "Next".
    • Step 2: Assign Security Groups
    • Select "Create a new security group".
    • Set rules for both HTTP and HTTPS. If you only do HTTPS, anyone who goes to http://yourdomain.com will get an Access Denied message. Allow them to access the site with HTTP, the software will forward them to HTTPS automatically.
    • Click "Next".
    • Step 3: Configure Security Settings
    • If you have an SSL certificate, you can upload that here. Otherwise select "Choose an existing certificate from AWS Certificate Manager (ACM)".
    • If you have not requested a certificate for your domain yet, you will need to click "Request a new certificate from ACM" and follow those instructions before proceeding.
    • Step 4: Configure Health Check
    • Ping Protocol: HTTP
    • Ping Port: 80
    • Ping Path: /app/tangerine/index.html
    • Response Timeout: 5 seconds
    • Interval: 10 seconds
    • Unhealthy threshold: 10
    • Healthy threshold: 2
    • Step 5: Add EC2 Instances
    • Select the EC2 instance running Tangerine.
    • Step 6: Add Tags
    • No tags are required for Tangerine.
    • Step 7: Review
    • If everything looks good, go ahead and create it!
    • Now proceed to your Load Balancers dashboard, click on your load balancer, click on the Instances tab, and now wait for your EC2 instance to be listed as "InService".
    • Configure your domain's DNS to point to this load balancer by clicking on the load balancer's Description tab and using the "DNS name" given to configure your Domain's DNS.

    SSH Login to Server

    Once your server is created, login with your key:

     ssh -i ~/.ssh/iyour_key -l ubuntu <your EC2 instance's IP address>
    +

    Now you may continue to step 2 in the installation instructions of Tangerine's README.md, then pick back up here.

    Configure Logs

    Send logs to AWS CloudWatch for building alarms and saving disk space.

    1. Create IAM user with programattic access and AWSAppSyncPushToCloudWatchLogs policy. Keep open credentials screen for reference.
    2. Install aws-cli with sudo apt-get install awscli.
    3. aws configure and give the credentials for the IAM user.
    4. Go to AWS Console -> IAM -> Access Management -> Roles -> Create Role, create a role called aws-cloudwatch-logs with an attached policy of AWSOpsWorksCloudWatchLogs.
    5. Go to AWS Console -> EC2 -> Instances -> <select your instance> -> Actions -> Security -> Modify IAM role and add the aws-cloudwatch-logs role to the EC2 instance.
    6. Go to AWS Console -> CloudWatch -> Logs -> Actions -> Create log group.
    7. Create the Log Group named after the instance name (ie. example-v3).
    8. Write the configuration to /etc/docker/daemon.json. Change awslogs-region to the "less specific" region name (eu-central-1 as opposed to eu-central-1b) and replace example-v3 in tag and awslogs-group to reflect the EC2 instance name.
    9. Then run systemctl restart docker. If containers were already running, you may need to recreate them for settings to take hold. For Tangerine, that just means running ./start.sh again.
    10. After setting up Tangerine, navigate in your browser to AWS Console -> CloudWatch -> Logs and select your instance's log group. There you will find two streams, one for the tangerine container the other for couchdb container using the "tag" pattern you configured in daemon.json.
    {
    +    "log-driver": "awslogs",
    +    "log-opts": {
    +        "awslogs-region": "eu-central-1",
    +        "awslogs-group": "example-",
    +        "tag": "example-{{.Name}}"
    +    }
    +}
    +

    Configure Alarm

    With Docker logs being sent to AWS CloudWatch, you can configure an alarm to detect if Tangerine is down. The following directions explain how to send an automated email if a Tangerine heartbeat log message is not heard for 15 minutes.

    • Navigate in your browser to AWS Console -> CloudWatch -> Logs.
    • Open your server's log group.
    • Open the stream for Tangerine. If your tag pattern in /etc/docker/daemon.json is example-{{.Name}}, then your stream name will be example-tangerine.
    • In the Filter events text box, type heartbeat and press enter. This will filter the logs to all heartbeat messages.
    • With the filter still applied, click the "Create Metric Filter" button.
    • Fill out "Metric" form as follows:
      • Filter name: heartbeat
      • Filter pattern: heartbeat
      • Metric namespace: tangerine
      • Metric name: heartbeat
      • Metric value: 1
      • Default value: 0
      • Unit: leave blank
    • Navigate to your log group and click the "Metric filter" tab, click the checkbox in your Metric's box, then click "Create alarm" button.
    • Fill out the form:
      • Metric name: heartbeat
      • Statistic: Sum
      • Period: 15 minutes
      • Threshold type: Static
      • Whenever heartbeat is...: Lower
      • than...: 1
      • Additional configuration
      • Datapoints to alarm: 1 out of 1
      • Missing data treatment: Treat missing data as bad (breaching the threshold)
    • Fill out "Notification" form as follows:
      • Alarm state trigger: In alarm
      • Select an SNS topic: Create new topic
      • Create a new topic...: <server name>-tangerine-heartbeat
      • Email endpoints that will receive the notification...: <your email address>
    • Click "Create Topic" button, then "Next" button.
    • Fill out "Name and description" form as follows then click "Next" button:
      • Alarm name: <server name>-tangerine-heartbeat
    • Now on the "Preview and create" screen, click "Create Alarm"
    • Check your email to confirm subscription to SNS Topic.
    \ No newline at end of file diff --git a/system-administrator/integrate-group-with-github/index.html b/system-administrator/integrate-group-with-github/index.html new file mode 100644 index 0000000000..7789cbb8f3 --- /dev/null +++ b/system-administrator/integrate-group-with-github/index.html @@ -0,0 +1,15 @@ + Integrate a group's content with a repository on Github - Tangerine Documentation

    Integrate a group's content with a repository on Github

    Step 1

    Create a group in the Editor. Note the ID in the URL starting with group-.

    Step 2

    Create a repository on Github for your group's content.

    Step 3

    SSH into the server and create a "deploy key" the server will use to authenticate to Github with. When running ssh-keygen, do not password protect the key file. When it prompts you for a password, just hit enter.

    ssh <your server>
    +sudo su
    +ssh-keygen -t rsa -b 4096 -C "root@domain_of_server"
    +cat /root/.ssh/id_rsa.pub
    +

    Change the key permissions if necessary.

    Copy the key contents that we just "cat'ed" to the screen. Then go to your Repository on Github and click on Settings -> Deploy keys -> Add deploy key and paste that key in the key contents, enable "Allow write access" and save.

    Step 4

    Now we push our group's initial content to our github repository with the following commands...

    cd tangerine/data/client/content/groups/<group id>
    +git init
    +git add .
    +git commit -m "first commit"
    +git remote add origin <githut repository SSH URL>
    +git push origin master
    +

    You should now see on your Github Repository code page a list of files pushed from the server.

    Step 5

    We'll now configure your server to periodically pull content changes from Github.

    ssh <your server>
    +sudo su
    +crontab -e
    +
    Enter the following onto a new line. Replace <group id> with appropriate Group ID.
    * * * * * cd /home/ubuntu/tangerine/data/client/content/groups/<group id> && GIT_SSH_COMMAND='ssh -i /root/.ssh/id_rsa' git pull origin master && git add . && git commit -m 'auto-commit' && GIT_SSH_COMMAND='ssh -i /root/.ssh/id_rsa' git push origin master
    +

    \ No newline at end of file diff --git a/system-administrator/managing-data-conflicts/index.html b/system-administrator/managing-data-conflicts/index.html new file mode 100644 index 0000000000..dfeb1171e1 --- /dev/null +++ b/system-administrator/managing-data-conflicts/index.html @@ -0,0 +1,6 @@ + Managing Data Conflicts - Tangerine Documentation

    Managing Data Conflicts

    When using Sync Protocol 2 we can sync data down to Devices. Because of this it is possible for two Devices to edit the same data between syncs. This causes a "Data Conflict". When a Device is syncing and a conflict is detected.

    To manage Data Conflicts you will need to know the IP Address of your server and have the CouchDB Admin credentials found in config.sh.

    Step 1: Find Documents with Conflicts

    Go to <serverIpAddress>:5984/_utils/#database/<groupId>/_design/shared_conflicts/_view/shared_conflicts. This is a list of Documents with conflicts. Click the first one in the list which will open the Document with conflicts. Note this doc has link in the second bar from the top on the right with label of "Conflicts".

    Step 2: Merge Conflict Revisions contents into Doc

    With the Document open, in Fauxton, copy the URL and open another window with the same URL side by side. On the window on the left, click the "Conflicts" link in the top right of that window. Note the "Conflicting Revisions" drop down in the Conflicts Browser. These Conflicting Revisions are the contents of Documents that were the "losing revision" when in a conflict. Selecting a "Conflict Rev" in the Conlict Revisions dropdown will result in the difference between the Conflicting Revision's contents and the current revision's contents. JSON highlighted in green belongs to the Conflict Rev.

    Cycle through each of the Conflict Revs migrating JSON highlighted in green over to the current Document edit view in your browser on the right. When all contents in the Conflict Revs have been migrated (AKA "merged") into the current Document, save the current doc with the changes and proceed to the next step.

    Step 3: Archive Conflict Revisions

    Use the pouchdb-couchdb-archive-conflicts CLI tool to archive the conflicts in the Document. This will result in removing the Document for the conlist list in step 1 and save a copy of each conflict revision into `<serverIpAddress>:5984/_utils/#database/<groupId>-conflict-revs/.

    Install the tool. Requires Node.js (https://nodejs.org/).

    npm install -g pouchdb-couchdb-archive-conflicts
    +

    archive-conflicts http://<username>:<password>@<serverIp>:5984/<groupId> <docId> 
    +

    Now return to Step 1 and pick the next Document in the list.

    Reviewing Archived Conflict Revisions

    To review a Documents archived conflict revisions, add a byConflictDocId view to the database.

    function (doc) {
    +  emit(doc.conflictDocId, doc.conflictRevId);
    +}
    +

    When viewing this view you can then click "Options" and enter the follwing under "By Keys" ["<docId>"].

    \ No newline at end of file diff --git a/system-administrator/managing-legacy-conflict-issues/index.html b/system-administrator/managing-legacy-conflict-issues/index.html new file mode 100644 index 0000000000..ff747cee15 --- /dev/null +++ b/system-administrator/managing-legacy-conflict-issues/index.html @@ -0,0 +1,4 @@ + Managing Legacy Conflict Issues - Tangerine Documentation

    Managing Legacy Conflict Issues

    The following describes a process for managing data conflicts using a deprecated feature known as "Auto-merge" and "Conflict Issues".

    When using Sync Protocol 2 we can sync data down to Devices. Because of this it is possible for two Devices to edit the same data between syncs. This causes a "Data Conflict". When a Device is syncing and a conflict is detected, the Tangerine software will try to resolve that conflict by merging the two versions. When Tangerine does this, it creates a "Conflict Issue" which stores the two versions and the merged version for safe keeping (in Issue this is refered to as A, B, and Merged). These are important to review in the Issue queue to ensure that the merge is satisfactory. Sometimes a merge will not be possible and the data will be left in a Conflict state according to CouchDB's definition of Conflict.

    To manage Data Conflicts you will need to know the IP Address of your server and CouchDB Admin credentials found in config.sh.

    Monitor how many Conflict Issues there are by Document ID

    1. Go to <serverIpAddress>:5984/_utils/#/database/<groupId>/_design/issuesOfTypeConflictByConflictingDocTypeAndConflictingDocId/_view/issuesOfTypeConflictByConflictingDocTypeAndConflictingDocId
    2. Click wrench for view on left hand column, make sure "Reduce: Count" is enabled on the view.
    3. Click "Options", check "Reduce" and then set "Group Level" of 2.

    Get a list of all conflict issues in a group

    <serverIpAddress>/<groupId>/_design/issuesOfTypeConflictByConflictingDocTypeAndConflictingDocId/_view/issuesOfTypeConflictByConflictingDocTypeAndConflictingDocId?reduce=false

    Check a single document for conflicts

    Get a list of all conflict issues for a document

    If you know the document id for a case, you can get a list of its conflict issues.

    In the following example, the first parameter in the key is set to 'case' to signify that it is a case document (type:'case'); however, in the future conflicts may also betype:'response'.

    Replace <docId> and enter the following into your browser:

    <serverIpAddress>/<groupId>/_design/issuesOfTypeConflictByConflictingDocTypeAndConflictingDocId/_view/issuesOfTypeConflictByConflictingDocTypeAndConflictingDocId?reduce=false&key=["case","<docId>"]

    Get the number of conflicts in a document

    If you know the document id for a case, replace <docId> and enter the following into your browser:

    <serverIpAddress>/<groupId>/_design/issuesOfTypeConflictByConflictingDocTypeAndConflictingDocId/_view/issuesOfTypeConflictByConflictingDocTypeAndConflictingDocId?reduce=true&group_level=2&key=["case","<docId>"]

    Monitor active Database Conflicts in CouchDB

    Go to <serverIpAddress>:5984/_utils/#database/<groupId>/_design/shared_conflicts/_view/shared_conflicts.

    To view the number of conflicts per document, click the table tab.

    To resolve a conflict, click on the row to open the Doc in conflict. Then click the "Conflicts" tab to resolve the Conflict.

    Viewing the history of Documents in the database

    Sometimes it helps to look back at the history of a Case. When a Case is open in Tangerine Editor, you can run the following in the Chrome Devtools Console.

    await T.case.getCaseHistory()
    +

    To look at the full picture that CouchDB server is aware of, use the following URL structure...

    <serverIpAddress>:5984/<groupId>/<docId>?revs_info=true
    +
    You will find the response contains a _revs_info property with an array of revisions. If a revisions has "status" of "unavailable", then that revision is on the Device it came from. You can find out which Device likely has that revision by finding the next available revision, getting the doc at that revision, and inspecting the modifiedByDeviceId property. This is the version of the doc that was uploaded to the server.

    <serverIpAddress>:5984/<groupId>/<docId>?rev=<revId>
    +
    \ No newline at end of file diff --git a/system-administrator/mysql-js/index.html b/system-administrator/mysql-js/index.html new file mode 100644 index 0000000000..b757f1a80f --- /dev/null +++ b/system-administrator/mysql-js/index.html @@ -0,0 +1,11 @@ + MySQL-JS Module - Tangerine Documentation

    MySQL-JS Module

    Enabling MySQL-JS module

    Important! If reenabling the mysql module, remove the mysql folder: rm -r data/mysql

    Step 1

    Ensure the variables from the MySQL section are in your customized config.sh file.

    Mysql

    • T_MYSQL_CONTAINER_NAME="mysql" # Either the name of the mysql Docker container or the hostname of a mysql server or AWS RDS MySQL instance.
    • T_MYSQL_USER="admin" # Username for mysql credentials
    • T_MYSQL_PASSWORD="password" # Password for mysql credentials
    • T_USE_MYSQL_CONTAINER="true" # If using a Docker container, set to true. This will automatically start a mysql container when using a Tangerine launch script.

    Step 2

    Ensure the T_MYSQL_PASSWORD variable is set to a sufficiently secure string. Failure to properly secure this password will without a doubt result in ransomware bots hacking your database.

    Step 3

    Add the mysql module to T_MODULES_ENABLED in config.sh.

    For example:

    T_MODULES="['csv','mysql-js']"
    +

    Step 4

    Run the start script to load in new configuration. Do this even if your server is already running. Note that restarting the container will not work, we have to run ./start.sh to recreate the container with the new configuration.

    ./start.sh <version>
    +

    Note: Upgrading an older version of Tangerine may require running docker exec tangerine push-all-groups-views after to enable indexes used for mysql

    Step 5

    Clear reporting cache to start generating a MySQL database for each group.

    docker exec tangerine reporting-cache-clear
    +

    You can check in on the progress of generating the mysql database using the mysql-report command. (Warning The mysql-report command creates a heavy workload to an instance so do not use it when mysql is trying to process a lot of data from couchdb. See the "Troubleshooting" section below.) It will return for each kind of case data and form, how many records are in the source database vs. how many have made it over to mysql. Note that if your system is under heavy load during the processing of this, this command may stress it out even more so it may be best to wait until you see a load of less than one using a tool like top or htop.

    docker exec -it tangerine bash 
    +mysql-report <groupId> | json_pp
    +

    Step 6

    In the reboot instruction in crontab that to starts Tangerine on reboot, add mysql container to the containers that start before tangerine and increase the sleep command to 60 seconds. Failure to implement this will result in tangerine failing to start on reboot.

    @reboot docker start couchdb mysql && sleep 60 && docker start tangerine
    +

    Also add a cron job to run mysql-report at 1 a.m every day - this will keep the mysql indexes current.

    # Run mysql-report at 1 a.m every day:
    +# 0 1 * * * docker exec tangerine mysql-report group-479f455e-b1bd-481b-8bd7-0d985a07431c
    +

    Step 7

    The most basic way to access MySQL would be to use the MySQL CLI.

    docker exec -it tangerine bash
    +mysql -u"$T_MYSQL_USER" -p"$T_MYSQL_PASSWORD" -hmysql
    +

    On the mysql command line, list the available databases using show databases;. Note how the database names are similar to the Group ID's these correspond with except with dashes removed. For example, if the group ID was group-abc-123, the corresponding MySQL database would be groupabc123. To select a database, type use <database ID>; then show tables; to list out the available tables.

    Step 8

    To set up remote encrypted connections to mysql, three options:

    1. TLS: In the tangerine/data/mysql/databases folder you will find files ca.pem, client-cert.pem, and client-key.pem. Distribute those files to your MySQL users so they may connnect to your server's IP addres port 3306 using these certificates. For example, mysql -u admin -p"you-mysql-password" --ssl-ca=ca.pem --ssl-cert=client-cert.pem --ssl-key=client-key.pem.
    2. SSH: For each person using MySQL, they will need SSH access to the server. When granted, they may use tunneling of mysql port 3306 over SSH to access mysql at 127.0.0.1:3306. For example, to set up an SSH port forwarding on Mac or Linux, run ssh -L 3306:your-server:3306 your-server.
    3. VPN: If you connect to MySQL via the IP address of the server, using a VPN will ensure that communication with MySQL is encrypted. Note however that the traffic will be visible to those also on your VPN so make sure it's a trusted VPN only used by those who have permission to access the data.

    Resetting MySQL databases

    If you need to reset the mysql database, do the following: - stop the mysql docker instance: docker stop mysql - delete ./data/mysql - remove 'mysql-js' from T_MODULES - Run ./start.sh or ./develop.sh. This will remove the mysql-js module from enabledModules in the app couch database's modules doc. See "Disabling modules: mysql-js" in the console to confirm. - add 'mysql-js' to T_MODULES - this will init the mysql databases. - Run ./start.sh or ./develop.sh. This will add the mysql-js module from enabledModules in the app couch database's modules doc and create the databases. See "Enabling modules: mysql-js" in the console to confirm.

    Configuration

    • You may add configuration options to ./server/src/mysql-js/conf.d/config-file.js.
    • If you are using the mysql container and are having errors with very large forms, the new settings in ./server/src/mysql-js/conf.d/config-file.js should help. You will need to completely rebuild the mysql database. Stop the Tangerine and mysql containers. Delete (or -rename) the ./data/mysql directory.
      Then restart Tangerine using the ./start.sh or develop.sh script.
    • Important: If you already have a mysql instance running and don't want to rebuild the mysql database, delete the innodb-page-size=64K line from ./server/src/mysql-js/conf.d/config-file.js; otherwise, your mysql instance will not start.
    • If making changes to the innodb-page-size option, you must delete the ./data/mysql directory.

    Troubleshooting

    Issue: Data on the Mysql db is far behind the Couchdb.

    This scenario can happen when replicating data from a Production database on another server instance. Step to triage and resolve this issue:

    1. run docker ps -a to see if the tangerine and couchdb instances are up
    2. Bring back up those instance by using the start.sh script.
    3. Confirm using docker logs -f tangerine that the docker containers are back up and processing data correctly.
    4. If the server must catch up more than a day's worth of documents, use the wedge pre-warm-views at the end of the day to hit all views in the couchdb to pre-warm them (i.e. index those views).
    5. After the indexes have been built, use the mysql-report groupID command to see if the mysql and couchdb databases are caught up.
    \ No newline at end of file diff --git a/system-administrator/mysql-module/index.html b/system-administrator/mysql-module/index.html new file mode 100644 index 0000000000..ae90f30e8a --- /dev/null +++ b/system-administrator/mysql-module/index.html @@ -0,0 +1,11 @@ + MySQL Module (Legacy) - Tangerine Documentation

    MySQL Module (Legacy)

    Enabling MySQL module

    This module is the legacy module starting with the release of Tangerine v3.26.0. If you are running a version prior to v3.26.0, you should consider switching to the new MySQL-JS Module for improved performance.

    Important! If reenabling the mysql module, remove the mysql folder: rm -r data/mysql

    Step 1

    Ensure the variables from the MySQL section in config.defaults.sh are in your customized config.sh file.

    Step 2

    Ensure the T_MYSQL_PASSWORD variable is set to a sufficiently secure string. Failure to properly secure this password will without a doubt result in ransomware bots hacking your database.

    Step 3

    Add the mysql module to T_MODULES_ENABLED in config.sh.

    For example:

    T_MODULES="['csv','mysql']"
    +

    Step 4

    Run the start script to load in new configuration. Do this even if your server is already running. Note that restarting the container will not work, we have to run ./start.sh to recreate the container with the new configuration.

    ./start.sh <version>
    +

    Note: Upgrading an older version of Tangerine may require running docker exec tangerine push-all-groups-views after to enable indexes used for mysql

    Step 5

    Clear reporting cache to start generating a MySQL database for each group.

    docker exec tangerine reporting-cache-clear
    +

    You can check in on the progress of generating the mysql database using the mysql-report command. (Warning The mysql-report command creates a heavy workload to an instance so do not use it when mysql is trying to process a lot of data from couchdb. See the "Troubleshooting" section below.) It will return for each kind of case data and form, how many records are in the source database vs. how many have made it over to mysql. Note that if your system is under heavy load during the processing of this, this command may stress it out even more so it may be best to wait until you see a load of less than one using a tool like top or htop.

    docker exec -it tangerine bash 
    +mysql-report <groupId> | json_pp
    +

    Step 6

    In the reboot instruction in crontab that to starts Tangerine on reboot, add mysql container to the containers that start before tangerine and increase the sleep command to 60 seconds. Failure to implement this will result in tangerine failing to start on reboot.

    @reboot docker start couchdb mysql && sleep 60 && docker start tangerine
    +

    Also add a cron job to run mysql-report at 1 a.m every day - this will keep the mysql indexes current.

    # Run mysql-report at 1 a.m every day:
    +# 0 1 * * * docker exec tangerine mysql-report group-479f455e-b1bd-481b-8bd7-0d985a07431c
    +

    Step 7

    The most basic way to access MySQL would be to use the MySQL CLI.

    docker exec -it tangerine bash
    +mysql -u"$T_MYSQL_USER" -p"$T_MYSQL_PASSWORD" -hmysql
    +

    On the mysql command line, list the available databases using show databases;. Note how the database names are similar to the Group ID's these correspond with except with dashes removed. For example, if the group ID was group-abc-123, the corresponding MySQL database would be groupabc123. To select a database, type use <database ID>; then show tables; to list out the available tables.

    Step 8

    To set up remote encrypted connections to mysql, three options:

    1. TLS: In the tangerine/data/mysql/databases folder you will find files ca.pem, client-cert.pem, and client-key.pem. Distribute those files to your MySQL users so they may connnect to your server's IP addres port 3306 using these certificates. For example, mysql -u admin -p"you-mysql-password" --ssl-ca=ca.pem --ssl-cert=client-cert.pem --ssl-key=client-key.pem.
    2. SSH: For each person using MySQL, they will need SSH access to the server. When granted, they may use tunneling of mysql port 3306 over SSH to access mysql at 127.0.0.1:3306. For example, to set up an SSH port forwarding on Mac or Linux, run ssh -L 3306:your-server:3306 your-server.
    3. VPN: If you connect to MySQL via the IP address of the server, using a VPN will ensure that communication with MySQL is encrypted. Note however that the traffic will be visible to those also on your VPN so make sure it's a trusted VPN only used by those who have permission to access the data.

    Troubleshooting

    Issue: Data on the Mysql db is far behind the Couchdb.

    This scenario can happen when replicating data from a Production database on another server instance. Step to triage and resolve this issue:

    1. run docker ps -a to see if the tangerine and couchdb instances are up
    2. Bring back up those instance by using the start.sh script.
    3. Confirm using docker logs -f tangerine that the docker containers are back up and processing data correctly.
    4. If the server must catch up more than a day's worth of documents, use the wedge pre-warm-views at the end of the day to hit all views in the couchdb to pre-warm them (i.e. index those views).
    5. After the indexes have been built, use the mysql-report groupID command to see if the mysql and couchdb databases are caught up.
    \ No newline at end of file diff --git a/system-administrator/rescuing-a-full-Tangerine-tablet/index.html b/system-administrator/rescuing-a-full-Tangerine-tablet/index.html new file mode 100644 index 0000000000..872b2b192c --- /dev/null +++ b/system-administrator/rescuing-a-full-Tangerine-tablet/index.html @@ -0,0 +1,19 @@ + Rescuing a Full Tangerine Tablet - Tangerine Documentation

    Rescuing a Full Tangerine Tablet

    If you have a tablet that is no longer able to sync because the disk is full, you can pull files off it and install on a clean tablet.

    This is relevant only to tablets that are runing sqlite/sqlCypher.

    Listing files in private database dir

    Android databases are stored in a defined location. You may list them using the adb shell command.

    adb shell "run-as org.rti.tangerine ls -lsa -R /data/data/org.rti.tangerine/databases/"
    +

    If the package name of your Tangerine differs, substitute 'org.rti.tangerine' with your package name.

    This command should return something like the following:

    4 -rw-------  1 u0_a647 u0_a647       0 2021-11-04 08:15 shared-user-database
    +4 -rw-------  1 u0_a647 u0_a647       0 2021-11-04 08:15 shared-user-database-index
    +500 -rw-------  1 u0_a647 u0_a647  507904 2021-11-04 08:08 shared-user-database-mrview-058020864d38b2f7c7203401485e2f6d
    +3180 -rw-------  1 u0_a647 u0_a647 3248128 2021-11-04 08:10 shared-user-database-mrview-0b8e5d92430db6d71d1379d7c9862b79
    +500 -rw-------  1 u0_a647 u0_a647  507904 2021-11-04 08:06 shared-user-database-mrview-0c1a03125cc747770c5ae321753f4ec2
    +3156 -rw-------  1 u0_a647 u0_a647 3223552 2021-11-04 08:10 shared-user-database-mrview-34a25e481c98744e67427811b36567c4
    +2900 -rw-------  1 u0_a647 u0_a647 2961408 2021-11-04 08:13 shared-user-database-mrview-3d495638a28b32b444ca7a9452a9ec5e
    +2924 -rw-------  1 u0_a647 u0_a647 2985984 2021-11-04 08:12 shared-user-database-mrview-918a54f957639ea4a8d24e16c6d93f50
    +500 -rw-------  1 u0_a647 u0_a647  507904 2021-11-04 08:12 shared-user-database-mrview-a3b8826be74302784a7af55681039cac
    +744 -rw-------  1 u0_a647 u0_a647  757760 2021-11-04 08:08 shared-user-database-mrview-d121af6c2693286feb260c1843a65996
    +1344 -rw-------  1 u0_a647 u0_a647 1372160 2021-11-04 08:09 shared-user-database-mrview-e0db7a0ed91857c44dd6ad77ee5b37f4
    +2588 -rw-------  1 u0_a647 u0_a647 2641920 2021-11-04 08:07 shared-user-database-mrview-e4d1230d206245fc83e75ee420b1f31e
    +500 -rw-------  1 u0_a647 u0_a647  507904 2021-11-04 08:11 shared-user-database-mrview-fc95555b88f96506de12d279698695f2
    +72 -rw-------  1 u0_a647 u0_a647   69632 2021-11-04 08:04 tangerine-variables
    +

    In this example, the tablet was running with the "encryptionPlugin":"CryptoPouch" configuration in app-config.json. That switch configures the app to use CryptoPouch, which encrypts documents in the app's indexedb for storage and stores indexes using sqlCypher. This is why the shared-user-database is 0 but the indexes - such as shared-user-database-mrview-fc95555b88f96506de12d279698695f2 - are large.

    Pulling the files

    Once you list the files, you much change permissions on them and then pull them. Change the permissions (chmod 666) for each file you wish to transfer:

    adb shell "run-as org.rti.tangerine chmod 666 /data/data/org.rti.tangerine/databases/shared-user-database-mrview-e4d1230d206245fc83e75ee420b1f31e"
    +

    Once the permissions is changed, you may transfer the files:

    adb exec-out run-as org.rti.tangerine cat databases/shared-user-database-mrview-e4d1230d206245fc83e75ee420b1f31e > shared-user-database-mrview-e4d1230d206245fc83e75ee420b1f31e
    +

    After the files are transferred, you may reset the permissions:

    adb shell "run-as org.rti.tangerine chmod 600 /data/data/org.rti.tangerine/databases/shared-user-database-mrview-e4d1230d206245fc83e75ee420b1f31e"
    +

    Restoring a database

    In the docs/system-administrator/restore-from-backup.md, there are instructions in the section "Restoring backups onto a fresh Tangerine app installation" on how to restore a database. Those instructions show how to use Android File Transfer to move the database files to the clean Android device and restore the app using a fresh Tangerine installation.

    Database file analysis

    In the docs/system-administrator/restore-from-backup.md, there are instructions in the section "Viewing data from an encrypted backup" that show how to compile and use sqlcipher to view and fix a corrupt SqlCypher database.

    \ No newline at end of file diff --git a/system-administrator/restore-from-backup/index.html b/system-administrator/restore-from-backup/index.html new file mode 100644 index 0000000000..50945f7920 --- /dev/null +++ b/system-administrator/restore-from-backup/index.html @@ -0,0 +1,31 @@ + Restoring from a Backup - Tangerine Documentation

    Restoring from a Backup

    Backing up

    The Export Backup feature is available from the right-hand menu by selecting "Export Data."

    Backups come in two types: - tabs with in-app scryption: a single file per database - tabs with device encryption: one directory per database, each of which contain many files.

    The Export Backup screen displays the backup location, and if the device uses device encryption, an input where the user may modify the "Docs per backup file" parameter.

    Press "Export Data for all Users" to initiate export.

    It will display a status message after each database backup is saved. The backup files will be saved in the Documents/Tangerine/backups directory. Use Android File transfer tool to transfer the files. Save all of the database backup files or directories. There should be four databases backed up:

    • shared-user-database
    • users
    • tangerine-lock-boxes
    • tangerine-variables

    backup-files-listing

    The listing is similar when backing up an encrypted database; however, it backs up 4 files instead of 4 directories:

    backup-encrypted-log

    Restoring backups onto a fresh Tangerine app installation.

    This only works for sync-protocol-2.

    Connect the tablet to the pc with a USB cable. Use Android File transfer or Samsung Smart Switch to browse to the Documents/Tangerine/restore directory. Copy all database files or directories generated by the Export Data command from the pc to the restore directory.

    restore-files-listing

    Install the Tangerine app on the tablet. DO NOT do the initial device setup (language selection/enter admin password/etc); instead, press the "Restore Backup" button to start the restore process.

    Home restore button

    Read the instructions. When ready, press the "Restore Backup" button. It will display a confirmation prompt:

    Restore backup prompt

    If the device does not have a "Documents" directory in Internal Storage, it displays an error. The Troubleshooting section provides details about the error and its resolution:

    Restore backup prompt

    The restore feature logs the process for each database. After the databases have been restored, it initiates indexing of the databases:

    Restore backup prompt

    When restoring an encrypted database, indexing is not run due to the state of the application at this point of the installation. In this case, prepare to wait a few minutes or longer after restarting the app and logging in to allow it to index the home page. Once the home page is displayed, do a Sync to upload/download any updated files; this will also kick off indexing.

    backup-files-listing

    After the restore process is complete, click the context button (|||) to close Tangerine and launch it again to load with the restored databases.

    Restoring form history

    On a tablet, the history of every form response change is saved. An agglomeration of these changes is what is sync'd to the server. It is possible to view all of these changes using the tablet backup.

    After restoring the database, open the app in DevTools and in the javascript console enter the following commands:

    const db = await T.user.getUserDatabase()
    +// docId is the document _id of the document you are trying to get the history from.
    +let docId = 'uuid'
    +const diffs = await T.tangyForms.getDocRevHistory(docId)
    +// get diffs into copy buffer
    +copy(diffs)
    +

    If there were any conflicts in the doc, they should be in _conflicts.

    You may wish to list all issues:

    const issues = (await db.query('byType', {key: 'issue', include_docs: true}))
    +.rows
    +.map(row => row.doc)
    +.filter(issue => issue.resolveOnAppContexts && issue.resolveOnAppContexts.includes('CLIENT'))
    +

    Viewing data from an encrypted backup

    This is a deep dive - you probably don't need to do this.

    Ask user to go to Export Data and press "Export Data for all Users". The backup files will be saved in the Android/data/org.rti.tangerine/files directory. Transfer the shared-user-database file. Then ask the user to go to the About menu and read off the Device ID.

    In Fauxton, look up the device record in the group-uuid-devices database. Copy the value for the key property.

    Building SqlCipher on MacOSX:

    ```shell script git clone https://github.com/sqlcipher/sqlcipher.git cd sqlcipher/sqlcipher ./configure --enable-tempstore=yes CFLAGS="-DSQLITE_HAS_CODEC -I/usr/local/opt/openssl/include/" LDFLAGS="/usr/local/opt/openssl/lib/libcrypto.a"

    To use the compiled sqlcipher:
    +
    +```shell script
    +sqlcipher/sqlcipher ~/Downloads/shared-user-database
    +
    In the sqlcipher console, enter the key:

    PRAGMA key = 'secret-key-uuid-very-secret';
    +

    To list tables:

    .tables
    +
    Output: ```shell script attach-seq-store by-sequence local-store attach-store document-store metadata-store
    ## Recovering a corrupted database
    +
    +Open the database:
    +
    +```shell script
    +sqlcipher/sqlcipher ~/Downloads/shared-user-database
    +
    Enter the key:

    PRAGMA key = 'secret-key-uuid-very-secret';
    +

    Run a check on the database. It will probably return something like "database disk image is malformed", which is not terribly useful:

    sqlite>PRAGMA integrity_check;
    +

    Run the following commands to dump the sql and build a new database (kudos: https://blog.niklasottosson.com/databases/sqlite-check-integrity-and-fix-common-problems/):

    sqlite>.output backup.db
    +sqlite>.dump
    +sqlite>.quit
    +>sqlite3 database_fixed.db
    +sqlite>.read backup.db
    +sqlite>.quit
    +
    If there were no errors, you should be able to query database_fixed.db. If not, open backup.db in a text editor and rummage through the sql statements.

    \ No newline at end of file diff --git a/system-administrator/sync-protocol-1/index.html b/system-administrator/sync-protocol-1/index.html new file mode 100644 index 0000000000..e0a46926dc --- /dev/null +++ b/system-administrator/sync-protocol-1/index.html @@ -0,0 +1 @@ + Sync Protocol 1 - Tangerine Documentation

    Sync Protocol 1

    Sync protocol 1 is deprecated as of 12-15-2022. We strongly recommend using Sync protocol 2, which is more secure.

    Background

    Sync protocol 1 was the original sync protocol for Tangerine that features a one-way push sync to a server.

    Configuration

    config.sh

    • T_MODULES: Make sure that 'sync-protocol-2' is not listed in T_MODULES.
    • T_UPLOAD_TOKEN: The value for T_UPLOAD_TOKEN must match the value for 'uploadToken' in the group's app-config.json.
    • T_UPLOAD_WITHOUT_UPDATING_REV : A config.sh setting for use in high-load instances using sync-protocol-1. *** Using this setting COULD CAUSE DATA LOSS. *** This setting uses a different function to process uploads that does not do a GET before the PUT in order to upload a document. Please note that if there is a conflict it will copy the _id to originalId and POST the doc, which will create a new id. If that fails, it will log the error and not upload the document to the server, but still send an 'OK' status to client. The failure would result in data loss.

    app-config.json

    • uploadTokenThe value for 'uploadToken' must match the value for T_UPLOAD_TOKEN in config.sh.
    • uploadUnlockedFormReponses - when set to true this populates a list of doc_ids from responsesUnLockedAndNotUploaded view to be uploaded - even if doc.complete === false. This value is used mostly in projects Tangerine Class/Teach, where students are tested over time.

    Syncing

    Sync in sync-protocol-1 is very simple: if queries a view to check what docs satisfy this criteria: !doc.uploadDatetime || doc.lastModified > doc.uploadDatetime) If uploadUnlockedFormReponses is set, it includes docs where complete === false.

    \ No newline at end of file diff --git a/system-administrator/sync-protocol-2/index.html b/system-administrator/sync-protocol-2/index.html new file mode 100644 index 0000000000..f1490df8c1 --- /dev/null +++ b/system-administrator/sync-protocol-2/index.html @@ -0,0 +1 @@ + Sync Protocol 2 (two-way sync) - Tangerine Documentation

    Sync Protocol 2 (two-way sync)

    Background

    When Tangerine was originally created, devices would only use a one-way sync from the tablet to the server sync-protocol-1 doc. Sync Protocol 2 was developed to enable two-way sync; typically, all data from the tablet is pushed to the server, and - in order to conserve bandwidth - only data from some forms is pulled to the tablet.

    The form responses that are synced depend on which forms are configured for sync and limited to a grouping by the "location" field in that users' profile.

    For example: An installation has two Forms, Form A and Form B. Only Form A is configured to sync. User A who has "facility 1" assigned to them in their user profile creates a form response for Form A and Form B then initiates a sync to find that two form responses have been pushed up. User B has "facility 1" assigned to them in their user profile and initiates a sync to find they pulled down one form response for Form A that originated on User A's device. If User B modifies this form response, it will be pushed on the next sync and then later User A would pull down the change. Let's say there is a User C who is assigned to "facility 2" in their user profile. When they initiate a sync, they will not receive any form responses from the server because the server only has form responses from User A who is assigned to "facility 1".

    Enabling Sync Protocol 2 for new Groups

    Note: Sync Protocol 2 is usually automatically enabled for new groups; however, these instructions show how to manually configure it.

    1. Enable Sync Protocol 2 before creating a new group by editing config.sh by adding "sync-protocol-2" to T_MODULES.
    2. Create a new group.
    3. Define location list levels and content in Config -> Location List.
    4. Create a new form in Author -> Forms.
    5. Go to Deploy -> Device Users and create new Device Users.
    6. Go to Deploy -> Devices and create new Devices.
    7. Go to Deploy -> Releases and release the app.

    "syncProtocol":"2" Enables a "Device Setup" process on first boot of the client application. This requires you set up a "Device" record on the server. When setting up a Device record on the server, it will give you a QR code to use to scan from the tablet in order to receive its device ID and token.

    Upgrade an existing group to Sync Protocol 2

    If planning to use `"syncProtocol":"2" and a project already uses "centrallyManagedUserProfile" : true, remove "centrallyManagedUserProfile": true and configure the user profile's custom sync settings to push.

    Managing Data Conflicts

    Because we can sync data down to Devices, it's possible for two Devices to edit the same data between syncs. This causes a "Data Conflict". It's important for someone to monitor conflicts to ensure data integrity. Please refer to Managing Data Conflicts documentation.

    Modes and stages of Sync Protocol 2.

    There are two different modes of sync:

    • Initial device setup: After a device has been registered (on tablet), the initial sync is executed. The initial sync has 3 stages:
    • Pull: It pulls documents from the server in batches, set by initialBatchSize from app-config.json (default: 1000). No documents are pushed, since no data collection has happened so far.
    • Status uploaded: Status of this sync process is uploaded.
    • Database optimization: After this initial sync, database indexes are created an optimized, which takes a little while. These indexes are critical to the app's performance.

    • Routine sync: After a short period of data collection, the user executes an Online sync. This sync has 4 stages:

    • Pull: Pull any new or updated documents from the server.
    • Push: Pushes any documents created on the tablet
    • Status uploaded: Status of this sync process is uploaded.
    • Database optimization: Indexes are updated.

    Sync settings in app-config.json

    Here are the settings that may be modified in app-config.json for sync: - initialBatchSize = (default: 1000) Number of documents downloaded in the first sync when setting up a device. - batchSize (default: 200) - Number of documents downloaded upon each subsequent sync. - writeBatchSize = (default: 50) - Number of documents written to the tablet during each sync batch. - changes_batch_size = (default: 50) - Enables support for reducing the number of documents processed in the changed feed when syncing. This setting will help sites that experience crashes when syncing or indexing documents. Using this setting will slow sync times.

    If the tablet user logs in as "admin", she may access the "Admin Configuration" menu. The "Pull all docs from the server" feature enables "catching up" any documents that were missed in previous syncs. This resets a placeholder variable ("since") to 0, causing the replication API to replicate any documents or updated documents that are not on the tablet. This feature uses the "initialBatchSize" setting to download larger batches of documents.

    If users report errors during sync, consider reducing these settings. The "writeBatchSize" is the most critical setting because it manages how many documents are written to the database at a time. If the batch is too large, the sync may fail.

    Security

    A big part of using Sync Protocol 2 is embracing device configuration into the workflow. Sync Protocol 2 is more than just sync: it also provides the structure for encrypting a database. A mobile device must be registered in Tangerine before it may sync. The "Deploy / Devices" page enables registration of a device. Device registration creates a key (for encrypting the db), token(for sync authentication with the server), and other identifiers associated with that device. The admin then scans a QR code for the device's registration that installs the device record (which includes the key) in the device's tangerine-lock-boxes IndexedDB database (see LockBoxService), and is used to encrypt the Tangerine databases. A user's username and password are used to decrypt the lockbox. - See db.factory to see how a key is passed in to encrypt a db. - See sync.service to see how deviceToken is used for authentication in the syncSessionUrl.

    \ No newline at end of file diff --git a/system-administrator/sync-strategies/index.html b/system-administrator/sync-strategies/index.html new file mode 100644 index 0000000000..a317279b05 --- /dev/null +++ b/system-administrator/sync-strategies/index.html @@ -0,0 +1,8 @@ + Sync Strategies - Tangerine Documentation

    Sync Strategies

    Overview

    The choice of sync strategy impacts how Tangerine syncs with the server. If you configure a form to use two-way sync, it uses CouchDB replication; otherwise, it uses custom sync. How are these two types of sync different? - CouchDB replication: -- If there is conflicting data on the server, the document update fails and it creates a log of the conflict on the uploaded document -- It currently does not notify the tablet user that there was a conflict. The data on the server displays the previous data, not the new, conflict data. See below how to view the new, conflict data. -- Uses more bandwidth - Custom sync -- If there is conflicting data on the server, it overwrites the document and does not make a log of the conflict. It uses the pouchdb-upsert plugin to do the write. -- Uses less bandwidth

    How to tell if there are conflicts when using CouchDB replication?

    Add "conflicts=true" to the url if checking view curl, or in your application, add {conflicts: true} option when you get() it. It will list the conflicts:

    _conflicts:[
    +"29-0003a0b8af090d907efecde3aa121416",
    +"25-f712a217de615f44c66ddb16b1a53a19",
    +"14-bad1258430d22ad41dc9ce4123283c4f",
    +"5-3fcde4c45f910b7a0c541e837e4ffd3c"
    +]
    +

    Query the form using "rev" in the querystring to view the conflicted version.

    http://localhost:5984/group-58093841-eaeb-4e51-8675-29757d71fd35/3cec5368-7b89-43cd-9c59-bcd1584dd4ea?rev=5-3fcde4c45f910b7a0c541e837e4ffd3c
    +

    See https://pouchdb.com/guides/conflicts.html for more information.

    \ No newline at end of file diff --git a/system-administrator/tangerine-nginx-ssl/index.html b/system-administrator/tangerine-nginx-ssl/index.html new file mode 100644 index 0000000000..66e766c69b --- /dev/null +++ b/system-administrator/tangerine-nginx-ssl/index.html @@ -0,0 +1,26 @@ + Configuring Nginx as SSL proxy server for Tangerine - Tangerine Documentation

    Configuring Nginx as SSL proxy server for Tangerine

    Update

    Issue #3147 describes the start-ssl.sh script that automates installation of the SSL certificates as well as the letsencrypt-nginx-proxy-companion and nginx-proxy containers. The first time it runs it may error out - do a docker logs -f letsencrypt-nginx-proxy-companion to see error. If it does, restart the container (docker restart letsencrypt-nginx-proxy-companion).

    You may disregard the following notes - the new script supersedes them; however, they may have some useful information.

    Initial configuration

    First open config.sh and change the port mapping of Tangerine

    T_PORT_MAPPING="-p 8080:80"
    +

    Rebuild the container by running ./start.sh

    Pull nginx docker image and install certbot inside the nginx container

    If your container is not called tangerine adjust below. --link is to allow the ngin container to forward to the tangerine one

    docker pull nginx
    +docker run -p 80:80 -p 443:443 --link tangerine:tangerine --restart always --name nginx -d nginx
    +

    Go into the nginx container and execute the below commands

    docker exec -it nginx bash
    +
    +apt-get update && apt-get install certbot vim python3-certbot-nginx -y
    +

    Open the config file

    vi /etc/nginx/conf.d/default.conf
    +
    Adjust the server_name to your domain and the size of the client body

    server_name My.Domain.com;
    +client_max_body_size 0; 
    +

    Replace the location directive with the one below:

    location / {
    +            # First attempt to serve request as file, then
    +            # as directory, then fall back to displaying a 404.
    +            proxy_pass_header  Server;
    +            proxy_set_header   Host $http_host;
    +            proxy_redirect     off;
    +            proxy_set_header   X-Real-IP $remote_addr;
    +            proxy_set_header   X-Scheme $scheme;
    +            proxy_set_header X-Forwarded-Host $host:$server_port;
    +            proxy_set_header X-Forwarded-Proto https;
    +            proxy_pass         http://tangerine;
    +        }
    +
    Save the file and exit vi

    Now execute and follow the promtps for cerbot. It will fail but that's ok

    certbot --nginx
    +
    The above will generate the certificate according to My.Domain.com name given. Select options 1. My.Domain.com

    Reload the config by running

    nginx -s reload
    +

    Exit the nginx container and add some configuration for autmatic updates of certificates Execute crontab –e and add the line below

     0 3 * * * docker exec -it nginx certbot renew --post-hook "service nginx reload"
    +

    Optionally save your image to your docker images

    docker commit nginx nginx/nginx:configuredNginx
    +

    Point your DNS to the actual server

    \ No newline at end of file diff --git a/system-administrator/troubleshooting-android-devices/index.html b/system-administrator/troubleshooting-android-devices/index.html new file mode 100644 index 0000000000..3c31c67dfb --- /dev/null +++ b/system-administrator/troubleshooting-android-devices/index.html @@ -0,0 +1,16 @@ + Troubleshooting Android Devices - Tangerine Documentation

    Troubleshooting Android Devices

    Modify the app-config.json on a Tangerine APK Install

    If an Android device is having trouble, you may want to tweak the app-config.json settings on that specific device. Use the following commands to pull the app-config.json file off the device, modify it on your computer, and then push it back to the device.

    adb shell
    +run-as org.rti.tangerine
    +# Discover what the path to the release is (will be a datetime), then use for subsequent paths.
    +ls files/cordova-hot-code-push-plugin/
    +cd files/cordova-hot-code-push-plugin/2021.09.14-18.12.15/www/shell/assets
    +cp app-config.json /sdcard/Download/
    +exit
    +adb pull /sdcard/Download/app-config.json
    +# Time to modify app-config.json.
    +vim app-config.json
    +adb push app-config.json /sdcard/Download/
    +adb shell
    +run-as org.rti.tangerine
    +# Make sure to use that datetime path discovered earlier.
    +mv /sdcard/Download/app-config.json /data/user/0/org.rti.tangerine/files/cordova-hot-code-push-plugin/2021.09.14-18.12.15/www/shell/assets/
    +
    \ No newline at end of file diff --git a/system-administrator/upgrade-checklist/index.html b/system-administrator/upgrade-checklist/index.html new file mode 100644 index 0000000000..24096b2b38 --- /dev/null +++ b/system-administrator/upgrade-checklist/index.html @@ -0,0 +1 @@ + Upgrade checklist - Tangerine Documentation

    Upgrade checklist

    Test on QA server

    • Clone production server to QA server.
    • Remove all cronjobs that may be pushing to remote git repositories.
    • Remove all CouchDB replications that may be pushing data to remote CouchDBs.
    • Update T_HOSTNAME in tangerine/config.sh.
    • Run ./start.sh <currently used version of tangerine>. This puts the updated config in config.sh into the container.
    • Update serverUrl in all app-config.json files for each group in tangerine/data/group/<your-group-id>/client/app-config.json. Find and replace from the tangerine folder by modifying the following command: find ./data/groups/ -type f -name "app-config.json" -print0 | xargs -0 sed -i '' -e 's/production-server-hostname/qa-server-hostname/g'
    • Release all APKs and PWAs. This puts all updated app config into the APKs and PWAs.
    • Install and set up PWA/APKs for groups to test.
    • Upgrade the QA server following the server instructions for the release you are upgrading to in CHANGELOG.md (https://github.com/Tangerine-Community/Tangerine/blob/master/CHANGELOG.md). Note that you must upgrade incrementally between the versions. If you skip one you may miss important updates or they may not apply correctly and you risk corrupting your install without knowing it.
    • Test functionality on the server.
    • Release updated APKs and PWAs.
    • Upgrade test tablet and test functionality.

    Deploy to production

    • Make backup of production server.
    • Release PWAs/APKs on the test channels for all groups.
    • Install and set up PWA/APKs for groups to test using APK/PWA on the test channel.
    • Upgrade the QA server following the server instructions for the release you are upgrading to in CHANGELOG.md (https://github.com/Tangerine-Community/Tangerine/blob/master/CHANGELOG.md). Note that you must upgrade incrementally between the versions. If you skip one you may miss important updates or they may not apply correctly and you risk corrupting your install without knowing it.
    • Test functionality on the server.
    • Release updated APKs and PWAs on the test channel.
    • Upgrade test tablet and test functionality.
    • Release updated APKs and PWAs on the live channel.
    • Note the Build ID of the APK or PWA on the live channel, then in the content repository tag a release with a corresponding Build ID in git. If using Github, use the Releases feature and note the version of Tangerine upgraded to along with any other notes.
    • Terminate the QA server.
    \ No newline at end of file diff --git a/system-administrator/upgrade-instructions/index.html b/system-administrator/upgrade-instructions/index.html new file mode 100644 index 0000000000..e87f15c1d4 --- /dev/null +++ b/system-administrator/upgrade-instructions/index.html @@ -0,0 +1,24 @@ + Upgrade instructions - Tangerine Documentation

    Upgrade instructions

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    Preparation

    Tangerine v3 images are relatively large, around 12GB. The server should have at least 20GB of free space plus the size of the data folder. Check the disk space before upgrading the the new version using the following steps:

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space.
    +df -h
    +

    If there is less than 20 GB plus the size of the data folder, create more space before proceeding. Good candidates to remove are: older versions of the Tangerine image and data backups.

    # List all docker images.
    +docker image ls
    +# Remove the image of the version that is not being used.
    +docker rmi tangerine/tangerine:<unused_version>
    +# List all data backups.
    +ls -l data-backup-*
    +# Remove the data backups that are old and unneeded.
    +rm -rf ../data-backup-<date>
    +

    Upgrade

    After ensuring there is enough disk space, follow the steps below to upgrade the server.

    1. Backup the data folder

      # Create a backup of the data folder.
      +cp -r data ../data-backup-$(date "+%F-%T")
      +

    2. Confirm there is no active synching from client devices

    Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server.

    docker logs --since=60m tangerine
    +
    1. Install the new version of Tangerine
      # Fetch the updates.
      +git fetch origin
      +# Checkout a new branch with the new version tag.
      +git checkout -b <new_version> <new_version>
      +# Run the start script with the new version.
      +./start.sh <new_version>
      +

    Clean Up

    After the upgrade, remove the previous version of the Tangerine image to free up disk space.

    docker rmi tangerine/tangerine:<previous_version>
    +
    \ No newline at end of file diff --git a/whats-new/index.html b/whats-new/index.html new file mode 100644 index 0000000000..7335912349 --- /dev/null +++ b/whats-new/index.html @@ -0,0 +1,1384 @@ + What's new - Tangerine Documentation

    What's new

    v3.31.1

    General Updates

    • Allow mysql outputs of TANGY-TIMED and TANGY-UNTIMED-GRID data

    Administration

    • The reporting-cache-clear script will honor the environmnt variable T_ONLY_PROCESS_THESE_GROUPS to limit the groups processed
    • Set T_ONLY_PROCESS_THESE_GROUPS to a comma-separated list of group names to limit the groups cleared and then processed by the script

    Fixes - Fixes for editing of Form Responses in the server web UI * Edits of Attendence, Behavior, and Scoring are currently prohibited in the server web UI * Verified and Archived Form Responses must be Unverified and Unarchived before editing is available - Teacher Dashboard Scoring: Fix issues with custom scoring - Fix output of Case Participants to mysql - Fix online survey release

    Libs and Dependencies - Bump version of tangy-form to v4.54.4 * Fix check for 'readOnly' input metadata * Fix undefined access of input without tagName * Fix missing function parens

    Server upgrade instructions

    See the Server Upgrade Insturctions.

    Special Instructions for this release: N/A

    v3.31.0

    New Features

    • Audio and Visual Feedback: A new Prompt Box widget available in form authoring allows a form designer to add audio and visual feedback connected to Radio Block widgets. This feature provides a toolset for creating self-guided assessments. See the example in tangy-forms. #3473

    • Client Login Screen Custom HTML: A new app-config.json setting, customLoginMarkup, allows for custom HTML to be added to the login screen. This feature is useful for adding custom branding or additional information to the login screen. As an example:

      "customLoginMarkup": "<div style='text-align: center;'><img src='assets/media/logo.png' alt='logo' style='max-width: 100%;'></div>"
      +

    • Improved Data Management:

    • Data Managers now have access to a full workflow to review, edit, and verify data in the Tangerine web server. The Data Manager can click on a record and enter a new screen that allows them to perform actions align with a data collection supervision process.
    • Searching has been improved to allow seaqrching for a specific ID in the imported data. This feature is useful for finding specific records synced to the server when reviewing or editing completed form responses. #3681

    Fixes - Client Search Service: exclude archived cases from recent activity - Media library cannot upload photos #3583 - User Profile Import: The process of importing an existing device user now allows for retries and an asynchronous process to download existing records. This fixes an issue cause by timeouts when trying to import a user with a large number of records. #3696 - When T_ONLY_PROCESS_THESE_GROUPS has a list of one or more groups, running reporting-cache-clear will only process the groups in the list

    Tangerine Teach

    • Add toggle in Attendence Check for 'late'. A teacher can click through the status of 'present', 'late', or 'absent' for each student.
    • Use studentRegistrationFields to control showing name and surname of student in the student dashboard

    Libs and Dependencies - Bump version of tangy-form to v4.31.1 and tangy-form-editor to v7.18.0 for the new Prompt Box widget - Bump version of tangy-form to v4.45.1 for disabling of tangy-gps in server edits

    Server upgrade instructions

    See the Server Upgrade Insturctions.

    Special Instructions for this release:

    Once the Tangerine and CouchDB are running, run the upgrade script for v3.31.0:

    docker exec -it tangerine /tangerine/server/src/upgrade/v3.31.0.js

    v3.30.2

    New Features

    • Customizable 'About' Page on client #3677 -- Form developers can create or update a form with the id 'about'. There is an example form in the Content Sets -- The form will appear in the 'About' page on the client

    General Updates - Password Visibility -- the login and register screen on the client shows an 'eye' icon used to hide or show passwords - Re-organization of the client app menu - Reintroduce registrationRequiresServerUser app config setting to make managing central user more flexible - use registrationRequiresServerUser to require an import code when registering users on the client - use centrallyManagedUserProfile to require an import code AND only allow changes to the user profile on the server - use hideProfile to hide the manage user profile page from on the client

    Teach Module Updates - Behavior screen show a link instead of a checkbox to access the Behavior form - Hint text added to attendance, behavior, and scoring tables - Improved save messaging for attendance and scoring - In Attendance Reports: - add start and end dates to view a custom date range report - Fix the names not displaying in the tables

    Fixes - Get Media Uploads working in Editor #3583 - CSV Generation broken with 'doLocalWorkaround is undefined' error

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. 
    +df -h
    +# If there is not more than 12 GB plus the size of the data folder, create more space before proceeding. 
    +# Good candidates to remove are: data back-up folders and older versions of the Tangerine image
    +# rm -rf ../data-backup-<date>
    +# docker rmi tangerine/tangerine:<version>
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout -b v3.30.1 v3.30.1
    +./start.sh v3.30.2
    +# Run the update to copy the new About page to all groups on your site.
    +docker exec -it tangerine /tangerine/server/src/upgrade/v3.30.2.js
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:<previous_version>
    +

    v3.30.1

    New Features - Multiple Location Lists can be configured using the Tangerine server web interface -- Create and manage location lists for use in Tangerine forms -- The default location list is used for device and device user assignment. - The app-config.json teachProperties has new properties, "unitDates" and "studentRegistrationFields":

    "unitDates": [{"name": "Unidad 1","start": "2023-02-15", "end": "2023-04-23"}, {"name": "Unidad 2","start": "2023-04-24", "end": "2023-06-30"}], 
    +"studentRegistrationFields": ["student_name", "student_surname", "phone", "classId"]
    +
    The unitDates property is used to configure the dates for each unit in the Class module. The studentRegistrationFields property is used to configure the fields from the Student Registration form to be saved in the class attendance, behavior, and score register and CSV's. - The app-config.json teachProperties has a new property, "showAttendanceCalendar", which enables the Attendance Calendar in the Class module when set to true. - Intl/locale support in Class: The class module currently supports the es-gt locale. Add additional locales in class/module.ts:
    import { registerLocaleData } from '@angular/common';
    +import localeEsGt from '@angular/common/locales/es-GT';
    +registerLocaleData(localeEsGt);
    +
    - The "Request spreadsheets" CSV output form now has three new forms to view if useAttendanceFeature is set to true in app-config.json: Attendance, Behavior, and Score

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. 
    +df -h
    +# If there is not more than 12 GB plus the size of the data folder, create more space before proceeding. 
    +# Good candidates to remove are: data back-up folders and older versions of the Tangerine image
    +# rm -rf ../data-backup-<date>
    +# docker rmi tangerine/tangerine:<version>
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout -b v3.30.1 v3.30.1
    +./start.sh v3.30.1
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:<previous_version>
    +

    v3.30.0

    New Features

    • The 'teach' content-set now supports an optional 'Attendance' feature, enabled by adding "useAttendanceFeature": true and "homeUrl": "attendance-dashboard" to app-config.json. It also has a new Class/Attendance menu which enables collection of those values per student, and an 'Attendance' report.
    • The Attendance records generate _id's based on the grade, curriculum, user, and date and time of the record, so that they can be sorted chronologically. See dashboard.service generateSearchableId for details.
    • Class now supports eventFormRedirect to redirect to different url after submit: on-submit="window.eventFormRedirect =/attendance-check"
    • New app-config.json configuration for teach properties: ```js "teachProperties": { "units": ["Unidad 1", "Unidad 2"], "attendancePrimaryThreshold": 80, "attendanceSecondaryThreshold": 70, "scoringPrimaryThreshold": 70, "scoringSecondaryThreshold": 60, "behaviorPrimaryThreshold": 90, "behaviorSecondaryThreshold": 80, "useAttendanceFeature": true } The PrimaryThreshold and SecondaryThreshold values are used to determine the color of the cell in the reports.

    • Updated docker-tangerine-base-image to v3.8.0, which adds the cordova-plugin-x-socialsharing plugin and enables sharing to WhatsApp.

    Fixes - Fixed PWA assets (sound,video) only work when online #1905

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade. 
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout -b v3.30.0 v3.30.0
    +./start.sh v3.30.0
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.29.1
    +

    v3.29.1

    Fixes

    • Fix undefined referencein markQualifyingEventsAsComplete

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. 
    +df -h
    +# If there is not more than 12 GB plus the size of the data folder, create more space before proceeding. 
    +# Good candidates to remove are: data back-up folders and older versions of the Tangerine image
    +# rm -rf ../data-backup-<date>
    +# docker rmi tangerine/tangerine:<version>
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout -b v3.29.1 v3.29.1
    +./start.sh v3.29.1
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:<previous_version>
    +

    v3.29.0

    New Features - Case, Event and Form Archive and Unarchive

    We have released an update to Tangerine which allows for the archiving and un-archiving of both events, and forms within events. This is an extension of the already existing functionality by which an entire case can be archived. The purpose of this is to empower data management teams using Tangerine to "clean up" messy cases where extraneous data has been added to a case in error, or by a conflict situation. The purpose of this document is to summarize both the configuration to enable this, and to demonstrate the use of these functions. This functionality will only apply to the web-based version of Tangerine, and will not be available on tablets.

    Package Updates - Updated tangy-form to v4.40.0

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. 
    +df -h
    +# If there is not more than 12 GB plus the size of the data folder, create more space before proceeding. 
    +# Good candidates to remove are: data back-up folders and older versions of the Tangerine image
    +# rm -rf ../data-backup-<date>
    +# docker rmi tangerine/tangerine:<version>
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout -b v3.29.0 v3.29.0
    +./start.sh v3.29.0
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:<previous_version>
    +

    v3.28

    • This became v4

    v3.27.8

    New Features - New server configuration setting for output value of optionally not answered questions - The value set in the config variable T_REPORTING_MARK_OPTIONAL_NO_ANSWER_WITH in config.sh will be the value of questions that are optional and not answered by the respondent. - The default value is "SKIPPED" for consistency with previous outputs - CSV outputs now include the metadata variables startDateTime and endDateTime auto-calculated from the startUnixTime and endUnixTime variables - Additional parameter for the csv data set generation process to ignore user-profile and reports from the output csv files

    Fixes - Copy all media directories from the client form directories to ensure assets are available in online surveys - Allows form developers to publish images and sounds in online surveys - Fix the language dropdown in online surveys - Outputs will no longer try to process outputs for TANGY-TEMPLTE inputs

    Breaking Changes - Removes build dependencies for legacy python mysql output module - For those using the legacy module, see the documentation move to the new mysql-js module

    Package Updates - Lock @ts-stack/markdown to 1.4.0 to prevent breaking of builds

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade. 
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout -b v3.27.8 v3.27.8
    +./start.sh v3.27.8
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.27.7
    +

    v3.27.7

    Fixes - Enable mysql-js module outputs for online-survey app data

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout -b v3.27.7 v3.27.7
    +./start.sh v3.27.7
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.27.6
    +

    v3.27.6

    Fixes - Address issues using the CaseService createCaseEvent API in on-submit logic by making the function synchronous

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.27.6
    +./start.sh v3.27.6
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.27.5
    +

    v3.27.5

    Fixes - CSV Generation: Fix permissions on generate csv batch script

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.27.5
    +./start.sh v3.27.5
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.27.4
    +

    v3.27.4

    Fixes - Synchronization: Update Reduce Batch Size button to apply during normal sync for pull and push

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.27.4
    +./start.sh v3.27.4
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.27.3
    +

    v3.27.3

    Fixes - Fix running the reporting-cache-clear command on the mysql-js module - Extend the particpantID key to 80 chars to handle long keys for T_MYSQL_MULTI_PARTICIPANT_SCHEMA - For those using mysql-js: This change requires running reporting-cache-clear to take effect. - Fix missing groupId in user-profile PR: #3494 - This bugfix added groupId to the user-profile. - In mysql-js, it also throws an error when groupId is missing. Relevant commit. This is different from earlier behavior, which lets the document pass without an error. All docs should have a groupId.

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.27.2
    +./start.sh v3.27.2
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.27.2
    +

    v3.27.2

    Fixes - Tangerine on Android APK ignore requestFullscreen() #3539 - This fix above also adds a new app-config.json property - exitClicks - enables admin to set number of clicks to exit kioskMode. - Fixed: Tangy-radio button and tangy keyboard do not render on Online survey #3551

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.27.2
    +./start.sh v3.27.2
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.27.1
    +

    v3.27.1

    Fixes - Limit debugging logs in csv generation to prevent exec from hitting max_buffer issue - Add protection to use of onEventOpen and onEventClose API

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.27.1
    +./start.sh v3.27.1
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.27.0
    +

    v3.27.0

    NEW Features - Update triggers for Case API hooks

    The Case Service API has a set of functions that are triggered on events. An implementer of Tangerine using the Case module can add these triggers to their case definition json in order to hook into these actions. The variable name to add is the trigger, e.g. onCaseOpen and the value is valid javascript that will run when the trigger is fired. The following changes were made to the hooks:

    -- onCaseClose hook will fire when the case is closed by the user by clicking the 'X' in the upper-left corner -- onCaseOpen hook will fire when the case is opened (no change) -- onEventOpen has been changed to fire when a Case Event is clicked on the Case Summary page -- onEventClose has been changed to fire when the Event page is closed -- onEventCreate has been added and will fire when the user creates a new event using the dropdown in the Case Summary page -- onEventFormOpen has been added and will fire when the user opens a form from the Event page -- onEventFormClose has been added and will fire when the user closes a form

    Fixes - Check for custom search js when creating user dbs (Tangerine Preview)

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.27.0
    +./start.sh v3.27.0
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.26.2
    +

    v3.26.2

    DEPRECATION NOTICE AND UPCOMING MODULE DELETION

    The mysql module is deprecated; it will be removed soon from this source code in the v3.28.0 release. We have been using the mysql-js in production for a few months and it is more performant and reliable than the output of the mysql module. We recommend switching to the mysql-js module. See the MySQL-JS Module doc for upgrade and configuration information.

    New Features - New group content-set dropdown: PR 3275 - https://github.com/Tangerine-Community/Tangerine/pull/3275 - enables a content-set dropdown and is already in main. Modify the template (content-sets-example.json) and rename to content-sets.json to enable the dropdown.

    Fixes - Created Feedback dialog to resolve layout issue on mobile devices PR: #3533 - Fix for Class listing breaks if you archive all classes in Teach; unable to add new classes. Issue: #3491 - Fix for Mysql tables not populating; ER_TOO_BIG_ROWSIZE error in Tangerine logs. Issue: #3488 - Changed location of mysql-js config file to point to the mysql-js directory. Also increased memory parameters in conf.d/config-file.cnf. - If you are using the mysql container and are having errors with very large forms, the new settings in ./server/src/mysql-js/conf.d/config-file.js should help. You will need to completely rebuild the mysql database. See the "Resetting MySQL databases" section in the MySQL-JS Module docs. - Important: If you already have a mysql instance running and don't want to rebuild the mysql database, delete the innodb-page-size=64K line from ./server/src/mysql-js/conf.d/config-file.js; otherwise, your mysql instance will not start. - Fix for CSV Download fails with larger forms. Issue: #3483

    Backports

    The following feature was backported from v3.24.6 patch release:

    • T_UPLOAD_WITHOUT_UPDATING_REV : A new config.sh setting for use in high-load instances using sync-protocol-1. *** Using this setting COULD CAUSE DATA LOSS. *** This setting uses a different function to process uploads that does not do a GET before the PUT in order to upload a document. Please note that if there is a conflict it try to POST the doc which will create a new id and copy the _id to originalId. If that fails, it will log the error and not upload the document to the server, but still send an 'OK' status to client. The failure would result in data loss.

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.26.2
    +./start.sh v3.26.2
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.26.1
    +

    v3.26.1

    NEW Features - New configuration parameter: T_LIMIT_NUMBER_OF_CHANGES - Number of change docs from the Couchdb changes feed queried by reporting-worker (i.e. use as the limit parameter). Default: 200. - Added volume mapping for translations dir in start script. - A new mysql-js module replaces the old mysql module. Documentation is here. The new mysql-js module is faster and more accurate than the old mysql module. It no longer uses an intermediate "group-uuid-mysql" couchdb; instead, it reads from the _changes feed and writes directly to a MySql database. To use the new module, add mysql-js to the T_MODULES list of modules and configure the following settings: - T_MYSQL_CONTAINER_NAME="mysql" # Either the name of the mysql Docker container or the hostname of a mysql server or AWS RDS Mysql instance. - T_MYSQL_USER="admin" # Username for mysql credentials - T_MYSQL_PASSWORD="password" # Password for mysql credentials - T_USE_MYSQL_CONTAINER="true" # If using a Docker container, set to true. This will automatically start a mysql container when using a Tangerine launch script.

    Fixes - Student subtest report incorrect for custom logic inputs #3464 - Init paid-worker file when server restarted. - Fix bug in start.sh script for --link option - Rename T_REBUILD_MYSQL_DBS to T_ONLY_PROCESS_THESE_GROUPS. Configure T_REBUILD_MYSQL_DBS to list group databases to be skipped when processing data through modules such as mysql and csv.

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.26.1
    +./start.sh v3.26.1
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.26.0
    +

    v3.26.0

    NEW Features

    • MySQL JS Module: -- Track and output changes through the CouchDB Changes Feed -- Connect to a MySQL Server of your choice via a url and credentials
    • Add app-config flag to force confirmation of each form response created on the client
    • Update to tangy-form and tangy-form-editor which enables configuration of automatic scoring in Editor for groups using Class. Issue: #1021
    • Documented a list of Reserved words in Tangerine
    • Bump docker-tangerine-base-image to v3.7.4 (enables RECORD_AUDIO permission for APK's), tangy-form to 4.38.3, tangy-form-editor to 7.15.4.

    Fixes

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.26.0
    +./start.sh v3.26.0
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.26.0
    +

    v3.25.1

    Fixes

    • Fix logic in has merge change permissions

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.25.1
    +./start.sh v3.25.1
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.25.1
    +

    v3.25.0

    NEW Features

    • Improvements to Issues on the Client and Server 3413 -- Add app-config flag to allow client users to Commit changes to Issues -- Add user-role permissions to select which events or forms Issue changes can be commited on the client -- Pull form responses changed in Issues on the server down to the client
    • Add parameter to CSV Dataset Generation that allows exclusion of archived form definitions

    Fixes

    • Apply isIssueContext correctly on the client

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.25.0
    +./start.sh v3.25.0
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.25.0
    +

    v3.24.6

    NEW Features

    • T_UPLOAD_WITHOUT_UPDATING_REV : A new config.sh setting for use in high-load instances using sync-protocol-1. *** Using this setting COULD CAUSE DATA LOSS. *** This setting uses a different function to process uploads that does not do a GET before the PUT in order to upload a document. Please note that if there is a conflict it will copy the _id to originalId and POST the doc, which will create a new id. If that fails, it will log the error and not upload the document to the server, but still send an 'OK' status to client. The failure would result in data loss.

    v3.24.4

    NEW Features

    • Ability to add scoring from the interface (24 hours) #1021

    Fixes

    • User is forced to stay on form until submission [#3215] - changed current-form-id to incomplete-response-id
    • Bumped tangy-form to 4.37.0 and tangy-form-editor to 7.14.11.

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.24.4
    +./start.sh v3.24.4
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.24.3-final
    +

    v3.24.3-final

    Please note that this release is tagged v3.24.3-final, not v3.24.3. This is a deviation from our usual format; we will resume the previous format in the next release.

    New Feature - To force user to stay on form until submission, set `"forceCompleteForms":true' in the group app-config.json. Issue: #3215

    Fixes - Separate Archived and Active forms in Request Spreadsheet screen #3222 - When incomplete results upload is enabled on a group, do not save empty record when using sync-protocol 1. #3360 - Enable editing the "No" confirmation alert for tangy-consent #3025 - Fix Sync error caused by async directory error when creating media directories #3374 - Exclude client-uploads folder from APK and PWA releases #3371 - Remove ordering of inputs when creating spreadsheets #3252 - Bumped tangy-form-editor to v7.14.8 to add Video Capture input warning text #3376 - Bumped tangy-form to 4.36.3. - Updated the online-survey-app routing to route to a specific form, and also adds an optional routing option. PR:#3387

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.24.3-final
    +./start.sh v3.24.3-final
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.24.2
    +

    v3.24.2

    Fixes - Hide Case Events form the Schedule View that are 'inactive' - Add the 'endUnixTimestamp' to mysql outputs generated by the python module - Remove unnecessary and expensive query for conflicts during synchronization on the client PR: #3365

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.24.2
    +./start.sh v3.24.2
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.24.1
    +

    v3.24.1

    New Features

    • Feature: New version of mysql module, called mysql-js, which is coded in javascript instead of python. This module exports records much faster than previous version. It should also use much less memory and provide more flexibility in terms of column data types and (eventually) support of different types of databases. Issue: #3047
    • Feature: Enable upload of files created by the tangy-photo-capture and tangy-video-capture inputs. PR: #3354 Note: In order to cause minimal negative impact upon current projects, the default behavior will be to save image files created by the tangy-photo-capture input to the database, instead of saving to a file and uploading. That being said, it is preferable to save as a file and upload. To over-ride this default, set the new mediaFileStorageLocation property to 'file' in the group's app-config.json. The default is 'database'. If this property is not defined, it will save to the database. New groups will be created with mediaFileStorageLocation set to 'file'. Videos created using the tangy-video-capture input will always be uploaded to the server due to their large file size.

    Fixes - The default password policy (T_PASSWORD_POLICY in config.sh) has been improved to support most special characters and the T_PASSWORD_RECIPE description has been updated to list the permitted special characters. Issue: https://github.com/Tangerine-Community/Tangerine/issues/3299

    Example:

    (\` ~ ! @ # $ % ^ & * ( ) \ - _ = + < > , . ; : \ | [ ] { } )
    +
    • Enable forms without location to be viewed in visits listing. PR: #3347
    • Fix results with cycle sequences that do not generate a CSV file. Issue: #3249 PR: 3345
    • Enable grids to be hidden based on skip logic #1391
    • Add confirmation to consent form if 'No' selected before the form is closed #3025. Activate this feature using the new property: confirm-no="true".
    • Fix app config doNotOptimize logic PR: #3358
    • Those using the doNotOptimize flag must reverse the logic in the appConfig.sh file when updating to this version

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.24.1
    +./start.sh v3.24.1
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.24.0
    +

    v3.24.0

    New Features

    • App user can reduce the batch size as a workaround when experiencing 'out-of-memory' errors with Rewind Sync
    • On occasion Rewind Sync will fail to complete due to 'out-of-memory' issues. The Advanced Sync section of the Sync page now shows a checkbox that when checked will reduce the batch sizes used to perform the Rewind Sync. As noted in the UI, the Rewind Sync will take longer to process however in most cases it will be able to complete the process. The batch size reduction will be reverted once the Rewind Sync is complete or the user unchecks the box.
    • System Admin can scan a QR code to download an APK or PWA release
    • When deploying Tangerine, it can be a long process to download and install the APK or PWA on multiple devices. To improve the deployment process, the Release tables now show a QR code that when scanned will download the release directly to a new device without the need to type in the URL.
    • Improvements to Sync: In case of false positives on push, keep pushing until nothing is pushed
    • Client Case Service API:
    • Case Event and Event Form (De)activation 3334
      • activateCaseEvent: Marks a Case Event as 'active' and shows it in the Case Event list
      • deactivateCaseEvent: Marks a Case Event as 'inactive' and hides it in the Case Event list
      • activateEventForm: Marks an Event Form as 'active' and shows it in the Event Form list
      • deactivateEventForm: Marks an Event Form as 'inactive' and hides it in the Event Form list
    • Editor Case Service API:
    • Add useful APIs used in the client Case Service API to the editor Case Service API 3325
    • Option to sync a case before viewing it 3237
    • Support for showing photo and signatures in Issues

    Fixes

    • Fix after update messaging and async issues

    Translations - Include Vietnamese translations

    Deprecations - Comparison Sync has been removed from this release to reduce confusion reported by Tangerine users. The Rewind Sync functionality out-performs Comparison Sync and is recommended for use when needed on all deployments.

    v3.23.1

    Fixes

    • Fixed bug in sync on PWA's. Also, do note that video file upload using the new tangy-form input <tangy-video-capture> only works for APKs Issue: [#3338] https://github.com/Tangerine-Community/Tangerine/issues/3338
    • Fixed tangy-form-editor to 7.14.2 to fix bug with input widget for tangy-keyboard-input (postfix field).

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.23.1
    +./start.sh v3.23.1
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.23.0
    +

    v3.23.0

    New Features

    • Enabled video upload feature which uses the new tangy-form input <tangy-video-capture>. There is a new video file upload section in the sync feature for both sync-protocol 1 and 2. implementation details in this PR: 3327. Issue: #3212 The new tangy-form input <tangy-video-capture> takes the following properties:
    • frontCamera: Boolean. Whether to use the front camera or the back camera. Default is true.
    • noVideoConstraints: Boolean. Whether to force use of front or back camera. If true, chooses the first available source. Default is true.
    • codec: String. The codec to use. Default is 'video/webm;codecs=vp9,opus' - AKA webm vp9. It is possible the device may not support all of these codecs. Other potential codecs include video/webm;codecs=vp8,opus and video/webm;codecs=h264,opus.
    • videoWidth: Number. The width of the video. Default is 1280 and videoHeight: Number. The height of the video. Default is 720.
    • Bump tangy-form lib to 4.34.3, tangy-form-editor to 7.14.1.

    Fixes

    • Add postfix property to tangy-keyboard-input. Also add highlight to value entered. Issue: 3321

    v3.22.4

    New Features

    • Feature: Tangerine CLI for dropping mysql tables and resetting mysql .ini files. PR: #3281 Usage: docker exec tangerine module-cache-clear mysql

    Fixes

    • Error when mysql module creates a table with duplicate participantId PR: #3279
    • New languages - Bengali, Dari, Hindi, Pashto, Portuguese, Updated Russian, Swahili, Urdu #3263
    • Filter archived case events out of Schedule View #3267
    • Many fixes to Teach:
    • Add Current Date to Teach Subtest Report #3273
    • Remove some appended Teach CSV columns #3271
    • Fix student subtest report failing by transforming data only for related curriculum #3272
    • Student subtask report is failing with error #3270
    • CSV file contains tangy-input metadata and displaces all inputs #3227
    • Fix summary upload #3265
    • Records should be one doc per Student per Curriculum per Class. Not per Student per Curriculum per Class per Item. #3264
    • Provide Bengali number translation in Student Grouping Report #3255
    • Bengali numbers are not being replaced in Class Grouping report #3228

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.22.4
    +./start.sh v3.22.4
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.22.3
    +

    v3.22.3

    Fixes

    • Fix all Tangy Templates are missing when reviewing completed form responses.

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.22.3
    +./start.sh v3.22.3
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.22.2
    +

    v3.22.2

    Fixes

    • Download All button on Spreadsheet Request info page does not download #3232
    • Spreadsheet Requests page does not load for new groups #3233
    • Fix use of window.eventFormRedirect #3211
    • Spreadsheet Request will fail to generate Download All zip if one form has specific characters in the title #3217

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.22.2
    +./start.sh v3.22.2
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.22.1
    +

    v3.22.1

    Fixes

    • Fix: Tangy Template elements all say "false" if using environment variables like caseService and T #3203
    • Make issue diffs less crash prone #3200
    • Fix: Case fails to open after selecting Case in search behind a "load more" button #3194
    • Fix: Unable to scroll to last item in search list if there is not more button #3195
    • Fix: After typing a search, "load more" button appears with no search results for a few seconds #3196
    • On a Spreadsheet Request page, style the download all button's icon as white.
    • Unify and fix the exclude pii label on spreadsheet requests.

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Check logs for the past hour on the server to ensure it's not being actively used. Look for log messages like "Created sync session" for Devices that are syncing and "login success" for users logging in on the server. 
    +docker logs --since=60m tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.22.1
    +./start.sh v3.22.1
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.22.0
    +

    v3.22.0

    Fixes

    • Device User should not be able to register Device Account without username #3162
    • CSV Datasets are not filtering by month and year when a 'Month' and 'Year' is selected #3181
    • Make Loc and t available on Editor's window object for consistency with Client environment #3161
    • Fix messaging during data optimization and reduce number of view optimized that are never used #3165
    • Prevent menu items from jumping around on Deploy page #3169
    • Fix bug causing document updates to get skipped over in sync after a Comparison Sync #3179

    Deprecate single csv download in favor of Spreadsheet Requests

    See screenshots here.

    • Change terminology referring to "CSV" to more commonly recognized "Spreadsheet" term.
    • "CSV Datasets" term changed to "Spreadsheet Requests".
    • Fix "CSV Datasets are not filtering by month and year when a 'Month' and 'Year' is selected #3181"
    • Request Spreadsheets page: Submit button now hovers and is sticky to bottom of page; "*" in Month/Year selection clarified as "All months"/"All years"; "Description" no longer required and given own line for better formatting; other formatting cleanup.
    • Data page: Removed deprecated CSV Download button; updated language; added "Request Spreadsheets" button for quick access to making a request for spreadsheets.
    • Spreadsheet Request Info page: Now dynamically updates as Spreadsheets are rendered with row counts and status; removed unnecessary filename to download all, instead it's a "download all" button; new types of status including "File removed", "Stopped", "Available", and "In progress"; Month and Year values of "*" now clarified as "All months" and "All years"; loading screen improvements; title of page now the date the spreadsheets were requested on.
    • Spreadsheet Requests page: Updated language; fixed total Spreadsheet Requests calculation in pagination; if status of Spreadsheet Request is "Available" the status shows in green; if the status of the Spreadsheet Request is "In progress" a spinner is shown where the Download button will be; labels of "More Info" and "Download" added to corresponding buttons; loading overlay now shown on initial load and when changing pages.
    • Spreadsheet Templates page: Updated terminology from CSV Templates to Spreadsheet Templates.

    New Features

    • Show recent activity as default search results #3171
    • Make a cached version of the Device information available to form logic on T.device.device #3183
    • Group Administrator configures Device Account password policy #3172
    • On search UI: limit initial results to 10 for fast load, add a "Load More" button for pagination, and style improvements #3164

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.22.0
    +./start.sh v3.22.0
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.21.0
    +

    v3.21.0

    Developers:Good to Know

    • The master branch has been moved to the main branch. No development will happen on the master branch, which has been deleted. Also, please note the updates to the Release Workflow

    Fixes

    • Prevent unnecessary CaseService saves by comparing hashes #3155
    • Prevent loss of case changes when leaving incomplete form by always saving the case #3156
    • Prevent on-submit of a form running in one Case from being able to run in another case by navigating quickly to another Case. We inject T and case (caseService) variables into Tangy Form (formPlayer) from EventFormComponent. This will add instanceFrom: 'EventFormComponent' to the caseService (and also assigns ['instanceFrom'] = 'EventComponent' in EventComponent). Note that if you have any use of window.T or window.caseService, you will need to make them T and caseService to take advantage of this fix. Commit: 716bc5e9
    • Bump tangy-form to v4.29.1 and tangy-form-editor to v7.10.2 Commit: a3f785310

    New Features

    • Add support for running an SSL frontend. Issue: #3147
    • Make CORS settings configurable by T_CORS_ALLOWED_ORIGINS Commit: 1f448f7e
    • Add ability to generate CSV datasets for all groups and all forms. This feature provides the new generate-csv-datasets command and csvDataSets route. #3149
    • Add support for Tangy Form's useShrinker flag, implemented as AppConfig.saveLessFormData. This is an experimental mode in Tangy Form that only captures the properties of inputs that have changed from their original state in the form. This should lead to smaller formResponses and quicker sync data transfers. Commit: 35a05c2b, Tangy-form pull: Add support for shrinking form responses #209

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.21.0
    +./start.sh v3.21.0
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.20.4
    +

    v3.20.4

    Fixes

    • Fixes resuming an unfinished Event Form. Commit bf97492

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.20.4
    +./start.sh v3.20.4
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.20.3
    +

    v3.20.3

    Fixes

    • Editing Timed Grids on Forms: Capture at item and Duration are compared as strings leading to unexpected validation scenarios #3130
    • Fix CORs usage in Tangerine APIs when outside applications are using credentials. #3132
    • When Tangerine creates CouchDB users for Sync, DB Administration, and Reporting, restrict that users access to the databases for the group they are assigned. This is a tightening of security to support use cases where users of groups on the same server should be restricted from accessing other groups data on the same server when Sync Protocol 2 and Database Administrator features are being used. #3118
    • Data Manager views in CSV which cycle sequence was used in each form response #3128.
    • Fix access denied message when using Tangerine APIs #3133
    • Make status translateable on Tangerine Teach Task Report. #3089
    • When editing Timed Grids on Forms, "Capture at Time" and "Duration" are compared as strings leading to unexpected validation scenarios. #3130
    • Fix database export when using Sync Protocol 1 by using the correct database names #3120

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.20.3
    +./start.sh v3.20.3
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.20.2
    +

    v3.20.2

    Fixes

    • Improve listing of items in the Data menu. Issue: #3125
    • Fix issue where Group User on server with permission to access database would not have access. Commit: a10162d9
    • Add Amharic translation.
    • Fix issue when backup has never run, the Clean backups command in Maintenance on client fails, and the process alert does not go away. This PR also copies over a fix for clearing all progress messages from Editor. PR: #3098
    • Fix bad url for Print Content feature in Editor/Author. PR: #3099
    • Clicking on unavailable form in Case should not open it. Issue: #3063
    • The csv and mysql outputs must carry over the 'archived' property from the group db. PR: #3104
    • Bump tangy-form to v4.28.2 and tangy-form-editor to v7.9.5. Includes fix for tangy-input-groups change logic Issue: #2728
    • Users should enter dataset description when creating a dataset in Editor PR: #3078
    • Avoid crashes when properties on the markup are accessed before being available to the component #3080
    • Replace special chars with underscore in CSV output. PR: #3003
    • Refresh global reference to T.case when using a case so most importantly the correct context is set PR: #3108
    • Link to download data set downloads a JSON file with headers and group config doc. Issue: #3114
    • CSV template creation fails. Issue: #3115
    • Restart couchdb container on failure. PR: #3112
    • APK and PWA Updates fail with User not logged in (every time) #3111
    • Fix error when looping through input values for data dictionary. PR: #3124
    • Add config to allow output of multiple participants in MySQL. Consult the PR for implementation details. If you wish to enable this feature, add T_MYSQL_MULTI_PARTICIPANT_SCHEMA:true to the config.sh script. PR: #3110

    Upgrade notice

    If your project was already using the Data Conflicts tools that were installed manually, you must remove those in order to prevent a conflict with the Database Conflicts tool that is now automatically installed in Tangerine -> Deploy -> Database Conflicts. Reset the group-uuid/editor directory with the content-sets/case-module/editor components or the content-sets/case-module-starter/editor/index.html file.

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.20.2
    +./start.sh v3.20.2
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.20.1
    +# This will index all database views in all groups. It may take many hours if 
    +# the project has a lot of data.
    +wedge pre-warm-views --target $T_COUCHDB_ENDPOINT
    +

    v3.20.1

    Fixes

    • Fix Form Editor removes manually added on-resubmit logic in tangy-form #3017
    • Support old PWAs that did not check for all permissions when installed in order to get permanent storage #3084

    New Features

    • Add Maintenance page to client to enable app administration tasks (clear out old backups and fix permissions) and disk space statistics. #3059

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.20.1
    +./start.sh v3.20.1
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.20.0
    +# This will index all database views in all groups. It may take many hours if 
    +# the project has a lot of data.
    +wedge pre-warm-views --target $T_COUCHDB_ENDPOINT
    +

    v3.20.0

    Fixes

    • Improve rendering of Device listing. PR: #2924 Note that you must run the update or else the Device view will fail.
    • Converted print form view as a single table #2927
    • Improvements to restoring a database from a backup PR: #2938
    • On server group's security page, fix link to adding roles and show loading screen when saving role.
    • Data outputs for CSV's now include the 'archived' property. #2988
    • This one goes out to the coders: Prevent CaseService singleton injection into Case related components #2948. This is an important change in how cases are handled - they are no longer singletons. If you are developing scripts for a form and there are problems accessing T.case, see the comments in #2948 for a solution.

    New Features

    • New CSVs related to Cases now available for Case Participants, Case Events, and Case Event Forms. https://github.com/Tangerine-Community/Tangerine/pull/2908
    • Online Survey user is warned if they are using an unsupported web browser (Internet Explorer). https://github.com/Tangerine-Community/Tangerine/pull/3001
    • Data Manager generates CSV with specific columns using CSV Templates.
    • Data Manager restores Case Event stuck in Conflict Revision. Add the can_restore_conflict_event permission to the users' role(s) to enable. #2949
    • Enable Data Conflict Manager for groups. 2997 This is based on the couchdb-conflict-manager web component.
    • In Offline App, when submitting a form, opening a case, creating a case, etc., a new loading screen is shown. #3000
    • In Online Survey, new support for switching language without interrupting the survey. #2643
    • For PWA's, there is a new device permissions step in device setup to guarantee persistent storage #3002
    • The login screen may now have custom markup. #2979
    • Statistical files are now available in Stata .do format for corresponding forms #2971
    • The new usePouchDbLastSequenceTracking property in app-config.json and settings page enables the use of PouchDB's native last sequence tracking support when syncing. #2999
    • The new encryptionPlugin:'CryptoPouch' property in app-config.json enables testing of the CryptoPouch extension currently in development. #2998 Please note that this feature is not yet ready for deployment. There are now three different possible storage configurations for Tangerine:
    • "encryptionPlugin":"CryptoPouch" - Configures the app to use CryptoPouch, which encrypts documents in the app's indexedb for storage.
    • "turnOffAppLevelEncryption": true - Configures the app without encryption, using the app's indexedb for storage instead of sqlite/sqlCypher.
    • "encryptionPlugin":"SqlCipher" - or without any additional configuration (SqlCipher is the default configuration.) - Configures the app to use SqlCipher, which encrypts documents in an external sqlLite database for storage.
    • We have changed how we determine which storage engine is being used. In the past we exposed a window['turnOffAppLevelEncryption'] global variable based on the same flag in app-config.json; however, now we are determining in app-init.ts which engine is running and exposing either window['cryptoPouchRunning'] or window['sqlCipherRunning'] to indicate which engine is running. It is important to note that even the app is configured with encryptionPlugin:'CryptoPouch' in app-config.json, the app may have been installed without that setting and is actually running sqlCypher. This is why it is important to observe if either window['cryptoPouchRunning'] or window['sqlCipherRunning'] is set.

    Backports/Good to Know

    When we add new features or fix issues in patch releases of Tangerine, those code changes usually get added automatically to any new releases of Tangerine. To make sure users of new releases are aware of those changes, we will occasionally mention them in this section in case they have missed them in the Changelog for the corresponding earlier release. Please note that when you install or upgrade a new Tangerine release, please review the Changelog for any changes in minor or patch releases.

    • Server admin can configure regex-based password policy for Editor. Instructions in the PR: #2858 Issue: #2844
    • Show loading screen in more places that typically hang such as the Case loading screen, issue loading, issue commenting, and many other places when working with Issues on the sever. (demo: https://youtu.be/RkoUN41jqr4)
    • Enhancements to support for archiving cases:
    • Added ability to search archived cases. Issue: #2977 Important : Run docker exec -it tangerine /tangerine/server/src/upgrade/v3.19.3.js to enable searching archived cases.
    • Added archive/unarchive Case functionality and permission for "can delete" #2954
    • Added backup and restore feature for Tangerine databases using device encryption. Increase the appConfig.json parameter dbBackupSplitNumberFiles (default: 200) to speed up the backup/restore process if your database is large. You may also change that parameter in the Export Backup user interface. Updated docs: Restoring from a Backup PR: #2910
    • Updates to tangy-form lib to 4.25.18 (Changelog), which provides:
    • Support for changing a page content's language and number system without reloading the page.
    • A fix for photo-capture so that it de-activates the camera when going to the next page or leaving a form. Also a new feature for configuring compression
    • Implemented a new 'before-submit' event to tangy-form in order to listen to events before the 'submit' event is dispatched.
    • A fix for User defined Cycle Sequences.
    • Important If your site uses csvReplacementCharacters to support search and replace configuration for CSV output, which was released v3.18.2, you must change the configuration string. See issue #2804 for information about the new schema.
    • Feature: Editor User downloads CSVs for multiple forms as a set Issue: #2768 PR:#2777
    • Feature: Remove configurable characters from CSV output #2787.

    Server upgrade instructions

    Important upgrade: Please note that you must run update below (v3.20.0.js) to install the new listDevices view. If you don't the Devices listing will fail.

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.20.0
    +./start.sh v3.20.0
    +# Run the update to install the new listDevices view.
    +docker exec -it tangerine /tangerine/server/src/upgrade/v3.20.0.js
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.19.1
    +# This will index all database views in all groups. It may take many hours if 
    +# the project has a lot of data.
    +wedge pre-warm-views --target $T_COUCHDB_ENDPOINT
    +

    v3.19.3

    Fixes

    • Fix issue where loading screen would not close after submitting a proposal on an Issue.
    • Fixes from v3.18.8 incorporated.
    • Fixes to how role based permission rules are applied on the schedule view.
    • Fix CaseService.rebaseIssue from failing due to accessing eventForms incorrectly.

    New Features

    • Show loading screen in more places that typically hang such as the Case loading screen, issue loading, issue commenting, and many other places when working with Issues on the sever. (demo: https://youtu.be/RkoUN41jqr4)
    • Material design applied to loading indicator on the server.
    • New cancel button on loading indicator on the server. Will warn that this may cause data corruption and data loss. (demo: https://youtu.be/da9cxG5w8c0)
    • Added ability to search archived cases. Issue: #2977 Important : Run docker exec -it tangerine /tangerine/server/src/upgrade/v3.19.3.js to enable searching archived cases.

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.19.3
    +./start.sh v3.19.3
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.19.2
    +# Run the v3.19.3.js update to enable indexing of archived documents.
    +docker exec -it tangerine /tangerine/server/src/upgrade/v3.19.3.js
    +# This will index all database views in all groups. It may take many hours if 
    +# the project has a lot of data.
    +wedge pre-warm-views --target $T_COUCHDB_ENDPOINT
    +

    v3.19.2

    Fixes

    • Added process indicator when archiving, un-archiving, or deleting a case. Issue: #2974
    • Add v3.19.2 update to recover if v3.19.0 search indexing failed

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.19.2
    +./start.sh v3.19.2
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.19.1
    +# Perform additional upgrades.
    +docker exec -it tangerine bash
    +# This will index all database views in all groups. It may take many hours if 
    +# the project has a lot of data.
    +wedge pre-warm-views --target $T_COUCHDB_ENDPOINT
    +

    v3.19.1

    Fixes

    • Improved backup and restore file processing. Docs: Restoring from a Backup PR: #2910
    • Added archive/unarchive Case functionality and permission for "can delete" #2954

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.19.1
    +./start.sh v3.19.1
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.19.0
    +# Perform additional upgrades.
    +docker exec -it tangerine bash
    +# This will index all database views in all groups. It may take many hours if 
    +# the project has a lot of data.
    +wedge pre-warm-views --target $T_COUCHDB_ENDPOINT
    +

    v3.19.0

    New Features

    1. Data Manager requests and downloads CSVs for multiple forms as a set. When logged into the server and in a group, you will now find a "Download CSV Data Set" menu item under "Data". From there you can view all of the CSV Data Sets you have generated in the past, the status of wether or not they have finished generating, a link to download them, and other meta data. Click the "New Data Set" button and you will be able to select any number of forms to generate CSVs for, data for all time or a specific month, and wether or not to exclude PII. This is especially useful for generating CSVs that take longer to generate than the automatic logout built into the server. You may request a CSV Data Set, log out, and then log back in later to check in on the status and download it. A Server Administrator can also configure cron with a generate-csv-data-set command to generate a data set on a daily, weekly, or monthly basis, handy for situations where you want CSVs to automatically generate on the weekend and then download them on Monday. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2768) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2777)
    2. Data Manager archives a Case to remove it from reporting output and Devices. This adds an "archive" button on Cases that flags all related Form Responses as archived and removes them from CSV output and Search on Devices. This uses the new T.case.archive() API which adds an 'archived' flag for those docs and saves a minimal version of the doc with enough data to be indexed on the server. Search on client and server CSV output are modified to filter archived docs. When viewing cases in Editor, displays "Archived" when viewing an archived case. When client syncs, it deletes any docs with the 'archived' flag and sets deletedArchivedDocs In the replicationStatus log. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2843) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2776)
    3. Devices Manager reconfigures claimed Device sync settings and selects multiple Sync Locations for a Device. Details: To change a Device's sync settings currently requires a reinstall of the app on the Device and setting up all the accounts again. This PR will allow system admins to change the sync settings for a Device which then triggers on next sync a Rewind Push, database delete, then a first pull with the new sync settings. Subsequent syncs then use the new sync settings. This PR also refactors the Create and Edit forms for Devices on the server so that multiple sync locations can be added. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2867) (PR: #2782)
    4. Device Manager estimates how large an initial sync will be given selected sync settings. When setting up sync settings for a Device, it is useful to know how many documents will need to be downloaded given which forms are configured for syncing down and the locations assigned. There is now a "calculate down-sync size" button at the bottom of Device edit/creation forms that when pressed will tally up the documents needing to be down synced given the device sync settings. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2845) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2818)
    5. Devices Manager monitors for Devices close to filling up disk space. Devices now report how much free space they have to the server after a sync. This can be monitored on the Deploy > Devices list. When a Device reports having less than 1GB free storage, a warning is shown on the Devices list. (Ticket: 2779) (PR: 2795)
    6. Server User views the version of Tangerine installed. Any user on the server can now view the version of Tangerine installed by going to Help menu in the left nav bar. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2846) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2794)
    7. Database Consumer accesses Tangerine MySQL databases via web browser. Users of Tangerine's MySQL database sometimes are not allowed to install tools such as MySQL Workbench on their work computers. This PR makes starting PHPmyAdmin (a mysql viewer) as a web service a configuration option in Tangerine so no one has to install software on their computer to access Tangerine MySQL. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2847) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2793)
    8. Data Collector creates Account on Device and associates with any User Profile in Group (ignoring Device assignment/sync settings). By default, when a Data Collector creates an Account on Device, they can only associate with User Profiles that are assigned to the same location as the Device's Assigned Location. Add "disableDeviceUserFilteringByAssignment":true to the app-config.json for the group and this restriction will be removed. Tablets will also sync all User Profiles, ignoring the Device's configured Sync Location(s). (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2848) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2792)
    9. Form Developer writes code that can access Case's related Location metadata without writing asynchronous code. When working synchronously in forms, we don't currently have access to the related Location Node data without loading the Location List async and using T.case.case.location to search the hierarchy for the node we want. This PR loads all related Location Nodes into memory at T.case.location when the context of a Case is set. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2849) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2791)
    10. Server Administrator configures substitutions for CSV output. This feature allows a Server Administrator to update the group's configuration in the app database to contains Regex string replacements for CSV output. This can be handy in situations where Data Analysts are having trouble parsing CSV data that contains line breaks and commas. An example configuration to remove line breaks and commas from data would be "csvReplacementCharacters": [{"search": ",", "replace": "|"}, {"search": "\n", "replace": "___"}]. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2787) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2788)
    11. Server Administrator configures Tangerine to not auto-commit in groups' data directories to preserver manually managed git content repositories. When using git to manage group content in a git flow like manner, the automatic commit can result in unnintentional commits. System Administrators can now turn off this auto-commit by configuring Tangerine's config.sh with T_AUTO_COMMIT="false". If set to true also include the frequency T_AUTO_COMMIT_FREQUENCY="60000" (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2614) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2748)
    12. Data Collector proposes change to a Form on a Case. The issues feature that has been available on the server is now optionally also available on Devices by "allowCreationOfIssues": true to client/app-config.json for the group you want this enabled. Most of the features of Issues you are familiar with from the server are there, except for merging proposals which is not allowed. Issues from Devices are uploaded to the server where proposals can be merged by a Data Manager. We also streamlined the Issue creation and proposal process by skipping the page to fill out an issue title/description, and then forward them directly to creating a proposal. To aid in issue titles/descriptions that make sense, Content Developers can now add templateIssueTitle and templateIssueDescription to Case Definition files. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2850) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2330) (Demo Video)
    13. Data Manager updates Issue Title and Issue Description Data Managers will now find a metadata tab on an Issue where they can update the Title, Description, and new "Send to" settings. (Issue: https://github.com/Tangerine-Community/Tangerine/issues/2851) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2330)
    14. Data Manager sends an Issue to all Devices in Sync Area or specific Device When create/configuring an Issue, Data Managers now have the option to send an Issue to a specific location in a Sync Area, or send it to a specific Device by Device ID. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2854) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2330)
    15. Forms Developer defines custom logic for Device's search of Cases and Forms In some cases there are situations where the standard variables for searching do not cover all things we want searched, or there is a compound field we want to be searched. Adding a client/custom-search.js file allows the Forms Developer to hook into the map function used to generate the search index. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2852) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2740)
    16. Data Manager views list of Issues related to Case When viewing a Case on the server, the first screen when opened will now show a list of related Issues. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2723) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2573)
    17. Form Developer uses API to override Device User based access to Event Forms on a per Case Event basis Currently we can configure in a Case Definition the operation permissions on all instances of an Event Form. This change allows a Form Developer to write logic that would control those permissions on a per Event Form basis by setting the same permissions property on the Event Form itself. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2624) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2660)
    18. Form Developer configures Devices to skip optimization of database views not in use on Device. Some projects relying heavily on a custom app will find they do not use all of the standard Tangerine database views, thus they can be skipped during the data optimization phase after a sync. In app-config.json, you can add a new doNotOptimize property with a value as an array of views to skip. To discover what views your app is indexing, see the console logs from a device during the optimization phase. You may discover some views you can add to doNotOptimize to speed up that optmization process. (Commit: https://github.com/Tangerine-Community/Tangerine/commit/4b8864470c1cad98e43152dd6bb3c91ee3e576a6)
    19. System Administrator batch imports all forms from a Tangerine v2 group into a Tangerine v3 group Tangerine v3 now has a script that will import all v2 group forms into a v3 group without having to do each form individually. (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2857) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2584)

    Fixes

    1. Issue created programatically in on-submit says we must rebase but no button to rebase #2785 Cases that have used the T.case.createIssue() API in forms to create Issues on the current form have recently found the resulting issues are broken. This is due to a change in when the Form Response is associated with the case (later than when T.case.createIssue() is called in a form's on-submit). To remedy this, we've added a new T.case.queueIssueForCreation("Some label", "Some comment") API. If you are using T.case.createIssue(), immediately upgrade and replace its usage with T.case.queueIssueForCreation(). (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2785) (Example: https://github.com/Tangerine-Community/Tangerine/blob/next/content-sets/case-module/client/test-issues-created-programatically-on-client/form.html#L5)
    2. Using a simpler reverse sort for device status (PR: https://github.com/Tangerine-Community/Tangerine/pull/2775)
    3. Increase likelihood that migration of data to mysql will recover where it left off if server restarts. (PR: https://github.com/Tangerine-Community/Tangerine/pull/2773)
    4. From Case Definitions, the onCaseOpen and onCaseClose now also run in the server context. (PR: https://github.com/Tangerine-Community/Tangerine/pull/2696)
    5. "openEvent is not defined" when accessing a case in Editor (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2800)
    6. Synclog date/time header is incorrect and sort is broken (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2762)
    7. Synchronization UX Improvements - remove error state after retries when retry is successful (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2808) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2826)
    8. Fix missing 'form_' from id for v2 import (Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2856) (PR: https://github.com/Tangerine-Community/Tangerine/pull/2726)
    9. Minor tweak to tangerine-preview README (PR: https://github.com/Tangerine-Community/Tangerine/pull/2735)

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.19.0
    +./start.sh v3.19.0
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.18.3
    +# Perform additional upgrades.
    +docker exec -it tangerine bash
    +push-all-groups-views
    +update-down-sync-doc-count-by-location-id-index '*'
    +# This will index all database views in all groups. It may take many hours if 
    +# the project has a lot of data.
    +wedge pre-warm-views --target $T_COUCHDB_ENDPOINT
    +

    v3.18.10

    Fixes

    • Backport: Make status translateable on Tangerine Teach Task Report. #3089
    • Backport: When editing Timed Grids on Forms, "Capture at Time" and "Duration" are compared as strings leading to unexpected validation scenarios. #3130

    v3.18.9

    Fixes

    v3.18.8

    v3.18.7

    Fixes

    • Back-ported some fixes to the backup and restore feature from the v3.19.1 branch.
    • Fixed issue with Teach where third subtask would not open correctly.

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.18.7
    +./start.sh v3.18.7
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.18.6
    +

    v3.18.6

    Updates

    • Updated tangy-form lib from 4.25.11 to 4.25.14 (Changelog), which provides:
    • A fix for photo-capture so that it de-activates the camera when going to the next page or leaving a form.
    • Implemented a new 'before-submit' event to tangy-form in order to listen to events before the 'submit' event is dispatched.
    • A fix for User defined Cycle Sequences.

    Fixes

    • Remove incorrect exception classes for changes processing #2883 PR: #2883 Issue: #2882
    • Added backup and restore feature for Tangerine databases using device encryption. Increase the appConfig.json parameter dbBackupSplitNumberFiles (default: 200) to speed up the backup/restore process if your database is large. You may also change that parameter in the Export Backup user interface. Updated docs: Restoring from a Backup PR: #2910

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.18.6
    +./start.sh v3.18.6
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.18.5
    +

    v3.18.5

    Fixes

    • Server admin can configure regex-based password policy for Editor. Instructions in the PR: #2858 Issue: #2844

    v3.18.4

    Fixes

    • Backported a fix from the v3.19.0 branch for "Save the lastSequence number after each change is processed in the tangerine-mysql connector" Issue #2772
    • Address crashes when importing data using the mysql module #2820
    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.18.4
    +./start.sh v3.18.4
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.18.3
    +

    v3.18.3

    Fixes

    • Important If your site uses csvReplacementCharacters to support search and replace configuration for CSV output, which was released v3.18.2, you must change the configuration string. See issue #2804 for information about the new schema.
    • Backported a fix from the v3.19.0 branch for "Issue created programmatically in on-submit says we must rebase but no button to rebase #2785"
    • Description: Cases that have used the T.case.createIssue() API in forms to create Issues on the current form have recently found the resulting issues are broken. This is due to a change in when the Form Response is associated with the case (later than when T.case.createIssue() is called in a form's on-submit). To remedy this, we've added a new T.case.queueIssueForCreation("Some label", "Some comment") API. If you are using T.case.createIssue(), immediately upgrade and replace its usage with T.case.queueIssueForCreation().
    • Ticket: https://github.com/Tangerine-Community/Tangerine/issues/2785
    • Example: https://github.com/Tangerine-Community/Tangerine/blob/next/content-sets/case-module/client/test-issues-created-programatically-on-client/form.html#L5

    v3.18.2

    • Feature: Editor User downloads CSVs for multiple forms as a set Issue: #2768 PR:#2777
    • Feature: Remove configurable characters from CSV output #2787.
    • Documentation updates for backup/restore and fixes to image paths
    • Fix default user profile so it doesn't assume use of roles or location
    • Disabled "Print form backup" in Editor
    • Improvements to display of "Print metadata" in Editor
    • Update and fix for Cycle Sequences to enable numbering of sequences starting from 1. PR's: #231, #269
    • Bump tangy-form to 4.25.11 and tangy-form-editor to 7.8.8.

    v3.18.1

    • Fix backup when using os encryption and sync protocol 2 and cordova. (PR: #2767)
    • Fix creating of new Device Users when using Sync Protocol 2. (PR: #2769)
    • Fix default user profile form for Sync Protocol 1 users. We should not assume they are using roles or location.

    v3.18.0

    New Features

    • Enable configurable image capture in client #2695
    • Makes image capture work with a max size attribute - PR: #218
    • Add photo capture widget #203
    • Serve base64 image data as image files #2706 PR: #2725
    • Add Cycle sequences 1603
    • Sort by lastModified in the client case search #2692
    • Enable assigning multiple roles in forCaseRole in the eventFormDefinition #2694
    • Enable defining custom functions or valid JavaScript expressions that will be called when an event is opened and when an event is closed. On open and close events for case and case-events: #2696
    • Teach-specific strings in Russian for default content-set #2676
    • Uploads status such as app version when updating the app #2756

    Bugfixes

    • Initialize git in content repository before running git commands #2667
    • Only show the links to historical releases when T_ARCHIVE_PWAS_TO_DISK and T_ARCHIVE_APKS_TO_DISK in the config.sh are set to true #2608
    • Fix form breaking when form name has single quote #2489
    • Add print options to archived forms #1987
    • Fix Grid having negative values #2294
    • Fix to allow for running on m1 Macs #2631 #2631 Thanks @fmoko and @evansdianga!
    • For projects using the Case Reporting screen but don't have anything in reports.js but do have markup in reports.html, avoid crash due to empty file #2657
    • V2 import script fixes #2675
    • Allow HTML markup in option labels 2453
    • Reset grid values when grid is restarted #
    • Mark last attempted automatically when grid is auto-stopped #2467

    New Documentation

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.18.0
    +./start.sh v3.18.0
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.17.11
    +

    v3.17.12

    • Feature: Remove configurable characters from CSV output #2787.

    This release also has bugfixes specific to the Class module, which now uses updated API's for form rendering.

    • Feature for Class/Teach: Archive or enable a class. Issue: #2580
    • Bugfix for Class/Teach: Teach loses data and blocks app if Class form is not submited #2783
    • Bugfix for Class/Teach: App should return user to previous Curriculum when resuming app. Issue: #2648
    • Refactor Class to handle changes in tangy-form; Bug in CSV rendering for Tangerine Teach. Issue: #2635

    v3.17.11

    • Added support for custom update scripts for each group. Add either a before-custom-updates.js or after-custom-updates.js to the root of your content depending on when you wish the script to run. Script needs to return a Promise. See Issue 2741 for script example. PR: #2742
    • Add support for filtering PII variables on Case Participant data and Event Form data in Synapse caches. List the variable names in your group's content folder reporting-config.json. For example: { "pii": ["foo_variable"] }. This config was previously stored in the groups database.
    • Fixed bug that prevented rewind sync from working.

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.17.11
    +./start.sh v3.17.11
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.17.10
    +

    v3.17.10

    • Skip optimizing sync-queue, sync-conflicts, and tangy-form views after Sync Protocol 2 sync completes.
    • Using T.case.load() in a form? This release fixes a bug where EventForm.formResponseId would be not set when submitting forms in cases where a form has loaded a different case and then the save case back again thus detaching the memory reference being previously set.
    • Remove trailing whitespace from variables for mysql outputs to avoid illegal column names.
    • Add response-variable-value API with support for returning jpeg and png base64 values as files.
    • Refactor TANGY-SIGNATURE and TANGY-PHOTO-CAPTURE output in CSVs to be URLs of the image files.
    • Creates work-around for deployments that are unable to use custom-scripts. Issue #2711 PR #2712

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.17.10
    +./start.sh v3.17.10
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.17.9
    +

    v3.17.9

    New Features and Buffixes

    • Prevent failed calls to T.case.save() in forms by avoiding any saves to a case when a form is active. PR, Issue
    • Enable assigning multiple roles in forCaseRole in the eventDefinition #2694 - Cherry-picked commit 3e4938a0a80c57 only.
    • Enable defining custom functions or valid JavaScript expressions that will be called when an event is opened and when an event is closed. On open and close events for case and case-events: #2702

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.17.9
    +./start.sh v3.17.9
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.17.8
    +

    v3.17.8

    • Fix use of initial batch size #2685
    • Created generate-form-json script that generates the form json for a group from its form.html file. Usage: docker exec tangerine generate-form-json group-uuid The script loops through a group's forms.json and creates a form.json file in each form directory, next to its forms.html. Before using this script, run npm install. Issue: #2686
    • The synapse module now uses the json from generate-form-json to exclude PII. Also, the synapse module takes substitution and pii fields to accommodate schema changes and pii fields not identified in forms. PR: #2697

    Place these properties in the groups Couchdb:

      "substitutions": {
    +    "mnh_screening_and_enrollment_v2": "mnh01_screening_and_enrollment"
    +  },
    +  "pii": [
    +    "firstname",
    +    "middlename",
    +    "surname",
    +    "mother_dob"
    +  ]
    +

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.17.8
    +./start.sh v3.17.8
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.17.7
    +

    v3.17.7

    • fix CSV generation issue: #2681

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.17.7
    +./start.sh v3.17.7
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.17.6
    +

    v3.17.6

    • fix issue w/ empty replicationStatus?.userAgent
    • Switched from just-snake-case to @queso/snake-case - better Typescript compatability.

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.17.6
    +./start.sh v3.17.6
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.17.5
    +

    v3.17.5

    • Bumps tangy-form to 4.23.3, editor to 4.23.3. Issue: 2620
    • Update date carousel to 5.2.1 with fix for clicking the today button. PR: #2677

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.17.5
    +./start.sh v3.17.5
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.17.4
    +

    v3.17.4

    • Enables support for reducing the number of documents processed in the changed feed when syncing using the 'changes_batch_size' property in app-config.json. This new setting will help sites that experience crashes when syncing or indexing documents. Using this setting will slow sync times. Default is 50. During recent tests, the following settings have been successful in syncing a location with over 12,700 docs that was experiencing crashes:
    • "batchSize": 50
    • "writeBatchSize": 50
    • "changes_batch_size": 20

    Please do note that these particular settings do make sync very slow - especially for initial device sync. - Removed selector from push sync - was causing a crash on large databases. Using a filter instead in the push syncOptions to exclude '_design' docs from being pushed from the client. - Adds "Encryption Level" column to the Devices Listing, which shows if the device is running 'OS' encryption or 'in-app' encryption. - 'OS' encryption: Encryption provided by the device operating system; typically this is File-based (Android 10) or Full-disk encryption (Android 5 - 9). - 'in-app' encryption: Database is encrypted by Tangerine.

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.17.4
    +./start.sh v3.17.4
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.17.3
    +

    v3.17.3

    • Automatically retry after failed sync. (https://github.com/Tangerine-Community/Tangerine/pull/2663)
    • Do not associate form response with Event Form if only opened and no data entered.
    • Fix issue causing Android Tablets using OS level encryption to spontaneously start using in-app encryption.

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.17.3
    +./start.sh v3.17.3
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.17.2
    +

    v3.17.2

    • Add support for depending on Android Disk encryption as opposed to App Level encryption. Set turnOffAppLevelEncryption to true in client/app-config.json. Note that enabling this will not turn off App Level encryption for devices already installed, only new installations.
    • Fix race condition data conflict on EventFormComponent that is triggered when opening and submitting a form quickly. Prevent data entry until Case is loaded to avoid conflicting Case save of a fast submit.
    • Fix bug causing Device ID to not show up on About page on Devices.
    • When syncing, push before pull to avoid having to analyze changes pulled down for push.
    • Fix download links for archived APKs on Live channel.

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.17.2
    +./start.sh v3.17.2
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.17.1
    +

    v3.17.1

    • Add support for Form Versions when it hasn't been used before by defaulting the first entry in formVersions when a form version isn't defined on a Form Response.
    • Fix issue causing Device Admin user log in to fail.
    • Restore missing sectionDisable function in skip logic for forms.

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Ensure git is initialized in all group folders.
    +docker start couchdb
    +docker start tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.17.1
    +./start.sh v3.17.1
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.17.0
    +

    v3.17.0

    New Features and Fixes

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Ensure git is initialized in all group folders.
    +docker start couchdb
    +docker start tangerine
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.17.0
    +./start.sh v3.17.0
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.16.4
    +

    v3.16.5

    Fixes

    • T_ARCHIVE_APKS_TO_DISK and/or T_ARCHIVE_PWAS_TO_DISK setting have no effect. Issue: #2608
    • Bug in CSV rendering for Tangerine Teach. Issue: #2635 new setting outputDisabledFieldsToCSV in groups doc

    Developer Interest

    There is now a content set for developing projects with the Class module enabled in content-sets/teach. Sets the following properties in app-config.json:

    • "homeUrl": "dashboard"
    • "uploadUnlockedFormReponses": true

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Ensure git is initialized in all group folders. 
    +docker start couchdb
    +docker start tangerine
    +docker exec tangerine sh -c "cd /tangerine/groups && ls -q | xargs -i sh -c 'cd {} && git init && cd ..'"
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.16.5
    +# If you are enabling the new mysql module, follow the instructions in `docs/system-administrator/mysql-module.md` to update the config.sh file (steps 1 through 3)
    +# If you do not wish APK and PWA archives to be saved, set T_ARCHIVE_APKS_TO_DISK and/or T_ARCHIVE_PWAS_TO_DISK to false.
    +# Then return here before starting tangerine
    +# Now you are ready to start the server.
    +./start.sh v3.16.5
    +docker exec tangerine push-all-groups-views
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.16.4
    +# If setting up mysql return to step 5 in `docs/system-administrator/mysql-module.md`
    +

    v3.16.4

    New Features

    • Warning about data sync: Any site that upgraded to v3.16.2 is at risk of having records stay on the tablet unless they upgrade to v3.16.3 or v3.16.4. After upgrading to v3.16.4, go to the Online Sync feature and click the new 'Advanced Options' panel. There are two new options for sync - Comparison Sync and Rewind Sync. Comparison sync enables the Sync feature to compare all document id's on the local device with the server and uploads any missing documents. Rewind Sync resets the sync "placeholder" to the beginning, ensuring that all docs are synced. It doesn't actually re-upload all docs; it instead checks that all docs have been uploaded. It is more thorough than Comparison Sync. Both of the features are for special cases and should not be used routinely. Issue: #2623

    There are two settings that can be configured for Comparison sync: - compareLimit (default: 150) - Document id's must be collected from both the tablet and server in order to calculate what documents need to be sync'd to the server. This setting limits the number of docs queried in each batch. - batchSize (default: 200) - Number of docs per batch when pushing documents to the server. This same configuration setting is used for normal sync, so please take care when making changes to it.

    This new "Comparison" option is very new and may have rough edges. In our experience, if the app crashes while using it, re-open the app and try again; chances are that it will work. If it consistently fails, lower the value for app-config.json's compareLimit property.

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Ensure git is initialized in all group folders.
    +docker start couchdb
    +docker start tangerine
    +docker exec tangerine sh -c "cd /tangerine/groups && ls -q | xargs -i sh -c 'cd {} && git init && cd ..'"
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.16.4
    +# If you are enabling the new mysql module, follow the instructions in `docs/system-administrator/mysql-module.md` to update the config.sh file (steps 1 through 3)
    +# If you do not wish APK and PWA archives to be saved, set T_ARCHIVE_APKS_TO_DISK and/or T_ARCHIVE_PWAS_TO_DISK to false.
    +# Then return here before starting tangerine
    +# Now you are ready to start the server.
    +./start.sh v3.16.3
    +docker exec tangerine push-all-groups-views
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.16.3
    +# If setting up mysql return to step 5 in `docs/system-administrator/mysql-module.md`
    +

    v3.16.3

    New Features

    • Warning about data sync: Any site that upgraded to v3.16.2 is at risk of having records stay on the tablet unless they upgrade to v3.16.3. After upgrading to v3.16.3, run the new "Push all docs to the server" feature available from the Admin Configuration menu item. This feature resets push sync to the beginning, ensuring that all docs are pushed. It doesn't actually re-upload all docs; it instead checks that all docs have been uploaded.

    • Added "Push all docs to the server" feature to the Admin Configuration menu item.

    • Added Operating System and Browser Version to Device listing.

    Fixes - Data collected after first registering and after updates fails to upload. Issue: #2623

    Server upgrade instructions

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Ensure git is initialized in all group folders. 
    +docker start couchdb
    +docker start tangerine
    +docker exec tangerine sh -c "cd /tangerine/groups && ls -q | xargs -i sh -c 'cd {} && git init && cd ..'"
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.16.3
    +# If you are enabling the new mysql module, follow the instructions in `docs/system-administrator/mysql-module.md` to update the config.sh file (steps 1 through 3)
    +# If you do not wish APK and PWA archives to be saved, set T_ARCHIVE_APKS_TO_DISK and/or T_ARCHIVE_PWAS_TO_DISK to false.
    +# Then return here before starting tangerine
    +# Now you are ready to start the server.
    +./start.sh v3.16.3
    +docker exec tangerine push-all-groups-views
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.16.2
    +# If setting up mysql return to step 5 in `docs/system-administrator/mysql-module.md`
    +

    v3.16.2

    New Features

    • Enables filtering of Case Event Schedule by Device's Assigned Location PR: #2591

    Fixes - Enables editing of device description. Commit: #2613

    Server upgrade instructions

    If you want to enable filtered Case Event Schedule by Device's Assigned Location, add filterCaseEventScheduleByDeviceAssignedLocation to your groups' app-config.json set to a value of true.

    Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Ensure git is initialized in all group folders. 
    +docker start couchdb
    +docker start tangerine
    +docker exec tangerine sh -c "cd /tangerine/groups && ls -q | xargs -i sh -c 'cd {} && git init && cd ..'"
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.16.2
    +# If you are enabling the new mysql module, follow the instructions in `docs/system-administrator/mysql-module.md` to update the config.sh file (steps 1 through 3)
    +# If you do not wish APK and PWA archives to be saved, set T_ARCHIVE_APKS_TO_DISK and/or T_ARCHIVE_PWAS_TO_DISK to false.
    +# Then return here before starting tangerine
    +# Now you are ready to start the server.
    +./start.sh v3.16.2
    +docker exec tangerine push-all-groups-views
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.16.1
    +# If setting up mysql return to step 5 in `docs/system-administrator/mysql-module.md`
    +

    v3.16.1

    New Features

    • Improves sync stats and add "Export Device List" feature PR: #2610

    Fixes - Fixes Editor form creation issue #2605 and form copy issue #2604 - Adds check for calculateLocalDocsForLocation before running update to index an index it depends upon. - Update tangy-form to 4.21.3, tangy-form-editor to 7.6.5 to fix dynamically set level tangy location not resuming correctly #202

    Server upgrade instructions Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Ensure git is initialized in all group folders. 
    +docker start couchdb
    +docker start tangerine
    +docker exec tangerine sh -c "cd /tangerine/groups && ls -q | xargs -i sh -c 'cd {} && git init && cd ..'"
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.16.1
    +# If you are enabling the new mysql module, follow the instructions in `docs/system-administrator/mysql-module.md` to update the config.sh file (steps 1 through 3)
    +# If you do not wish APK and PWA archives to be saved, set T_ARCHIVE_APKS_TO_DISK and/or T_ARCHIVE_PWAS_TO_DISK to false.
    +# Then return here before starting tangerine
    +# Now you are ready to start the server.
    +./start.sh v3.16.1
    +docker exec tangerine push-all-groups-views
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.16.0
    +# If setting up mysql return to step 5 in `docs/system-administrator/mysql-module.md`
    +

    v3.16.0

    New Features

    • Warning about data sync: If you have implementations that have multiple tablets syncing from the same location, some docs may not be on all tablets due to issues with earlier versions of sync. This release resolves that particular issue and provides ways to ensure that all tablets share the same data. We have implemented several ways to rectify and understand potential data inconsistencies across tablets in the field:
    • After updating the server to 3.16.0 and after updating and syncing the clients, the Device dashboard will now display the number of docs on each tablet ("All Docs on Tablet") and the number of docs according to the device's Location configuration ("Form Responses on Tablet for Location").

      • Depending on the Configure/Sync settings, the "All Docs on Tablet" count may be close, but not exactly the same, since not all forms may be synced to the tablets.
      • The "Form Responses on Tablet for Location" count should be the same for all tablets that share the same location configuration. Please note that "Form Responses on Tablet for Location" count needs to be activated by adding "calculateLocalDocsForLocation": true to app-config.json; also note that it has not been widely tested and may be unstable. (If you activate this feature, you may also add "findSelectorLimit" to modify how many batches are used to calculate this value. Default is 200. Lower is safer but slower.)

      These data points may help in identifying data inconsistencies. Remember - only after updating and syncing the tablets, will these new doc counts be populated with data in the Devices listing. Making a note of the document counts per tablet will help establish a baseline. - Next step would be to run the new "Force Full Sync" feature, which is implemented in two ways: - If you add the new "forceFullSync" : true setting in the group's app-config.json, the client will perform a full sync upon the next update. Since this takes time and Internet bandwidth, you may wish to notify users before enabling this feature. - When logging in as "admin" user on the client tablet, a new menu item called "Admin Configuration" will be visible below the "Settings" item. This new item enables manual operation of the "Force Full Sync" feature. It is labeled "Pull all docs from the server" in the user interface. - You may adjust the settings for how many documents "Force Full Sync" downloads at a time by adjusting the initialBatchSize property in app-config.json. The default is 1000 documents per batch. This setting is also used when performing the initial load of documents on a tablet. - Tangerine Release Archives: Every Tangerine APK or PWA release is saved and tagged. If your site is configured for archives (which is the default), you may download previous Android releases. PR: #2567 - A "Description" field has been added to the Devices listing to faciliate identification of devices or groups of devices.
      - Beta Release Mysql module: Data sync'd to Tangerine can be output to a MySQL database. Warning: This should not yet be deployed on a production server; the code for this feature is still in development. We recommend creating a separate server for the Tangerine/MySQL installation and replicate data from the production server to the Tangerine server that would provide the MySQL service. Docs: docs/system-administrator/mysql-module.md PR: #2531 - Devices listing offers more information about the sync process, including version, errors, and sync duration.

    Fixes - Changes to the sync code should improve sync stability and speed. #2592 You may configure certain sync properties: - initialBatchSize = (default: 1000) Number of documents downloaded in the first sync when setting up a device. - batchSize (default: 200) - Number of documents downloaded upon each subsequent sync. - writeBatchSize = (default: 50) - Number of documents written to the tablet during each sync batch. - Updated tangy-form-editor to v7.6.4, which improves functionality of duplicate entire section. PR: #173 - Updates the Schedule View to use date-carousel 5.2.0 which provides unix timestamps instead of date strings. #2589 - Upgrade tangy-form to fix issue causing on-open of first items to not run when proposing changes in an Issue. - Deactivate App.checkStorageUsage if using Sync Protocol 2. This was not compatible and should not run. - Allow projects to disable GPS warming to save on battery with disableGpsWarming in app-config.json. - Add missing import of editor/custom-scripts.js when using editor so Data Dashboards can have imported JS files.

    Server upgrade instructions Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Ensure git is initialized in all group folders. 
    +docker start couchdb
    +docker start tangerine
    +docker exec tangerine sh -c "cd /tangerine/groups && ls -q | xargs -i sh -c 'cd {} && git init && cd ..'"
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.16.0
    +# If you are enabling the new mysql module, follow the instructions in `docs/system-administrator/mysql-module.md` to update the config.sh file (steps 1 through 3)
    +# If you do not wish APK and PWA archives to be saved, set T_ARCHIVE_APKS_TO_DISK and/or T_ARCHIVE_PWAS_TO_DISK to false.
    +# Then return here before starting tangerine
    +# Now you are ready to start the server.
    +./start.sh v3.16.0
    +docker exec tangerine push-all-groups-views
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.15.6
    +# If setting up mysql return to step 5 in `docs/system-administrator/mysql-module.md`
    +

    v3.15.8

    • New Sync code reduces the number of network requests by disabling server checkpoints. It also supports three new app-config.json options to configure sync parameters that adjust data download size, how much data is written to the local database each batch, and initial data download:
    • batchSize: Number of docs to pull from the server per batch. Increasing this setting will decrease the number of network requests to the server when doing a sync pull. Default: 200
    • writeBatchSize: How many docs to write to the database at a time. If the database crashes, decreasing this option could be helpful. Default: 50
    • useCachedDbDumps: Enables caching of the group database to a file for a single download to the client upon initial device setup. This is an experimental feature therefore it is not enabled by default. (Some server code is also currently disabled.) Those files are stored at data/groups/groupName/client/dbDumpFiles. At this point, you must delete the dbDumpFiles if you wish to update the data in the initial device load. 2560
    • Disable the v3.15.0 update from groups that use sync-protocol 1.
    • Added 2021 to the report year.
    • Added simple network statistics to the device replicationStatus, which is posted after every sync.

    Server upgrade instructions Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.15.8
    +# Now you are ready to start the server.
    +./start.sh v3.15.8
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.15.7
    +

    v3.15.7

    New Features and Fixes - Fixes a bug in the CSV generation code that caused sections of rows in the CSV to output improperly. PR:#2558 - Adds a server config that allows the user to control the string used for variables that are undefined: T_REPORTING_MARK_UNDEFINED_WITH="UNDEFINED" - The default value of the new config file is set to "ORIGINAL_VALUE" so existing Tangerine instances will not be effected.

    Server upgrade instructions Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    Please add the below line into your config.sh to preserve current behavior (as a workaround for #2564)

    T_REPORTING_MARK_UNDEFINED_WITH=""
    +

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.15.7
    +# Now you are ready to start the server.
    +./start.sh v3.15.7
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.15.6
    +

    v3.15.6

    New Features and Fixes - New 'wakelock' feature for sync: When using the sync feature, the screen should not go to sleep or dim, enabling the sync process to proceed. This is especially useful during long sync processes. When you navigate to another page once Sync is complete, the wakeLock feature is disabled. - The Devices listing has a new option, "View Sync Log", which enables viewing status of the most recent replication, when available.
    - Added error messages when internet access drops during a sync. #2540 - Batch size for sync is configurable via pullSyncOptions and pushSyncOptions variable in a group's app-config.json. Default is 200. If the value is set too high, the application will crash.

    Server upgrade instructions Reminder: Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.15.6
    +# Now you are ready to start the server.
    +./start.sh v3.15.6
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.15.5
    +

    v3.15.5

    Fixes - In CSV output, if a section on a form is opened and then the later skipped, inputs on that skipped section will appear in CSV output as skipped. However, if the section is never opened, the inputs would show up in the CSV as blank values. This fix ensures that these remaining inputs are marked as skipped in CSV output. - Fix sync from breaking when syncing with a group with no data yet. - Improve messaging during sync by removing floating change counts and showing the total number of docs in the database after sync.

    Server upgrade instructions Consider using the Tangerine Upgrade Checklist for making sure you test the upgrade safely.

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.15.5
    +# Now you are ready to start the server.
    +./start.sh v3.15.5
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.15.4
    +

    v3.15.4

    Fixes - Sync: Sites with large datasets were crashing; therefore, we implemented a new sync function that syncs batches of documents to the server. PR: #2532

    Server upgrade instructions

    # Fetch the updates.
    +cd tangerine
    +git fetch origin
    +git checkout v3.15.4
    +# Now you are ready to start the server.
    +./start.sh v3.15.4
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.15.3
    +

    v3.15.3

    Fixes - After a large sync in sync protocol 2, improve overall app performance by indexing database queries. Because this may cause a long sync for projects not using this, you can set indexViewsOnlyOnFirstSync in app-config.json to true if you want to allow existing tables to avoid this long sync to catch up on views. - Add missing custom-scripts.js and custom-styles.css files to Editor app. We also add editor and client ID's to the body tag of the two app respectively. - Reduce database merge conflicts by preventing form responses from saving after completed. Prior to this version, on two tablets (or on a tablet and the server) if you opened the same form response and opened an item to inspect, it would cause a save on both tablets resulting in an unnesessary merge conflict. - New T.case.getCaseHistory(caseId) function for getting the history of save for a Case. Returns an array of JSON patches in RFC6902 format. Open a Case and run await T.case.getCaseHistory() in the console and it will pick up on the context. - New T.case.getEventFormHistory(caseId, caseEventId, eventFormId) function for getting the history of save for a form response in a Case. Returns an array of JSON patches in RFC6902 format. Open a Case, a Case Event, then an Event Form and run await T.case.getEventFormHistory() in the console and it will pick up on the context. - New opt-in app-config.json setting attachHistoryToDocs for enabling upload all history of Case and Event Form edits on a Tablet up to the Server. Without this setting on, the server only sees the history starting from time of upload. Note this has an impact on upload size of at least doubling it when turned on.

    Important configuration notice - Set indexViewsOnlyOnFirstSync in app-config.json to true if you want to allow existing tables to avoid this long sync to catch up on views.

    Server upgrade instructions

    # Fetch the updates.
    +cd tangerine
    +git fetch origin
    +git checkout v3.15.3
    +# Now you are ready to start the server.
    +./start.sh v3.15.3
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.15.2
    +

    v3.15.2

    Fixes

    • Rshiny module: Replaces hard-coded underscore separator with the configurable sep variable.
    • Error when processing CSV's: 2517

    Important configuration notice

    The v3.15.0 release included an update to the Editor Search feature #2416 that requires adding a searchSettings property to forms.json. In addition to running the upgrade script for v3.15.0; you must also make sure that all forms in a group's forms.json have searchSettings configured, especially the shouldIndex property. Examples are in the Case Module README "Configuring Text Search" section.

    Server upgrade instructions

    cd tangerine
    +# Check the size of the data folder.
    +du -sh data
    +# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
    +df -h
    +# Turn off tangerine and database.
    +docker stop tangerine couchdb
    +# Create a backup of the data folder.
    +cp -r data ../data-backup-$(date "+%F-%T")
    +# Fetch the updates.
    +git fetch origin
    +git checkout v3.15.2
    +# Now you are ready to start the server.
    +./start.sh v3.15.2
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.15.1
    +

    v3.15.1

    Fixes

    • Prevent opening of Event Forms on Editor when there is no corresponding Form Response available.
    • Fix Issue type detection when deciding what is going to be in the 'current' tab.
    • Update CSV output for signatures to be 'signature captured' and ''.
    • Fix Issues view causing Issue search result to appear once per event such as comment or proposal.
    • Integrate fixes in v3.14.6 including T.case.isIssueContext() API, and better API partity between being in an Event Form in a Case and being in an Event Form in an Issue.

    Server upgrade instructions

    # Fetch the updates.
    +cd tangerine
    +git fetch origin
    +git checkout v3.15.1
    +# Now you are ready to start the server.
    +./start.sh v3.15.1
    +docker exec -it tangerine push-all-groups-views  
    +docker exec -it tangerine reporting-cache-clear  
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.15.0
    +

    v3.15.0

    • New Features and fixes
    • Editor User searches Cases by keyword #2416 - This feature enables searching by any of the variables assigned in searchSettings/variablesToIndex in forms.json.
    • Transfer Participant between Cases #2419. Find Participant UI: #2439.
    • Update to Content Set 2.1 adds a package.json and build step to pin lib versions and add a build step for custom-scripts.
    • Added error message to Updates error alert. ccc1864
    • New "Release Online Survey" menu on Server allows you to release a single form for data collection online. Note the original "Deploy -> Release" menu item has been moved to "Deploy -> Release Offline App".
    • Fixed issue where "Tangy Gate" form element could be added in Editor but would not appear on Tablets.
    • Support for new "" element that brings a Partial Date style form element with support for the Ethiopian Calendar.
    • If using Sync Protocol 2, the first sync when registering a Device is now faster in cases where there is a lot of data already collected. Also the blank User Profile created for the Admin user on a device is no longer uploaded resulting in less noise in the Device Users list.

    • Important deprecation notice

    • The groupName property, once used in app-config.json, is no longer supported in recent releases of Tangerine. The groupId property is used in its place. Groups that use groupName will not be able to sync; they must migrate to groupId. This issue affects groups using sync-protocol-1. #2447
    • When form responses are unlocked in a Data Issue, the on-submit hook no longer runs. If you need logic to run, use the new on-resubmit hook.
    • If using Sync Protocol 2, the "Auto Merge" feature that tries to fix database conflicts is now off by default and database conflicts will not be logged as Issues. If you would like to keep it on, set "autoMergeConflicts": true in your group's client/app-config.json file. However be aware that turning this on will result in inconsistent results (https://github.com/Tangerine-Community/Tangerine/issues/2484). Monitoring for database conflicts can now be done by monitoring the syncConflicts view via CouchDB Fauxton.

    Server upgrade instructions:

    # Fetch the updates.
    +cd tangerine
    +git fetch origin
    +git checkout v3.15.0
    +# Now you are ready to start the server.
    +./start.sh v3.15.0
    +# Update the views - there are new views for Searches and Participant Transfers.
    +docker exec -it tangerine reporting-cache-clear 
    +docker exec -it tangerine /tangerine/server/src/upgrade/v3.15.0.js
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.14.6
    +

    v3.14.6

    Changes in v3.14.4 were abandoned, changes in v3.14.5 have been rolled into v3.15.0. The following are changes for v3.14.6.

    • Improve first sync performance: On first sync, skip push but set the last push variable to whatever we left on after the first pull.
    • Improve in-form API parity between context of a Case and context of an Issue proposal. Sets case context in more scenarious inside of Issue Form proposals.
    • Prevent form crashes and unintentional logic by adding the new T.case.isIssueContext() API for detecting if in the Issue context in a form.

    Server upgrade instructions:

    # Fetch the updates.
    +cd tangerine
    +git fetch origin
    +git checkout v3.14.6
    +# Now you are ready to start the server.
    +./start.sh v3.14.6
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.14.3
    +

    v3.14.3

    • Bugfix
    • Auto-merged conflicts overwrite "canonical" change made on Editor server #2441 - Prevents tablets from overwriting documents from Editor in special cases. After modifying the case record, add canonicalTimestamp to the document: "canonicalTimestamp":1603854576785
    • New Features and fixes for all Tangerine
    • Reduce number of unnecessary saves in Editor #2444
    • Improvements to Issues Listing #2398 Please update the group views (noted in the Server upgrade instructions below) in order to use the Issues Listing.
    • Upgrades in the Developers' Interest
    • Removed webpack from the Docker image. Custom apps should build their apps using their own webpack; the APK service will no longer perform that task.

    Server upgrade instructions:

    # Fetch the updates.
    +cd tangerine
    +git fetch origin
    +git checkout v3.14.3
    +# Now you are ready to start the server.
    +./start.sh v3.14.3
    +# Update the views - there is a new view used for Issues.
    +docker exec -it tangerine push-all-groups-views
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.14.2
    +

    v3.14.2

    • Bugfix
    • Fixes file path issue when bundling custom scripts in APK's.

    Server upgrade instructions:

    # Fetch the updates.
    +cd tangerine
    +git fetch origin
    +git checkout v3.14.2
    +# Now you are ready to start the server.
    +./start.sh v3.14.2
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.14.1
    +

    v3.14.1

    This is identical to v3.14.0 but was released to fix a problem with tangerine-preview v3.14.0 on npm.

    v3.14.0

    • New Features and fixes for all Tangerine
    • Usability Improvement for Device Registration: Added "Number of devices to generate" field to Device Registration. Submitting a single form to add multiple devices to a group should simplify large deployments. #2402
    • Important bugfix for sync issue in poor network situations: If you currently have an active 3.13 deployment, run the 3.14 update on client to make sure all data is sync'd to the server. #2399
    • Automatic conflict resolution on client: Basic support for automatic merges of conflicts in EventForms. #2272 Documentation for testing conflicts
    • Form version support: Enables use of previous form versions for form display. #2365 Support for versioning is not yet implemented in the Editor; however, there is documentation on how to implement form versions manually.
    • User Interface updates: The 4.19.0 tangy-form lib version features the following fixes:
      • Required Field Asterisk (*) does not align with the question text #2363
      • Error Text and Warning Text have the same style - this is confusing for users #2364
    • Setting packageName in app-config.json causes app to crash: The docker-tangerine-base-image update to 3.7.0 improves Android and Cordova lib dependencies, and the release-apk code now rebuilds the Android code whenever an APK is built. #2366
    • New module for rshiny development: Adds option to csv module to change delimiter from '.' to '_'#2314
    • Documentation Update: Re-organization of some documentation and addition of missing image files. #2401
    • Upgrades in the Developers' Interest
    • Upgraded docker-tangerine-base-image to v3.7.1: Upgrade to Android API_LEVEL 30, Cordova 10, node:14.12.0-stretch. #1890 Caching cordova-android platform to avoid network issues when customizing packageName. #7
    • Important note for users of tangerine-preview There was a problem with v3.14.0 on npm; therefore, please use tangerine-preview v3.14.1.

    v3.13.1

    • Fix: Issues on Editor always ask us to rebase #2376
    • Fix: Issues screen will not load after upgrading from v3.10.0 to v3.13.0 #2378
    • Fix: Issues go missing after upgrading to v3.13.0 from v3.12.x #2377
    • Please be aware: this release was made in the release/v3.13.1-alt branch and to date has only been built as the v3.13.1-rc-2 image.

    Server upgrade instructions:

    # Fetch the updates.
    +cd tangerine
    +git fetch origin
    +git checkout v3.13.1
    +# Now you are ready to start the server.
    +./start.sh v3.13.1
    +# Run upgrade
    +docker exec -it tangerine /tangerine/server/src/upgrade/v3.13.1.js
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.13.0
    +

    v3.13.0

    • New Features and fixes for all Tangerine
    • Download Location List as CSV: You can now download a location list as a CSV. If you prefer editing a Location List via something like Excel, this makes editing an existing location list easier, which can then be imported when done editing in Excel. Note: Advise careful use of this export feature until #2336 is fixed. #2107
    • Duplicate a Section:When editing a form, you can now easily duplicate an entire section with the "duplicate section" button. #2109. Warning - this feature does not handle complex objects such as tangy checkbox groups well; be sure to check the code it generates. This issue will be addressed in the next 3.13 point release.
    • Group Data Dashboard in Editor: "Dashboard" is now a menu item available in a Group under the Data menu. This link can be enabled by group role (disabled by default). When on the Dashboard page, it displays a customizable dashboard for that specific group. Customizing Dashboards currently requires HTML and Javascript knowledge but in the future we may build a configurator for Dashboards.
    • More Menu Permissions in Editor:Add additional Editor permissions to completely cover menu level access in a group
    • Automatic conflict resolution: After a sync pull on client, detects type of conflict and resolves it. View status of merges in the Issues feature. #1763
    • Fix extending session in Editor - When prompted to extend session shows up session is not really extended. #2266
    • New Features and Fixes for Case Module
    • Client "Issues" feature: "Issues" previously could only be viewed using Editor. With this release, Issues can now be accessed from Client in a Case module enabled Group via the top level "Issues" tab. This tab can be disabled adding or modifying "showIssues": false, to app-config.json. Note that only issues created targeting the "CLIENT" context (See CaseService API documentation) will show up in the Client "Issues" tab.
    • Easier searching on Client: Previously on Client when searching for "Facility 8" you would need to type exactly "Facility 8". Now search is case insensitive and you may type "facility 8" to match against "Facility 8".
    • T.case.setEventWindow API fix: Previously when setting an Event window, the end time for the window was mistakenly ignored and set to the start time. This is now fixed. #2304
    • New Features for Sync Protocol 2 Module
    • Export device sheets: When registering Devices, we now offer an option to print "Device Sheets". Device Sheets include the registration codes for a Device and also some human readable metadata. Each row can also be used as a label for each device that can be fastened to a device using affordable clear packing tape. #2269
    • Restore Backup on Android Tablet: Backups can now be restored. Restore is an option when first opening a freshly installed APK. #2127
    • Better support for working on the same Case on two devices: When working offline on the same Case on two Devices, after a sync, it may seem like the changes on one Tablet have gone missing for some time until the "database conflicts" are resolved using the CouchDB Futon interface on the server. Starting in v3.13.0 we'll start to employ algorithms for automatically merging to speed up the process of resolving these database conflicts.
    • Notes for System Administrators
    • After upgrade, you will no longer find group content directories in ./data/client/content/groups/, they will be in ./data/groups/. Inside each group's directory you will also find they have been split into a client and editor directory. All previous content will now be in the client directory while you may place content for the Group's Data Dashboard in the editor folder.

    Server upgrade instructions:

    This update changes the path to group content to /tangerine/groups/${groupId}/client. If your group is managing content via a Github/cron integration, you will need to change the path to content in its cron job. Change GROUP-UUID to your group id in the following command:

    cd /home/ubuntu/tangerine/data/groups/GROUP-UUID/client && GIT_SSH_COMMAND='ssh -i /root/.ssh/arc-forms-dev' git pull origin master && git add . && git commit -m 'auto-commit' && GIT_SSH_COMMAND='ssh -i /root/.ssh/arc-forms-dev' git push origin master
    +

    The update:

    # Fetch the updates.
    +cd tangerine
    +git fetch origin
    +git checkout v3.13.0
    +# Now you are ready to start the server.
    +./start.sh v3.13.0
    +# Run upgrade
    +docker exec -it tangerine /tangerine/server/src/upgrade/v3.13.0.js
    +# Add or modify `"showIssues": false,` to the group's app-config.json if you do not want to display the Issues tab in client.
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.12.0
    +

    v3.12.5

    • Fixed issue with black screen when moving from p2p tab to home

    v3.12.4

    • Fixed issue with QR code boundary boxes in tangy-form #158. Bumped tangy-form v4.17.10 and tangy-form-editor to 7.2.5

    v3.12.3

    • Fixed issue with mutually exclusive checkboxes in tangy-form #154. Bumped tangy-form v4.17.9 and tangy-form-editor to 7.2.3

    v3.12.2

    • When saving edits to a form, "Show if" logic has been written as tangy-if logic in the HTML. Form now on it will be written as show-if logic for consistency.
    • tangy-if logic in has in the past in just showing/hiding an question on a form. It will now also reset the value if there is input and it is then hidden.
    • In v3.12.0, caseService.getCurrentCaseEventId() was incorrectly removed. It has been added back, and an additional caseService.getCurrentEventFormId() function has been added for consistency.
    • Fix Android 10 compatibility issue with P2P Sync mechanism causing tablets to crash.

    v3.12.1

    • Change behavior of show-if logic so that when a question hides, the value is reset.
    • Adjust behavior of how Event Forms are added: If EventForm.autoPopulate is left undefined and required is true, then the form should be added.

    v3.12.0

    • New Features for Case Module
    • Data Collector finds Event Forms are automatically created on Case Event creation and after adding a Participant #2147 [Demo]
    • Data Collector has found a non required form has become required #2233
    • Data Collector finds Case Event is automatically marked as complete #2235 [Demo]
    • Data Collector sees indicator on Event Form when corresponding Form Response has not been synced to a device #2232 [Demo]
    • Data Collector views a dedicated page for a Participant's Event Forms for a specific Case Event #2236 [Demo]
    • Data Collector is redirected to custom route after Event Form is submitted #2237 [Demo]
    • Fixes for Case Module
    • Device User registering only sees user profiles they can associate with restricted by location the Device is assigned #2248
    • When all optional and incomplete forms are removed (no required forms in the event) from an event on the client the + button is not shown to re-add any of them #2113
    • Delete an incomplete form from a case does not refresh the screen #2114
    • Fixes for all of Tangerine
    • Autostop is not triggered when marking the entire lineas incorrect #1869
    • Mark entire line of grid as incorrect cannot be undone #1651
    • Meta data print screen Prompt and Hint are not displayed for Radio Buttons (single type) #1748
    • Form Metadata view of Checkboxes with one option is missing #2239
    • New features for Sync Protocol 2
    • Restore encrypted backup on Device #2127

    • API Changes for Case Module

    • caseEvent.status is now caseEvent.complete which has a value of true or false as opposed to the status strings.
    • caseService.startEventForm(...) is now caseService.createEventForm(...).
    • caseService.deleteEventFormInstance(...) is now caseService.deleteEventForm(...).
    • caseService.getCaseEventFormsData(...) is now caseService.getEventFormData(...).
    • caseService.setCaseEventFormsData(...) is now caseService.setEventFormData(...).

    Server upgrade instructions:

    # Fetch the updates.
    +cd tangerine
    +git fetch origin
    +git checkout v3.12.0
    +# Now you are ready to start the server.
    +./start.sh v3.12.0
    +# Run upgrade
    +docker exec -it tangerine reporting-cache-clear 
    +# Remove Tangerine's previous version Docker Image.
    +docker rmi tangerine/tangerine:v3.11.0
    +
    Note that after running the upgrade script, your reporting caches may take some time to finish rebuilding.

    Android upgrade instructions: If you are upgrading an Android device that was installed with Tangerine v3.8.0 or greater, you will need to regenerate your APK and reinstall, otherwise you may use the over the air updater.

    v3.11.0

    • New Features in all Tangerine
    • Device Manager installs many Tangerine APKs on a single device #2182
    • CSV output enhancements:
      • Editor User indicates whether to include PII in CSV export #1771
      • User profile information available in CSV export #2081
      • Editor User exports CSV file that contains the group and form name #2108
    • New 'T' namespace for helper functions #2198
    • New Features in Tangerine with Case Module enabled
    • Data Collector changes location of Case #2098 [demo]
    • Data Collector views an alert #2020 [demo]
    • Data Collector views custom report #2143 docs
    • Developer notes
    • Group permissions #2187

    Server upgrade instructions:

    # Fetch the updates.
    +cd tangerine
    +git fetch origin
    +git checkout v3.11.0
    +# Now you are ready to start the server.
    +./start.sh v3.11.0
    +# Run upgrade
    +docker exec -it tangerine /tangerine/server/src/upgrade/v3.11.0.js
    +
    Note that after running the upgrade script, your reporting caches may take some time to finish rebuilding.

    Android upgrade instructions: If your groups are using Sync Protocol 2 module, an APK reinstall is required. Release the APK and reinstall on all Android Devices. If your groups are not using Sync Protocol 2, you may upgrade Android tablets over the air using the usual release process.

    v3.10.0

    Upgrade instructions:

    # Fetch the updates.
    +cd tangerine
    +git fetch origin
    +git checkout v3.10.0
    +# Now you are ready to start the server.
    +./start.sh v3.10.0
    +# Run upgrade
    +docker exec -it tangerine /tangerine/server/src/upgrade/v3.10.0.js
    +

    v3.9.1

    • Fixes
    • Database views are missing when running tangerine-preview or npm start #2096
    • Event Schedule day view duplicates day event and show it in previous day as well #2103
    • According to date carousel, events appear off by one week #2094
    • Event Schedule templates are failing #2085
    • Reports form is not added to forms.json #2088
    • Events appear off by one day in Schedule List #2101
    • CouchDB port should be configurable in config.sh #2092
    • When opening the schedule view the first page is missing the header dates #2082
    • Data Collector unable to open an Event from the Event Schedule #2102
    • When editing a radio button options in editor, options should be in one column, not two #2090

    v3.9.0

    • Features
    • Set and get properties for Case Event Forms #2023
    • Data Manager reviews Cases PR
    • Data Collector removes Event Form. PR
    • Fixes
    • Fix additional memory leaks in Case module causing tablets to slow down. PR
    • Make getValue() function in Event Form List Item related templates less likely to crash. change
    • Fixed incompatibilities with 2-way sync and P2P Sync.
    • Fixed issue causing tablets to crash when syncing with a database with tens of thousands of records.
    • Developer notes
    • Editor and Clients upgraded to Angular 8.
    • Changes
    • Due to current limitations of two way sync, two changes have been made to the Device form in Tangerine Editor. First, changing a Device's assigned location and sync settings after the Device record has been claimed will no longer be allowed. Second, devices will now always be required to be assigned to the last level in your location hierarchy.

    Upgrade instructions:

    When you run the upgrade script, if you are using sync protocol 2 and have enabled, forms configured for 2 way sync will now be configured to use CouchDB sync to push documents up as opposed to "custom sync".

    # Fetch the updates.
    +cd tangerine
    +git fetch origin
    +git checkout v3.9.0
    +# If you did not upgrade your config.sh in v3.8.1, migrate it now.
    +# Move custom variables from config.sh_backup to config.sh. Note that T_ADMIN and T_PASS are no longer needed. 
    +mv config.sh config.sh_backup
    +cp config.defaults.sh config.sh
    +# To edit both files in vim you would run...
    +vim -O config.sh config.sh_backup
    +# Now you are ready to start the server.
    +./start.sh v3.9.0
    +# Run upgrade
    +docker exec -it tangerine /tangerine/server/src/upgrade/v3.9.0.js
    +

    v3.8.1

    • Client app performance improvements
    • Improved caching of files. We are caching important configuration files for faster page loads (app-config.json, forms.json, location-list.json) and the Roboto font and have reduced redundant rendering calls. 1991
    • Loading spinner when opening an Event Form in a Case. #1992
    • Fixed a memory leak when viewing a Case which was causing tablets to crash if spending too much time on a Case screen. #2000
    • Radiobuttons now load faster on forms.
    • Editor fixes
    • Fixed an issue causing editor content region to be untouchable when window was narrow. #1940
    • Improved CSV output so it now contains Release ID, Device ID, and Build Channel on every row #349
    • Developer notes
    • We focused on issues with slow performance on tablets when viewing forms. We are caching important configuration files (app-config.json, forms.json, location-list.json) and the Roboto font and have reduced redundant rendering calls. More information in the Globals doc.
    • Server Admin notes
    • We cleaned up config variables in config.sh, deprecated T_ADMIN and T_PASS #1986
    • New generate-cases command for load testing a large number of Cases based on your custom content in a group. #1993
    • New reset-all-devices command for reseting the server token and database keys for all devices. Note that after running this command you will need to reinstall on all devices and reregister with new QR codes. This command is useful if you are migrating a large amount of devices to a new group or a new server and you want to maintain Device ID consistency with the Device Serial numbers you are tracking.

    Upgrade instructions:

    # Fetch the updates.
    +cd tangerine
    +git fetch origin
    +git checkout v3.8.1
    +# Now migrate custom variables from config.sh_backup to config.sh. Note that T_ADMIN and T_PASS are no longer needed. 
    +mv config.sh config.sh_backup
    +cp config.defaults.sh config.sh
    +# To edit both files in vim you would run...
    +vim -O config.sh config.sh_backup
    +# Now you are ready to start the server.
    +./start.sh v3.8.1
    +

    v3.8.0

    v3.8.0 is a big and exciting release! To accomodate the long list of changes, we split up this round of release notes into sections: General, Sync Protocol 2 Module, and Case Module, and Developer notes.

    General

    The following are features and fixes that are coming to all Tangerine installs. With this release comes an improved Editor UI experience, a faster device setup process, new form features, and much more.

    Group tabs are now in 4 sections Breadcrumbs allows you to navigate back up deeply nested areas
    group page form editing
    • Editor User browses Group UI by nested categories (as opposed to flat list) #1880
    • Device Administrator is prompted to authorize permissions on first app load #1896
    • Data Collector defines password according to policy #1867
    • Data Collector views device info such as Device ID, Assigned Location, Server URL, Group Name, and Release Channel. #1834
    • Data Collector in checkboxes chooses "none of the above", then other options are unselected #1822
    • Editor distinguishes between inputs that are hidden and skipped #1800
    • Minor tweaks to the menu (now there is a single "Sync" item) and added tab bars to some pages for consistency.

    Sync Protocol 2 Module

    Sync Protocol 2 is a new module that can be enabled on a Tangerine installation that adds Device management, the ability for form responses to sync to the server and back down to tablets, the ablity for two tablets to sync form responses with each other offline, and much more.

    Manage which devices have access to sync, when they last synced, when they last updated and which version Define which form responses are synced up and back down to tablets
    device management sync settings
    • Data Collector generates encrypted backup of Device #1909
    • Data Collector conducts a two-way sync with server only getting data from server relevant to their location #1755
    • Device sync by Location: Sync Protocol 2: Enables a "Device Setup" process on first boot of the client application. This requires you set up a "Device" record on the server. When setting up a Device record on the server, it will give you a QR code to use to scan from the tablet in order to receive it's device ID and token.
    • Data Collector syncs to server with large dataset #1757
    • Data collector synchronizes data between devices using an Offline P2P mechanism #279
    • Editor User configures two-way sync for form responses from specific forms #1753
    • Editor revokes access to syncing with server for a lost Device #1894

    Case Module

    • Data Collector views Case Events in Schedule with Estimated Day, Scheduled Day, Window, and Occurred On Dates #1737
    • Data Collector creates (another) instance of a repeatable form for a specific participant in a specific event(8hrs) #1786
    • Data Collector views which Participant they are filling out a form for #1820
    • Data Collector searches for a Case in a large dataset #1893
    • Improvements to Case Home search - limit docs to 25 when no phrase is entered: #1871. Added rule to delay search in Case Home until at least two characters have been entered. Search results now sorted by date record updated.
    • Lazy loading tabs in Case Home - this helps resolve some of the slowness in loading Case Home. Also disabled animations on tabs to remove jankiness.

    Developer Updates

    • Re-enabled git config in Dockerfile - still having git networking error even when off corp network.
    • Updated docker-tangerine-base-image to v3.4.0
    • New load testing doc.
    • Added random name generation to the script that generates new cases - useful for load testing and checking how well search listing works. If using the 'case-mother' switch, record templates are pulled from your group.

    Upgrade instructions

    On the server, backup your data folder and then run the following commands.

    git fetch origin
    +git checkout v3.8.0
    +./start.sh v3.8.0
    +docker exec -it tangerine /tangerine/server/src/upgrade/v3.8.0.js
    +

    Replace all ocurrences of localStorage.getItem('currentUser') with window.currentUser.

    v3.7.2

    • More fixes for upgrade process from v3.1.0.

    v3.7.1

    • Fix translations update script.
    • Fix client update process when upgrading from v3.1.0.

    Upgrade instructions: On the server, backup your data folder and then run the following commands.

    git fetch origin
    +git checkout v3.7.1
    +./start.sh v3.7.1
    +docker exec tangerine translations-update
    +

    v3.7.0

    Upgrade instructions: On the server, backup your data folder and then run the following commands.

    git fetch origin
    +git checkout v3.7.0
    +./start.sh v3.7.0
    +docker exec tangerine translations-update
    +

    v3.6.5

    Upgrade instructions: After the usual upgrade commands, also clear reporting caches with docker exec -it tangerine reporting-cache-clear.

    v3.6.4

    • Fix usage of T_CSV_MARK_DISABLED_OR_HIDDEN_WITH in some cases.

    v3.6.3

    • Allow disabled or hidden inputs output in CSV to be overridden using CSV_MARK_DISABLED_OR_HIDDEN_WITH in config.sh. The default value in config.defaults.sh is "999" which is what it has been for a few releases. When upgrading, do nothing if you want this to stay the same, otherwise use "ORIGINAL_VALUE" if you want to turn off the feature or set to your own custom value such as "SKIPPED".

    v3.6.2

    v3.6.1

    v3.6.0

    Upgrade instructions:

    On the server, backup your data folder and then run the following commands.

    git fetch origin
    +git checkout v3.6.0
    +./start.sh v3.6.0
    +

    Now you may publish a release to your Devices and run the "Check for Update" on each Device. Note that if you are looking to use the QR Code scanner and you have been using Android Installation, you will need to reinstall the App on Devices and make sure to note the additional permissions installation instructions noted in the README.md file for enabling the App to have Camera Access. If using the Web Browser Installation, there is no need to reinstall the app for Camera access.

    v3.5.0

    Upgrade instructions:

    Backup your data folder and then run the following commands.

    git fetch origin
    +git checkout v3.5.0
    +./start.sh v3.5.0
    +docker exec -it tangerine /tangerine/server/src/upgrade/v3.5.0.js 
    +

    If any of your on-change logic looks into a form item's contents using tangyFormItemEl.shadowRoot.querySelector(...) or this.$.content.querySelector(...), you must change it. The contents of the form can now be accessed at tangyFormItemEl.querySelector(...).

    Also, the content element is no longer available.

    For example:

    // replace
    +var el = this.$.content.querySelector('tangy-input[name=\'classId\']')
    +//with 
    +var el = this.querySelector('tangy-input[name=\'classId\']')
    +

    The advantage of moving this content out of the shadow DOM is that you can now style it directly from your app.

    v3.4.0

    • New Features
    • New groups now ordered by creation date: Creating new groups will now order them by the date the were created in the group list. #1584
    • Configurable Web App Device Orientation: You can now specify the Web App orientation (portrait, landscape, or any) on device using the T_ORIENTATION variable in config.sh. Add T_ORIENTATION="any" to config.sh to have more flexible orientations for PWA's. The options for T_ORIENTATION are at https://developer.mozilla.org/en-US/docs/Web/Manifest/orientation
    • Media Library and Image support for Forms: Each group now has a media library tab where they can uplaod images which can then be utilized when inserting the new "Image" item on forms. #1138
    • New ACASI widget: The ACASI widget is braodly based on the EFTouch widget, but focused on a more static presentation of images and sounds. #56
    • Configurable font size in grids: You may now configure the font size in tangy-timed and tangy-untimed grids using the Option Font Size input. In tangy-form, it is exposed as option-font-size. Example of generated code: <tangy-timed required columns="3" duration=80 name="class1_term2" option-font-size="5">
    • Auto-stop for tangy-radio-buttons: Add support for autostop in tangy-radio-buttons #49. In Editor, set the Threshold to the number of incorrect answers: screenshots. Autostop is implemented by using the hideInputsUponThreshhold helper, which takes a tangy-form-item element and compares the number of correct radio button answers to the value in its incorrect-threshold attribute. Example of generated code: <tangy-form-item id="item1" incorrect-threshold="2">
    • New "correct" attribute for radio button options: A new "correct" attribute has been added to tangy-list-item to store the correct value. There is a "Correct" checkbox next to each option. Example of generated code: <tangy-radio-buttons name="fruit_selection2" label="What is your favorite fruit?"> <option name="tangerine">Tangerine</option> <option name="cherry" correct>Cherry</option> </tangy-radio-buttons>
    • Fixes
    • Critical Sync and "data loss" fix: Some variants of v3.3.x saw cases where data seemed to be lost on the tablet and sync no longer worked. After this release is deployed to the server, release for your groups and instruct all tablets to upgrade. The upgrade process may take many minutes depending on the amount of data stored on the tablet due to a schema update in the database. For an in depth look at what this update does, see the code here.
    • Logstash Improvements #1516
      • User profiles were in a nested object, now they have been merged to be flat in the logstash output doc. See example here.
      • If a form response uses a location element, it will now be extracted out into a top level "geoip" property whose value is an object with "lat" and "lon" properties. See example here.
      • When new forms are created in the editor, they will no longer have a . character in their ID. This was causing some uneccessary and confusing logic in logstash config files. See PR here.
    • EFTouch: A large number of fixes have been made for EFTouch. See recent issues here.
    • Updated to tangy-form-editor ^5.18.0 for Change grid variables in CSV starting with variable_0 to variable_1.
    • A previous update to tangy-form to 3.15.1, tangy-form-editor to 5.17.0 to fixed Editing form level HTML requires two Save clicks
    • Beta Features
    • Two-way Sync: Allows for two-way sync of form responses. Can be configured to two way sync form responses for specific forms and also by geographic region defined in the user profile. See docs/feature-two-way-sync.md. and Add a tangy input inside a tangy box duplicates items, and enable Adjustable letter size for grids
    • Case Module
      • Add the "case" module to T_MODULES in config.sh and the default landing page for a group will be the cases search page and new "Case Management Editor" tab will appear in groups for creating and editing Case Definitions. #1517
      • Clientside search of Forms for Case Management Groups allows Cases to be found using the device camera to scan a QR code. See docs/case-management-group.md.
      • Add event time and scheduling to Case Mangement Groups #1518
      • New layout for Case and Case Event pages.

    Upgrade instructions:

    Backup your data folder and then run the following commands.

    git fetch origin
    +git checkout v3.4.0
    +

    ./start.sh
    +

    v3.3.1

    This release fixes a feature that made it into v3.3.0 but had a bug and was disabled. This release fixes that bug and makes it available.

    • As an Editor user I want to be able to do an initial import of my location structure. #1117

    v3.3.0

    • Features
    • Assessor reviews high level case variables, AKA "Case Manifest" #1399
    • Assessor changes language setting to Russian #1402
    • Untimed Grid subtest #1366
    • Editor Style Upgrades (April 2019) #1421
    • Group Names can now have spaces and special characters #1424
    • Editor configures Timed Grid to show or hide labels on buttons #1432
    • Server Admin tunes the reporting delay between when an upload occurs and it shows up in reporting outputs #1441
    • CSV output for single checkboxes now show up as "0" and "1" as opposed to "" and "on" #1367
    • CSV output for single radiobuttons now show up as "0" and "1" as opposed to "null" and "on" #1433
    • You can now limit who can add/see sitewide users to only the USER1 account by setting T_USER1_MANAGED_SERVER_USERS to "true" in config.sh #1381.
    • Client now has an "About" page with details about what Tangerine is #1465.

    Upgrade instructions:

    Backup your data folder and then run the following commands.

    git fetch origin
    +git checkout v3.3.0
    +./start.sh
    +docker exec -it tangerine /tangerine/server/src/upgrade/v3.3.0.js 
    +

    v3.2.0

    • Features
    • Assessor changes language of App #1315
    • Editor provides feedback given data entered earlier in the form #1384
    • Assessor starts new Case is immediately forwarded to first form #1362
    • Assessor finds Form and Event in Case has been disabled/enabled due to custom logic #1363
    • Assessor confirms participant info using data from another form #1385
    • Server Admin restarts machine to find containers have automatically come back up #1388
    • Server Admin sets up Tangerine outage alarm #1389
    • Developer Notes
      • Ability to define database views on a per module basis in Client Angular #1419
      • Integrate test harness and TypeScript with server using NestJS #1413
      • Fix client tests, organize shared services and guards into the shared module, move client/app/ to client/ #1398

    Upgrade instructions:

    git fetch origin
    +git checkout v3.2.0
    +./start.sh
    +docker exec tangerine /tangerine/upgrades/v3.2.0.js 
    +
    - In each group's app-config.json, change "direction" to "languageDirection". - If using a translation other than English, change in each group's app-config.json, change "languageCode" to the corresponding language code. Current codes other than en for English is JO_ar for Jordanian and KH_km for Khmer.

    v3.1.0

    • Features
    • Item Editor UX Improvements #810
    • Assessor verifies correct location selected by reviewing metadata of location #1191
    • As an assessor I'd like to include a hint option to be displayed below the question text #1279
    • Grids: helper functions for grids #1183
    • Ability to mark an entire row as incorrect on grids #1333
    • Assessor's backed up form responses are archived when storage is filling up #1304
    • Assessor scans a QR Code into form #1309
    • All hidden inputs have reporting values of "999" #1349
    • Merge reporting output of radiobuttons into one column.
    • Bug fixes
    • Editor not properly logging users out resulting in getting stuck every 24 hours #1314
    • Min and Max for input number cannot be saved through the interface #1297
    • time on grids cannot be changes and is always 60 seconds #1301
    • Unclosed tags in html container can break form #1289
    • Tangy timed option values disappear #1302

    Note that #1349 will bve optional in future releases and you may not want to upgrade until that time.

    Upgrade instructions:

    git fetch origin
    +git checkout v3.1.0
    +./start.sh
    +docker exec tangerine reporting-cache-clear
    +

    v3.0.0-beta13

    Upgrade instructions from v3 betas

    git fetch origin
    +git checkout v3.0.0
    +# Note the new T_UPLOAD_TOKEN variable which is a replacement for the old upload account variables.
    +mv config.sh config.sh_backup
    +cp config.defaults.sh config.sh
    +vim config.sh
    +./start.sh
    +docker exec tangerine push-all-groups-views
    +docker exec tangerine reporting-cache-clear
    +

    For existing groups, you need to edit their app-config.json files in the ./data/client/content/groups folders. Replace them with the following template and make sure to update variables such as groupName, uploadToken, and serverUrl.

    {
    +   "listUsernamesOnLoginScreen" : true,
    +   "modules" : [ ],
    +   "groupName" : "pineapple",
    +   "securityQuestionText" : "What is your year of birth?",
    +   "hideProfile" : false,
    +   "direction" : "ltr",
    +   "columnsOnVisitsTab" : [],
    +   "hashSecurityQuestionResponse" : false,
    +   "uploadUnlockedFormReponses" : false,
    +   "uploadToken" : "change this to match T_UPLOAD_TOKEN in config.sh",
    +   "securityPolicy" : [
    +      "password"
    +   ],
    +   "homeUrl" : "case-management",
    +   "serverUrl" : "https://f571f419.ngrok.io/",
    +   "centrallyManagedUserProfile" : false,
    +   "registrationRequiresServerUser" : false
    +}
    +

    New features since v3.0.0-beta12

    • Server admin imports client archives into server #1166
    • After exporting data from clients, we now have an easy command line tool to import them. Place those exported files in ./data/archives folder and then run docker exec tangerine import-archives.
    • Consumers of reporting API find user profile data appended to form responses #1147
    • New logstash module for installations that want to use logstash to migrate data to an Elastic Search instance.
    • Enable by adding logstash to the list of modules in config.sh, then clear reporting caches docker exec -it tangerine bash; cd /tangerine/server/src/scripts; ./clear-all-reporting-cache.js;. You will find new <groupName>-logstash databases in CouchDB that you can configure logstash to consume.
    • Upload Tokens instead of upload usernames and passwords.
    • In your config.sh change T_UPLOAD_TOKEN to a secret phrase and then in existing groups add that to app-config.json as an "uploadToken" property and uploadUrl to serverUrl but without the username and password and upload/<groupName>. For example, "uploadUrl": "http://uploader:password@foo.tangerinecentral.org/upload/foo" would become "serverUrl":"http://foo.tangerinecentral.org", "groupName":"foo", "uploadToken":"secret_foo_passphrase".
    • If you not planning on updating clients right away, in config.sh set T_LEGACY="true" to support the older upload API that those clients expect. When all clients are upgraded, set that variable back to false.
    • Editor edits location list for group #982
    • @TODO
    • Editor creates, edits, and deletes form responses on the server #1047
    • Editor exports CSV of a form for a month of their choosing #1143
    • Editor sees user profile form related columns joined to CSV of all forms #1142
    • On client, prevent users from editing their own profile.
    • To impact new groups, change T_HIDE_PROFILE to "true" in config.sh .
    • To modify existing groups, change "hideProfile" in group level app-config.json to true.
    • Assessor registers on tablet, downloads form responses created on server #1129
    • On device registration, after user creates account, will force user to enter 6 character code that references online account.
    • To impact new groups, change T_REGISTRATION_REQUIRES_SERVER_USER to "true".
    • To modify existing groups, change "registrationRequiresServerUser" in group level app-config.json to true.
    • Editor updates client user profile on server, Assessor sees updated profile after next sync #1134
    • On client sync, will result in any changes made to a user profile on the server to be downloaded and reflected on the client.
      • To impact new groups, change T_CENTRALLY_MANAGED_USER_PROFILE to "true" in config.sh.
      • To modify existing groups, change "centrallyManagedUserProfile" in group level app-config.json to true.
    • Editor views tangy-timed items_per_minute calculation in the CSV #1100
    • <tangy-location> can be filtered by entries in the profile by adding attribute <tangy-location filter-by-global>. In the editor when editing a <tangy-location> you will find a new option "Filter by locations in the user profile?" you can check.
    • Advanced forms features (no GUI for these features)
    • <tangy-input-group> can be used to create repeatable groups of inputs. See the demo here.
    • Geofence for v3 #941
      • If you location list has latitude and longitude properties for each location, you can validate your <tangy-location> selection given a geofence in <tangy-gps>. See the screenshots here and a code example of how to build this in your form here.
    • Upload incomplete form responses (important for Class module)
    • To modify existing groups, set "uploadUnlockedFormReponses" to true in app-config.json.
    • Server Admin clears reporting cache #1064
    • Server Admin runs script to update views in databases #962
    • Server Admin limits by site or by group the number of form responses uploaded end up in reporting outputs #1155
    • This feature brings two new settings to config.sh.
    • Set T_PAID_MODE to "site" to limit on a sitewide level, use "group" to limit on a per group level.
    • Set T_PAID_ALLOWANCE from "umlimited" to a specific number like "1000" to limit form responses that end up in reporting outputs to one thousand.
    • This mechanism works by marking uploaded form responses as "paid". When you first upgrade to this release, none of your form responses will be marked as paid and will not end up in reporting outputs until they are marked as paid against the allowance. If you want to mark all current uploaded form responses as paid and only mark against their allowance for future uploads, set the allowance to unlimited and after the reporting caches have been built, set the allowance desired and run ./start.sh again.
    • Optional Modules you can turn on and off in config.sh T_MODULES list.
    • Note that if you are going to override the default T_MODULES list with an additional module such as class, don't forget to add modules such as csv if you still need them!
    • Reporting outputs (inluding CSVs) include the information about the number of children a location has. #1174

    Known issues

    • Memory leak results in Error: spawn ENOMEM #886
    • On the server command line run crontab -e and then add the following entry to restart the program every 24 hours 0 0 * * * docker stop tangerine; docker start tangerine.

    2.0.0 (pre-release)

    User Stories

    • As a Tangerine Database admin, I want to control which users have the "Manager" role for creating new groups #218
    • As a Tangerine Editor User, I expect to see timestamps on CSVs down to the second #223
    • As a user, if I end up on a http:// URL I want to be redirected to the https:// version of that URL #98

    Bugs

    • New groups default Client tabs are set up for workflow, should be vanilla tangerine #230
    • When Tangerine is first installed, User1 does not have the required Manager role so groups cannot be created #229
    • School Location Subtest does not render after upgrading from Tangerine 0.4.x to v2.0.0 #189
    • If a group was upgraded from 0.x.x and does not have a media folder, APK generating fails #186
    • Deleting group does not set security correctly on resulting "deleted" database #227
    • Large CSVs fail to generate #221
    • When a new Workflow is created it is missing retrictToRole, reporting, and authenticityParameters #228
    • Ensure /var/log/couchdb exists so CouchDB does not crash #216

    Technical

    • Document how to use SSL with Tangerine #219
    • Things to add to .gitignore #185
    • Clean up build process so client does not need to compile twice #74

    Upgrade directions

    • This is the first release with upgrade scripts so you will need to run all upgrade scripts between the version you started at and this one.

    For example, if you are at Tangerine 0.4.6, then you must run...

    docker exec -it tangerine-container /tangerine-server/upgrades/v1.0.0.sh
    +docker exec -it tangerine-container /tangerine-server/upgrades/v2.0.0.sh
    +

    If you are at Tangerine 1.7.8, then you must run...

    docker exec -it tangerine-container /tangerine-server/upgrades/v2.0.0.sh
    +

    v2.2.0

    User Stories

    • As a Site Owner I want to know how many results have been uploaded given arbitrary time period #457

    Technical

    • Refactor start.sh and config.defaults.sh to allow configurable ports and tag #456

    Upgrade direcections

    docker exec -it tangerine-container /tangerine-server/upgrades/v2.2.0.sh
    +
    \ No newline at end of file

Using APK Device Setup Installation