diff --git a/.meteor/packages b/.meteor/packages index 6922c999..7c6939b9 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -13,12 +13,12 @@ jquery@1.11.10 # Helpful client-side library tracker@1.2.0 # Meteor's client-side reactive programming library es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers. -ecmascript@0.12.4 # Enable ECMAScript2015+ syntax in app code +ecmascript@0.12.7 # Enable ECMAScript2015+ syntax in app code insecure@1.0.7 # Allow all DB writes from clients (for prototyping) accounts-password@1.5.1 twbs:bootstrap -iron:router +iron:router@=1.1.1 random@1.1.0 reload@1.3.0 check@1.3.1 @@ -36,7 +36,7 @@ dburles:google-maps standard-minifier-css@1.5.3 standard-minifier-js@2.4.1 mdg:validated-method -fourseven:scss +fourseven:scss@4.12.0 oauth@1.2.8 nooitaf:colors oauth1@1.2.2 @@ -55,4 +55,7 @@ dynamic-import@0.5.1 useraccounts:iron-routing aldeed:template-extension useraccounts:bootstrap -underscore +underscore@1.0.10 +mrt:cookies@=0.3.0 +fortawesome:fontawesome +dburles:collection-helpers diff --git a/.meteor/release b/.meteor/release index 97064e19..670d54bf 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@1.8.1 +METEOR@1.8.1-issue-10516.0 diff --git a/.meteor/versions b/.meteor/versions index 810ebfaf..3f2b23fc 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -44,6 +44,7 @@ cfs:upload-http@0.0.20 cfs:worker@0.1.5 check@1.3.1 coffeescript@1.0.17 +dburles:collection-helpers@1.1.0 dburles:google-maps@1.1.5 ddp@1.4.0 ddp-client@2.3.3 @@ -53,7 +54,7 @@ ddp-server@2.3.0 deps@1.0.12 diff-sequence@1.1.1 dynamic-import@0.5.1 -ecmascript@0.12.4 +ecmascript@0.12.7 ecmascript-runtime@0.7.0 ecmascript-runtime-client@0.8.0 ecmascript-runtime-server@0.7.1 @@ -64,7 +65,8 @@ es5-shim@4.8.0 fetch@0.1.1 force-ssl@1.1.0 force-ssl-common@1.1.0 -fourseven:scss@3.13.0 +fortawesome:fontawesome@4.7.0 +fourseven:scss@4.12.0 geojson-utils@1.0.10 hot-code-push@1.0.4 html-tools@1.0.11 @@ -79,7 +81,7 @@ iron:dynamic-template@1.0.12 iron:layout@1.0.12 iron:location@1.0.11 iron:middleware-stack@1.1.0 -iron:router@1.1.2 +iron:router@1.1.1 iron:url@1.1.0 jquery@1.11.11 launch-screen@1.1.1 @@ -104,6 +106,7 @@ mongo-decimal@0.1.1 mongo-dev-server@1.1.0 mongo-id@1.0.7 mongo-livedata@1.0.12 +mrt:cookies@0.3.0 mrt:moment@2.8.1 mrt:moment-timezone@0.2.1 msavin:mongol@2.0.1 @@ -156,7 +159,7 @@ url@1.2.0 useraccounts:bootstrap@1.14.2 useraccounts:core@1.14.2 useraccounts:iron-routing@1.14.2 -webapp@1.7.3 +webapp@1.7.4-issue-10516.0 webapp-hashing@1.0.9 xolvio:cleaner@0.3.3 zodiase:function-bind@0.0.1 diff --git a/Meteor b/Meteor new file mode 100644 index 00000000..e69de29b diff --git a/README.md b/README.md index 6398b450..6eeaac34 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,115 @@ -# CE Platform -The Collective Experience (CE) Platform facilitates the creation and operation of collective experience applications. By building to cordova/iOS and distributing this project as a native app, experiences can be launched with native push notifications and users can be targeted by their location for context-specific experiences. Currently, the platform facilitates image and text submissions for experiences. +## Collective Narrative -## Setup and Local Development +Collective Narrative is an extension of CE focused on collaborative storytelling. It allows authors to concisely write story "scripts" that are collaboratively played out and developed by participants. Currently, the repo contains "Murder Mystery," a CN script that allows three participants in different coffee shops to engage in a murder mystery centered around their current contexts. The following instructions detail how to test the murder mystery CN, how to write your own CN script, and how to continue development of the CN project. + +## CN Setup and Local Development 1. Install Meteor `curl https://install.meteor.com/ | sh`. 2. Clone the repository `git clone https://github.com/NUDelta/ce-platform.git`. 3. Navigate to the project folder `cd ce-platform`. 4. Run `meteor npm install` to install local dependencies. 5. Start the server using `meteor`. +6. Starting the server will also generate a python script for location testing. Copy and paste this script (labeled `FOR LOCATION TESTING RUN >>>>`) into a new terminal window to simulate users at specific locations. +7. Navigate to `http://localhost:3000/` in your web browser to view the experience. +8. For editing the codebase, we recommend using VS Code. + +### Murder Mystery Testing + +#### Local Testing + +For testing the murder mystery CN locally, use the following account credentials when signing in: + +1. username `meg`, password `password` +2. username `andrew`, password `password` +3. username `josh`, password `password` + +All three users need to sign in and participate in the murder mystery pre-story questionnaire before casting will begin. You can use different browsers or Private/Incognito mode to test the synchronous chat feature on a single computer. These default users can be modified in `testingconstants.js`. + +To test locally, you also need to ensure `host = "http://localhost:3000"` is uncommented in `simulatelocations.py`, and the other `host` definitions are commented. + +#### In-App Testing + +For the final Fall 2019 app, download the .ipa file [https://drive.google.com/file/d/13PUh_mMo_p1OhoEN-pidSyQtap5UC_ei/view?usp=sharing](here), then upload it to [diawi.com](www.diawi.com) to download it onto your iPhone. + +Then, go to [https://staging-ce-platform.herokuapp.com](https://staging-ce-platform.herokuapp.com) and open the browser console. From here, run the following commands: + +1. `Meteor.call(β€œfreshDatabase")` +2. `Meteor.call("createTestUsers")` + +Next, ensure `host = "https://staging-ce-platform.herokuapp.com"` is uncommented in `simulatelocations.py`, and the other `host` definitions are commented. + +Then, log in to [mhub.com](mhub.com) using the CE account credentials and navigate to the `users` collection within the `staging-ce-platform` database. Record the three `_id` values created for each test user. + +Lastly, run `python simulatelocations.py`, adding the `_id` values at the end. For example, `python simulatelocations.py ZrCZFhtfDStrXBGF2 K9ruF6p3uyrs7joR9 R7qfjYMJvyao8W2gN`. + +##### In-App Testing Notes + +If you've made changes to the code, you'll need to commit them and then run `git push staging collective-narrative:master` within the `ce-platform`directory to deploy these lastest changes to Heroku. + +Also, the .ipa link above should work even after you make changes to the CN-specific codebase, so don't worry about compiling a new iOS app every time you modify CN. If you do need to re-compile, follow the CE instructions copied in-part at the bottom of this readme. + +## CN Authoring Syntax + +At the core of the CN project is a concise syntax that makes authoring a CN accessible and easy. The syntax used to generate Murder Mystery can be viewed and modified in `cn.js`, which can be found at "imports" -> "api" -> "testing." Following are instructions on how to write your own CN, assuming you start with a blank `cn.js`. + +1. Start by declaring an export function. You must use the following name and syntax for the CN to be compiled properly: +```js +export const cn = () => { + +} +``` +2. Fill in the function with the eight required parameters of a CN. The first five are defined immediately while the last three start as empty arrays. The murder mystery CN can be referenced for examples of these parameters. Copy the parameters for the `templates` array exactly, as it refers to specific HTML templates currently required for CN. +```js +let title = 'Name of the CN' +let description = 'Description of the CN, displayed on the Home tab of the Cerebro app' +let notification = "Notification sent to the user's phone when the CN appears in their app" +let setting = ['CE detector used to trigger the CN', 'description of what user context the detector refers to'] +let templates = ['CNstart', 'CNchat'] +let questions = [] +let characters = [] +let prompts = [] +``` +3. Define the pre-story questions. Each participant is asked these questions after they agree to participate in a CN. They can be used to further understand the contexts of each participant, allowing for a more engaging story. Each question is an object with three fields: +```js +let questionName = { + question: 'The question itself', + responseType: 'text or dropdown', + responseData: 'name of variable to store the text answer, or an array of the dropdown options' +} +``` +Remember to update the `questions` array with all of your questions objects. + +4. Define the characters that participants will be cast as. Currently, they'll define what kind of instruction the participant receives at the beginning of the synchronous chat. These are also objects with three fields: +```js +let characterName = { + roleName: 'name of the role', + instruction: 'Instructions given to the participant who is cast as this role. This is sent as a private message to the participant, so others cannot see it.', + context: ['An array containing the various user contexts, derived from question answers, that define if a participant is cast as this character'], + max: integer, defining how many people should be cast as this character +} +``` +Remember to update the `characters` array with all of your character objects. + +5. Define the prompts. These will be sent by a narrator in the group chat to all participants. They are crucial for building the narrative and guiding participants. They are also objects with three fields: +```js +let promptName = { + prompt: 'The prompt sent by the narrator', + info: 'Optional: the name of the responseData variable used in the prompt. It is appended to the end of the prompt.', + timing: integer, representing the number of seconds after casting occurs, used to time when the prompt is sent +} +``` +Remember to update the `prompts` array with all of your prompt objects. + +6. At the end of your function, return an array containing all of the parameters, in this order: +```js +return [title, description, notification, setting, templates, questions, characters, prompts]; +``` + +### Notes + +- Templates currently conform to the CE concepts of `participateTemplate` and `resultsTemplate`, which are the HTML templates used to structure experiences in the Cerebro app. The goal is to have many different templates that satisfy different aspects of storytelling, allowing the author to use as many as they want to construct a story. But for now, exactly two templates need to be used, and only two are currently defined. The `CNstart` template allows for the pre-story questions that can further establish context before a CN. The `CNchat` template allows for synchronous storytelling experiences between participants and an author-defined narrator. +- While experiences are currently fixed at three participants, the number will eventually be author-defined. + +## Additional CE Setup Notes ### Windows Subsystem for Linux Specific Setup @@ -61,8 +164,8 @@ Exporting an iOS application as an `.ipa` file requires the `ceEnterpriseExport. ```xml provisioningProfiles - edu.northwestern.delta.A - Delta Lab A + edu.northwestern.delta.D + Delta Lab D ``` ```xml @@ -74,7 +177,7 @@ Push notifications are currently configured to work with the Enterprise A certif #### Export 1. Navigate to `../Cerebro-ios/ios/project` and open the `.xcworkspace`. -2. Change the Bundle Identifier to the same identifier as in the provisioning profile above (here, `edu.northwestern.delta.A`). +2. Change the Bundle Identifier to the same identifier as in the provisioning profile above (here, `edu.northwestern.delta.D`). 3. Copy `ceEnterpriseExport.sh` and `exportOptions.plist` to the same directory as the `.xcworkspace`. Then, run `./ceEnterpriseExport.sh` to create the application. 4. The `.ipa` can be found in the `Cerebro-export/` directory. Distribute your `.ipa` to testers using [diawi.com](www.diawi.com). diff --git a/Started b/Started new file mode 100644 index 00000000..e69de29b diff --git a/ceEnterpriseExport.sh b/ceEnterpriseExport.sh old mode 100755 new mode 100644 diff --git a/client/head.html b/client/head.html index 5a320bba..d62af2d6 100644 --- a/client/head.html +++ b/client/head.html @@ -25,7 +25,6 @@ - diff --git a/client/main.js b/client/main.js index 674910a0..4e0251da 100644 --- a/client/main.js +++ b/client/main.js @@ -3,6 +3,7 @@ import '/imports/startup/client'; //TODO: what's all this? Do we need to add availabliity? if (Meteor.isDevelopment) { Schema = require('../imports/api/schema.js').Schema; + Messages = require('../imports/api/messages/messages.js').Messages; Experiences = require('../imports/api/OCEManager/OCEs/experiences.js').Experiences; Images = require('../imports/api/ImageUpload/images.js').Images; Incidents = require('../imports/api/OCEManager/OCEs/experiences.js').Incidents; diff --git a/exportOptions.plist b/exportOptions.plist index 89750d6f..aa9a49dd 100644 --- a/exportOptions.plist +++ b/exportOptions.plist @@ -8,8 +8,8 @@ enterprise provisioningProfiles - edu.northwestern.delta.A - Delta Lab A + edu.northwestern.delta.D + Delta Lab D signingCertificate iPhone Distribution diff --git a/imports/api/ImageUpload/server/publications.js b/imports/api/ImageUpload/server/publications.js index f826983a..98d3f616 100644 --- a/imports/api/ImageUpload/server/publications.js +++ b/imports/api/ImageUpload/server/publications.js @@ -1,4 +1,4 @@ -import { Meteor } from 'meteor/meteor'; +// import { Meteor } from 'meteor/meteor'; import { Images, Avatars } from '../images.js'; Meteor.publish('images.activeIncident', function (incidentId) { diff --git a/imports/api/Logging/groundtruth_log/methods.js b/imports/api/Logging/groundtruth_log/methods.js index f445a7e1..35eace1e 100644 --- a/imports/api/Logging/groundtruth_log/methods.js +++ b/imports/api/Logging/groundtruth_log/methods.js @@ -1,4 +1,4 @@ -import {Meteor} from "meteor/meteor"; +// import {Meteor} from "meteor/meteor"; import { Locations } from "../../UserMonitor/locations/locations"; import {Groundtruth_log} from "./groundtruth_log"; diff --git a/imports/api/Logging/page_log/methods.js b/imports/api/Logging/page_log/methods.js index b525c15b..e0b62189 100644 --- a/imports/api/Logging/page_log/methods.js +++ b/imports/api/Logging/page_log/methods.js @@ -1,4 +1,4 @@ -import {Meteor} from "meteor/meteor"; +// import {Meteor} from "meteor/meteor"; import {Page_log} from "./page_log"; diff --git a/imports/api/OCEManager/OCEs/experiences.js b/imports/api/OCEManager/OCEs/experiences.js index 2fd81891..9fc5e8fb 100644 --- a/imports/api/OCEManager/OCEs/experiences.js +++ b/imports/api/OCEManager/OCEs/experiences.js @@ -1,4 +1,4 @@ -import { Meteor } from 'meteor/meteor'; +// import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { SimpleSchema } from 'meteor/aldeed:simple-schema'; @@ -13,6 +13,10 @@ Schema.Callback = new SimpleSchema({ }, function: { type: String + }, + chapter: { + type: Object, + optional: true, } }); @@ -26,6 +30,7 @@ Schema.SituationDescription = new SimpleSchema({ detector: { type: String }, + // sameTimeNumber (2 would mean that the situation requires 2 people to be in the situation at the same time) number: { type: Number, } @@ -46,6 +51,10 @@ Schema.NeedType = new SimpleSchema({ type: String, optional: true, }, + participateTemplate: { + type: String, + optional: true, + }, situation: { type: Schema.SituationDescription }, @@ -57,6 +66,13 @@ Schema.NeedType = new SimpleSchema({ numberNeeded: { type: Number, }, + // this is like a semaphore/mutex mechanism for participate routes as a resource a participant can latch onto + numberAllowedToParticipateAtSameTime: { + // if = 1, then this operates like a mutex + type: Number, + optional: true + // defaults to numberNeeded, like a semaphore. + }, notificationDelay: { type: Number, optional: true, @@ -103,6 +119,7 @@ Schema.Experience = new SimpleSchema({ }, participateTemplate: { type: String, + optional: true }, resultsTemplate: { type: String, diff --git a/imports/api/OCEManager/OCEs/methods.js b/imports/api/OCEManager/OCEs/methods.js index f4542eeb..5b8ebeab 100644 --- a/imports/api/OCEManager/OCEs/methods.js +++ b/imports/api/OCEManager/OCEs/methods.js @@ -1,33 +1,22 @@ -import { Meteor } from 'meteor/meteor'; -import { ValidatedMethod } from 'meteor/mdg:validated-method'; -import { SimpleSchema } from 'meteor/aldeed:simple-schema'; - -import { Experiences } from './experiences.js'; -import { Schema } from '../../schema.js'; -import { getUnfinishedNeedNames } from '../progressorHelper'; -import { matchAffordancesWithDetector, getPlaceKeys, - onePlaceNotThesePlacesSets, placeSubsetAffordances } from "../../UserMonitor/detectors/methods"; - -import { Incidents } from './experiences'; -import { Assignments, Availability } from '../../OpportunisticCoordinator/databaseHelpers'; -import { Submissions } from '../../OCEManager/currentNeeds'; -import {serverLog, log} from "../../logs"; -import {pullUserFromAvailabilityNeedUserMaps} from "../../OpportunisticCoordinator/server/identifier"; - - -/** - * Clears current availabilities for a user given a uid. - * @param uid {string} user to clear data for - */ -export const clearAvailabilitiesForUser = (uid) => { - let availabilityObjects = Availability.find().fetch(); - _.forEach(availabilityObjects, (av) => { - // remove user for each need in each - _.forEach(av.needUserMaps, (needEntry) => { - pullUserFromAvailabilityNeedUserMaps(av._id, needEntry.needName, uid); - }); - }); -}; +// import {Meteor} from 'meteor/meteor'; +import {ValidatedMethod} from 'meteor/mdg:validated-method'; +import {SimpleSchema} from 'meteor/aldeed:simple-schema'; + +import {Experiences} from './experiences.js'; +import {Schema} from '../../schema.js'; +import {getUnfinishedNeedNames} from '../progressorHelper'; +import { + getPlaceKeys, + matchAffordancesWithDetector, + onePlaceNotThesePlacesSets, + placeSubsetAffordances +} from "../../UserMonitor/detectors/methods"; + +import {Incidents} from './experiences'; +import {Assignments, Availability, ParticipatingNow} from '../../OpportunisticCoordinator/databaseHelpers'; +import {Submissions} from '../../OCEManager/currentNeeds'; +import {serverLog} from "../../logs"; +import {setIntersection} from "../../custom/arrayHelpers"; /** * Loops through all unmet needs and returns all needs a user matches with. @@ -109,17 +98,7 @@ export const sustainedAvailabilities = function(beforeAvails, afterAvails) { return sustainedAvailDict; }; -export const setIntersection = function(A, B) { - let A_with_string_elements = A.map((e) => { return JSON.stringify(e)}); - let B_with_string_elements = B.map((e) => { return JSON.stringify(e)}); - let beforeSet = new Set(A_with_string_elements); - let afterSet = new Set(B_with_string_elements); - let intersection = new Set( - [...beforeSet].filter(x => afterSet.has(x))); - - return Array.from(intersection).map((e) => { return JSON.parse(e)}); -}; // TODO: ryan do this plz. /** @@ -305,6 +284,12 @@ export const addContribution = (iid, contribution) =>{ },{ $push: {needUserMaps: {needName: contribution.needName, users: []}} }); + + ParticipatingNow.update({ + _id: iid + },{ + $push: {needUserMaps: {needName: contribution.needName, users: []}} + }); }; export const addEmptySubmissionsForNeed = (iid, eid, need) => { @@ -367,6 +352,11 @@ export const startRunningIncident = (incident) => { _id: incident._id, needUserMaps: needUserMaps }); + + ParticipatingNow.insert({ + _id: incident._id, + needUserMaps: needUserMaps + }); }; /** @@ -383,24 +373,6 @@ export const updateRunningIncident = (incident) => { // FIXME(rlouie): not accessing old need names here, so another function has to do this manually on submissions }); - // clear the user activeIncidents before clearing the availabilities - let old_needUserMaps = Availability.find({_id: incident._id}).needUserMaps; - _.forEach(old_needUserMaps, (needUserMap) => { - if (needUserMap.users.length > 0) { - _.forEach(needUserMap.users, (uid) => { - Meteor.users.update( - { - _id: uid - }, { - $pull: { - "profile.activeIncidents": incident._id - } - }); - // TODO(rlouie): maybe store activeIncidentNeedPlaceDistance info, so then pull incidents/needs like this too - }); - } - }); - Availability.update( { _id: incident._id, @@ -409,7 +381,7 @@ export const updateRunningIncident = (incident) => { needUserMaps: needUserMaps } } - ) + ); Assignments.update( { @@ -419,8 +391,17 @@ export const updateRunningIncident = (incident) => { needUserMaps: needUserMaps } } - ) + ); + ParticipatingNow.update( + { + _id: incident._id, + }, { + $set: { + needUserMaps: needUserMaps + } + } + ) }; @@ -498,6 +479,11 @@ export const getNeedFromIncidentId = (iid, needName) => { let incident = Incidents.findOne(iid); let output = undefined; + if (!incident) { + console.error(`Error in getNeedFromIncidentId: Could not find incident of iid = ${iid}`) + return false; + } + _.forEach(incident.contributionTypes, (need) => { if (need.needName === needName) { output = need; diff --git a/imports/api/OCEManager/OCEs/server/publications.js b/imports/api/OCEManager/OCEs/server/publications.js index 26ef595e..c9754c75 100644 --- a/imports/api/OCEManager/OCEs/server/publications.js +++ b/imports/api/OCEManager/OCEs/server/publications.js @@ -1,4 +1,4 @@ -import { Meteor } from 'meteor/meteor'; +// import { Meteor } from 'meteor/meteor'; import { Experiences, Incidents } from '../experiences.js'; Meteor.publish('experiences.all', function () { @@ -18,7 +18,7 @@ Meteor.publish('experiences.activeUser', function () { const user = Meteor.users.findOne(this.userId); let experienceIds = Incidents.find({ - _id: { $in: user.profile.activeIncidents } + _id: { $in: user.activeIncidents() } }).fetch().map((x) => { return x.eid }); @@ -69,11 +69,11 @@ Meteor.publish('incidents.all', function () { }); Meteor.publish('incidents.single', function (incidentId) { - return Incidents.find(incidentId); + return Incidents.find({_id: incidentId}); }); Meteor.publish('incidents.byId', function (incidentId) { - return Incidents.find(incidentId); + return Incidents.find({_id: incidentId}); }); Meteor.publish('incidents.activeUser', function () { @@ -84,7 +84,7 @@ Meteor.publish('incidents.activeUser', function () { } else { const user = Meteor.users.findOne(this.userId); return Incidents.find({ - _id: { $in: user.profile.activeIncidents } + _id: { $in: user.activeIncidents() } }); } }); diff --git a/imports/api/OCEManager/progressor.js b/imports/api/OCEManager/progressor.js index 6b76ce57..bdaea1b2 100644 --- a/imports/api/OCEManager/progressor.js +++ b/imports/api/OCEManager/progressor.js @@ -27,6 +27,7 @@ Meteor.methods({ }); export const updateSubmission = function(submission) { + console.log("update submission"); Submissions.update( { eid: submission.eid, @@ -53,15 +54,28 @@ export const updateSubmission = function(submission) { //checks the triggers for the experience of the new submission and runs the appropriate callbacks 5 function runCallbacks(mostRecentSub) { + console.log("running runCallBacks"); // need `cb` since all the callbacks called in the eval references this manager let cb = new CallbackManager(mostRecentSub); let callbackArray = Incidents.findOne(mostRecentSub.iid).callbacks; _.forEach(callbackArray, callbackPair => { + console.log("new submission, now checking for callbacks"); let trigger = callbackPair.trigger; let fun = callbackPair.function; if (eval(trigger)) { + // let chapter = callbackPair.chapter; + // console.log("chapter in callback is " + JSON.stringify(chapter)) + // console.log("chapter in callback 2is " + chapter) + // console.log ("cn flag: " + mostRecentSub.needName.substring(0,2)) + // if (mostRecentSub.needName.substring(0,2) == "cn") { + // console.log ("identifed CN") + // console.log("fun is " + fun) + // eval("(" + fun + "(" + chapter + "," +JSON.stringify(mostRecentSub) + "))"); + // } + console.log("callback triggered"); + console.log("fun: " + "(" + fun + "(" + JSON.stringify(mostRecentSub) + "))"); eval("(" + fun + "(" + JSON.stringify(mostRecentSub) + "))"); } }); @@ -82,6 +96,7 @@ class CallbackManager { //trigger used in callbacks: checks if the new sub was for the specified need newSubmission(needName) { + console.log("new submission"); if (needName === undefined) { return true; } else { @@ -89,6 +104,10 @@ class CallbackManager { } } + anyCharDead() { + return this.submission.content["anyCharDead"]; + } + //trigger used in callbacks: checks if the need is finished needFinished(needName) { return numUnfinishedNeeds(this.submission.iid, needName) === 0; @@ -119,6 +138,34 @@ class CallbackManager { } } + /** + * Assumes that submission takes the schema + * { + * iid: + * user: + * info: { + * sentence: "Fine, here is the sword." + * action: true + * } + * } + * @param chapterName + */ + chapterEnd(chapterName) { + // check if recent submission is the end of a chapter + if (this.submission.info.action === true) { + return true; + } + + return false; + + // the process of searching through the database of submissions for the one with the matching iid + // return Submissions.find({ + // iid: this.submission.iid, + // needName: chapterName, + // uid: { $ne: null } + // }) + } + //trigger used in callbacks: returns minutes since the first need was submitted. Additionally for this trigger, in runCallbacks we need to set a timer to run the function in the future timeSinceFirstSubmission(need) { return number; //minutes diff --git a/imports/api/OCEManager/progressor.tests.js b/imports/api/OCEManager/progressor.tests.js index 8d98fdfc..19b63ace 100644 --- a/imports/api/OCEManager/progressor.tests.js +++ b/imports/api/OCEManager/progressor.tests.js @@ -83,7 +83,7 @@ describe('Progressor Tests - Single Submission', function() { it('should remove the incident from active incidents in users profile', function() { const user = Meteor.users.findOne({_id: submissionObject.uid}); - chai.assert.isFalse(user.profile.activeIncidents.includes(submissionObject.iid), + chai.assert.isFalse(user.activeIncidents().includes(submissionObject.iid), 'active incident not removed from user profile'); }); diff --git a/imports/api/OCEManager/server/publications.js b/imports/api/OCEManager/server/publications.js index 673d60f6..110b9a7d 100644 --- a/imports/api/OCEManager/server/publications.js +++ b/imports/api/OCEManager/server/publications.js @@ -1,4 +1,4 @@ -import { Meteor } from 'meteor/meteor'; +// import { Meteor } from 'meteor/meteor'; import { Submissions } from '../currentNeeds.js'; Meteor.publish('submissions.activeIncident', function (incidentId) { diff --git a/imports/api/OpportunisticCoordinator/databaseHelpers.js b/imports/api/OpportunisticCoordinator/databaseHelpers.js index 57613a90..3c288bf8 100644 --- a/imports/api/OpportunisticCoordinator/databaseHelpers.js +++ b/imports/api/OpportunisticCoordinator/databaseHelpers.js @@ -1,6 +1,7 @@ import { Mongo } from "meteor/mongo"; import { SimpleSchema } from 'meteor/aldeed:simple-schema'; import { Schema } from '../schema.js'; +import {Location_log} from "../Logging/location_log"; Schema.Assignment = new SimpleSchema({ _id: { @@ -23,7 +24,9 @@ Schema.UserNeedMapping = new SimpleSchema({ type: String }, users: { - type: [Object], // {uid: String, place: String, distance: Number} + // For Availability and Assignments, this object can look like {"uid": uid, "place": place, "distance": distance} + // For ParticipatingNow, this object looks like {"uid": uid} + type: [Object], defaultValue: [], blackbox: true }, @@ -47,3 +50,20 @@ Schema.Availability = new SimpleSchema({ export const Availability = new Mongo.Collection('availability'); Availability.attachSchema(Schema.Availability); + +Schema.ParticipatingNow = new SimpleSchema({ + _id: { + type: String, + optional: true, + regEx: SimpleSchema.RegEx.Id, + }, + needUserMaps: { + type: [Schema.UserNeedMapping], + blackbox: true, + //TODO: this shouldn't be blackbox true, figure out why it's not doing its thang + }, + +}); + +export const ParticipatingNow = new Mongo.Collection('participating_now'); +ParticipatingNow.attachSchema(Schema.ParticipatingNow); diff --git a/imports/api/OpportunisticCoordinator/identifier.js b/imports/api/OpportunisticCoordinator/identifier.js new file mode 100644 index 00000000..c43a4222 --- /dev/null +++ b/imports/api/OpportunisticCoordinator/identifier.js @@ -0,0 +1,7 @@ + + +import { Availability } from "./databaseHelpers"; + +export const getUserAvailabilities = (uid) => { + return Availability.find({"needUserMaps.users.uid": uid}); +} diff --git a/imports/api/OpportunisticCoordinator/populateDatabase.js b/imports/api/OpportunisticCoordinator/populateDatabase.js index d45bbdbe..feadf7e7 100644 --- a/imports/api/OpportunisticCoordinator/populateDatabase.js +++ b/imports/api/OpportunisticCoordinator/populateDatabase.js @@ -15,7 +15,6 @@ export const insertTestUser = (username) => { user.profile.lastParticipated = null; user.profile.lastNotified = null; user.profile.pastIncidents = []; - user.profile.activeIncidents = []; user.profile.staticAffordances = user.profile.staticAffordances || {}; Meteor.users.insert(user); }; diff --git a/imports/api/OpportunisticCoordinator/server/coordinator.tests.js b/imports/api/OpportunisticCoordinator/server/coordinator.tests.js index ed9ebac6..86e2147c 100644 --- a/imports/api/OpportunisticCoordinator/server/coordinator.tests.js +++ b/imports/api/OpportunisticCoordinator/server/coordinator.tests.js @@ -5,8 +5,7 @@ import { updateAvailability } from './identifier'; import {CONSTANTS} from "../../Testing/testingconstants"; -import {createIncidentFromExperience, startRunningIncident, - setIntersection, sustainedAvailabilities} from "../../OCEManager/OCEs/methods"; +import {createIncidentFromExperience, startRunningIncident} from "../../OCEManager/OCEs/methods"; import {Experiences, Incidents} from "../../OCEManager/OCEs/experiences"; import { Submissions } from "../../OCEManager/currentNeeds"; import {insertTestOCE } from "../populateDatabase"; @@ -209,349 +208,3 @@ describe('Assignments Collection test', () => { }); }); - -describe('test checkIfThreshold. Single Need, Single UID; allowRepeatContributions: false', () => { - - const incident_id = Random.id(); - const eid = Random.id(); // doesn't really matter - const userA = Random.id(); - const NEEDNAME = 'Coffee Time'; - const updatedIncidentsAndNeeds = [ - { - _id: incident_id, - needUserMaps: [ - { - needName: NEEDNAME, - users: [ - {uid: userA, place: 'placeA', distance: 10.0} - ] - } - ] - } - ]; - const numberNeeded = 4; - beforeEach(() => { - resetDatabase(); - - Incidents.insert({ - _id: incident_id, - eid: eid, - callbacks: null, // dont need callbacks - contributionTypes: [ - { - needName: NEEDNAME, - situation: { - detector: Random.id(), - number: 1 - }, - numberNeeded: numberNeeded, - notificationDelay: 1, - // not including the parameter defaults to false - // allowRepeatContributions: false - } - ] - }); - - // userA is available for incident.need1 - Availability.insert({ - _id: incident_id, - needUserMaps: [ - { - needName: NEEDNAME, users: [ - { uid: userA, place: "place1", distance: 10.0 } - ] - }, - ], - }); - - // userA NOT assigned to incident yet - // runNeedsWithThresholdMet does these types of updates via adminUpdatesForAddingUserToIncident - // and the call to this function comes after checkIfThreshold - Assignments.insert({ - _id: incident_id, - needUserMaps: [ - { needName: NEEDNAME, users: [] }, - ], - }); - - // Empty submissions ready to be filled - for (let i = 0; i < numberNeeded; ++i) { - Submissions.insert({ - _id: Random.id(), - eid : Random.id(), - iid : incident_id, - needName : NEEDNAME, - uid : null, - }); - } - }); - - it('user SHOULD be able to participate on the first try', () => { - let incidentsWithUsersToRun = checkIfThreshold(updatedIncidentsAndNeeds); - - // should look something like this - // { vPnAsWkhjv8EN6n9p: { 'Coffee Time': [ 'tDm59tFq2XBBKQZm5' ] } } - chai.assert.isNotNull(incidentsWithUsersToRun, 'incidentsWithUsersToRun should not be empty'); - chai.assert.isNotNull(incidentsWithUsersToRun[incident_id], 'incidentsWithUsersToRun should contain incident'); - chai.assert.isNotNull(incidentsWithUsersToRun[incident_id][NEEDNAME], 'incidentsWithUsersToRun should contain needName'); - let users_for_need = incidentsWithUsersToRun[incident_id][NEEDNAME]; - let foundUser = users_for_need.find((userMeta) => userMeta.uid === userA); - chai.assert(foundUser, 'incidentsWithUsersToRun should contain userA'); - }); - - it('user SHOULD NOT be allowed to participate twice', () => { - // But user has already participated in the past - Submissions.insert({ - _id: Random.id(), - eid : Random.id(), - iid : incident_id, - needName : NEEDNAME, - uid : userA, - content : { - "this": "is a previous submission" - } - }); - - let incidentsWithUsersToRun = checkIfThreshold(updatedIncidentsAndNeeds); - - // should look something like this - // { vPnAsWkhjv8EN6n9p: {} } - chai.assert.isNotNull(incidentsWithUsersToRun, 'incidentsWithUsersToRun should not be empty'); - chai.assert.isNotNull(incidentsWithUsersToRun[incident_id], 'incidentsWithUsersToRun should contain incident'); - - // object should be empty - let obj = incidentsWithUsersToRun[incident_id]; - if (Object.keys(obj).length === 0 && obj.constructor === Object) { - // TODO(rlouie): do this with multiple needs going on, or multiple users - chai.assert(true, 'incidentsWithUsersToRun should NOT contain needName or the User'); - } else { - chai.assert(false, 'incidentsWithUsersToRun should NOT contain needName or the User'); - } - }); - -}); - -describe('test checkIfThreshold; Single Need, Single UID; allowRepeatContributions: true', () => { - - const incident_id = Random.id(); - const eid = Random.id(); // doesn't really matter - const userA = Random.id(); - const NEEDNAME = 'Coffee Time'; - const updatedIncidentsAndNeeds = [ - { - _id: incident_id, - needUserMaps: [ - { - needName: NEEDNAME, - users: [ - {uid: userA, place: 'placeA', distance: 10.0} - ] - } - ] - } - ]; - const numberNeeded = 4; - beforeEach(() => { - resetDatabase(); - - Incidents.insert({ - _id: incident_id, - eid: eid, - callbacks: null, // dont need callbacks - contributionTypes: [ - { - needName: NEEDNAME, - situation: { - detector: Random.id(), - number: 1 - }, - numberNeeded: numberNeeded, - notificationDelay: 1, - allowRepeatContributions: true - } - ] - }); - - // userA is available for incident.need1 - Availability.insert({ - _id: incident_id, - needUserMaps: [ - { - needName: NEEDNAME, - users: [ - {uid: userA, place: 'placeA', distance: 10.0} - ] - }, - ], - }); - - // userA NOT assigned to incident yet - // runNeedsWithThresholdMet does these types of updates via adminUpdatesForAddingUserToIncident - // and the call to this function comes after checkIfThreshold - Assignments.insert({ - _id: incident_id, - needUserMaps: [ - { needName: NEEDNAME, users: [] }, - ], - }); - - // Empty submissions ready to be filled - for (let i = 0; i < numberNeeded; ++i) { - Submissions.insert({ - _id: Random.id(), - eid : Random.id(), - iid : incident_id, - needName : NEEDNAME, - uid : null, - }); - } - }); - - it('user SHOULD be able to participate on the first try', () => { - let incidentsWithUsersToRun = checkIfThreshold(updatedIncidentsAndNeeds); - - // should look something like this - // { vPnAsWkhjv8EN6n9p: { 'Coffee Time': [ 'tDm59tFq2XBBKQZm5' ] } } - chai.assert.isNotNull(incidentsWithUsersToRun, 'incidentsWithUsersToRun should not be empty'); - chai.assert.isNotNull(incidentsWithUsersToRun[incident_id], 'incidentsWithUsersToRun should contain incident'); - chai.assert.isNotNull(incidentsWithUsersToRun[incident_id][NEEDNAME], 'incidentsWithUsersToRun should contain needName'); - let users_for_need = incidentsWithUsersToRun[incident_id][NEEDNAME]; - let foundUser = users_for_need.find((userMeta) => userMeta.uid === userA); - chai.assert(foundUser, 'incidentsWithUsersToRun should contain userA'); - }); - - it('user ALSO SHOULD be allowed to participate twice', () => { - // But user has already participated in the past - Submissions.insert({ - _id: Random.id(), - eid : Random.id(), - iid : incident_id, - needName : NEEDNAME, - uid : userA, - content : { - "this": "is a previous submission" - } - }); - - let incidentsWithUsersToRun = checkIfThreshold(updatedIncidentsAndNeeds); - - // should look something like this - // { vPnAsWkhjv8EN6n9p: { 'Coffee Time': [ 'tDm59tFq2XBBKQZm5' ] } } - chai.assert.isNotNull(incidentsWithUsersToRun, 'incidentsWithUsersToRun should not be empty'); - chai.assert.isNotNull(incidentsWithUsersToRun[incident_id], 'incidentsWithUsersToRun should contain incident'); - chai.assert.isNotNull(incidentsWithUsersToRun[incident_id][NEEDNAME], 'incidentsWithUsersToRun should contain needName'); - let users_for_need = incidentsWithUsersToRun[incident_id][NEEDNAME]; - let foundUser = users_for_need.find((userMeta) => userMeta.uid === userA); - chai.assert(foundUser, 'incidentsWithUsersToRun should contain userA'); - }); - -}); - -describe('Sustained (place, need) Match for Availability Dictionary', function() { - let availabilityDictionary = { - "asianFoodCrawlIncident": [ - ['ramen_dojo', 'noodleNeed', 10.0], - ['kongs_chinese', 'noodleNeed', 20.5] - ], - "groceryBuddiesIncident": [ - ['trader_joes', 'groceryNeed', 20.0] - ], - "sunsetTogether": [ - ['', 'sunsetTogetherNeed', undefined], - ['ramen_dojo', 'sunsetTogetherNeed', undefined], - ['kongs_chinese', 'sunsetTogetherNeed', undefined], - ['trader_joes', 'sunsetTogetherNeed', undefined] - ] - }; - - // sustained success - after notification delay - let sustainedAfterAvailDict = { - "asianFoodCrawlIncident": [ - ['ramen_dojo', 'noodleNeed', 3.0] - ] - }; - - // sustained for not place need -- after notification delay - let sustainedWeatherTimeNeedAfterAvailDict = { - "sunsetTogether": [ - ['', 'sunsetTogetherNeed', undefined], - ] - }; - - // not sustained - after notification delay - let notSustainedAfterAvailDict = { - "asianFoodCrawlIncident": [ - ['onsen_oden', 'noodleNeed', 25.0] - ] - }; - - it('Sustained Incident', function() { - let incidentIntersection = setIntersection(Object.keys(availabilityDictionary), Object.keys(sustainedAfterAvailDict)); - console.log(incidentIntersection); - chai.assert.equal( - JSON.stringify(incidentIntersection), - JSON.stringify(["asianFoodCrawlIncident"])); - }); - - it('Sustained [Place, Needs]', function() { - let incident = "asianFoodCrawlIncident"; - - let beforePlacesAndNeeds = availabilityDictionary[incident].map((place_need_dist) => place_need_dist.slice(0,2)); - let afterPlacesAndNeeds = sustainedAfterAvailDict[incident].map((place_need_dist) => place_need_dist.slice(0,2)); - let place_need_intersection = setIntersection( - beforePlacesAndNeeds, afterPlacesAndNeeds); - - chai.assert.equal( - JSON.stringify(place_need_intersection), - JSON.stringify([['ramen_dojo', 'noodleNeed']]) - ) - }); - - // different places - after notification delay - it('NOT Sustained [Place, Needs]', function() { - let incident = "asianFoodCrawlIncident"; - - let place_need_intersection = setIntersection( - availabilityDictionary[incident], - notSustainedAfterAvailDict[incident]); - - chai.assert.equal( - JSON.stringify(place_need_intersection), - JSON.stringify([]) - ) - }); - - it('Sustained AvailDict', function() { - let sustainedAvailDict = sustainedAvailabilities(availabilityDictionary, sustainedAfterAvailDict); - chai.assert.equal( - JSON.stringify(sustainedAvailDict), - JSON.stringify({ - "asianFoodCrawlIncident": [ - ['ramen_dojo', 'noodleNeed', 3.0] - ] - }) - ) - }); - - it('NOT Sustained AvailDict', function() { - let sustainedAvailDict = sustainedAvailabilities(availabilityDictionary, notSustainedAfterAvailDict); - chai.assert.equal( - JSON.stringify(sustainedAvailDict), - JSON.stringify({}) - ); - - chai.assert.equal(Object.keys(sustainedAvailDict).length, 0); - }); - - it('Sustained NonPlace AvailDict', function() { - let sustainedAvailDict = sustainedAvailabilities(availabilityDictionary, sustainedWeatherTimeNeedAfterAvailDict); - chai.assert.equal( - JSON.stringify(sustainedAvailDict), - JSON.stringify({ - "sunsetTogether": [ - ['', 'sunsetTogetherNeed', undefined] - ] - }) - ); - }); -}); diff --git a/imports/api/OpportunisticCoordinator/server/executor.js b/imports/api/OpportunisticCoordinator/server/executor.js index ae440495..39c8cba8 100644 --- a/imports/api/OpportunisticCoordinator/server/executor.js +++ b/imports/api/OpportunisticCoordinator/server/executor.js @@ -1,4 +1,4 @@ -import { Meteor } from "meteor/meteor"; +// import { Meteor } from "meteor/meteor"; import { Experiences } from "../../OCEManager/OCEs/experiences"; import { Incidents } from "../../OCEManager/OCEs/experiences"; import { Locations } from "../../UserMonitor/locations/locations"; @@ -13,6 +13,8 @@ import { checkIfThreshold } from "./strategizer"; import { Notification_log } from "../../Logging/notification_log"; import { serverLog, log } from "../../logs"; import {sustainedAvailabilities} from "../../OCEManager/OCEs/methods"; +import {needAggregator} from "../strategizer"; +import {setIntersection} from "../../custom/arrayHelpers"; /** * Sends notifications to the users, adds to the user's active experience list, @@ -36,9 +38,32 @@ export const runNeedsWithThresholdMet = (incidentsWithUsersToRun) => { let incident = Incidents.findOne(iid); let experience = Experiences.findOne(incident.eid); - _.forEach(needUserMapping, (usersMeta, needName) => { + // { [detectorId]: [need1, ...], ...} + let needNamesBinnedByDetector = needAggregator(incident); + let assignedNeedNames = Object.keys(needUserMapping); + + _.forEach(needNamesBinnedByDetector, (commonDetectorNeedNames, detectorId) => { + + // might have to distinguish what is logged done when its not half half vs not. + // maybe use a "strategy" class model + // if (commonDetectorNeedNames.length == 1) { + // + // } + + // before doing a dynamic strategy algorithm, make sure we are looking at the candidate needs + let candidateNeedNames = setIntersection(commonDetectorNeedNames, assignedNeedNames); + + // TODO: send to a dynamic route endpoint + // choose a random need from the aggregated set for now + let needName = candidateNeedNames[0]; + + let usersMeta = needUserMapping[needName]; + if (!usersMeta) { + return; + } + let newUsersMeta = usersMeta.filter(function(userMeta) { - return !Meteor.users.findOne(userMeta.uid).profile.activeIncidents.includes(iid); + return !Meteor.users.findOne(userMeta.uid).activeIncidents().includes(iid); }); //administrative updates @@ -54,31 +79,31 @@ export const runNeedsWithThresholdMet = (incidentsWithUsersToRun) => { let uidsNotNotifiedRecently = newUsersMeta.map(usermeta => usermeta.uid); let route = "/"; - let needObject = experience.contributionTypes.find((need) => need.needName === needName); - - if (needObject) { - log.cerebro(JSON.stringify(needObject)); - if (needObject.notificationSubject && needObject.notificationText) { - notifyForParticipating(uidsNotNotifiedRecently, iid, needObject.notificationSubject, - needObject.notificationText, route); - } - else if (experience.name && experience.notificationText) { - notifyForParticipating(uidsNotNotifiedRecently, iid, `Participate in "${experience.name}"!`, - experience.notificationText, route); - } else { - log.error('notification information cannot be found in the need or experience level'); - return; - } - - _.forEach(newUsersMeta, usermeta => { - Notification_log.insert({ - uid: usermeta.uid, - iid: iid, - needName: needName, - timestamp: Date.now() - }); - }); + // Try to notify, based on if the current need has need-specific notification info + let needObject = incident.contributionTypes.find((need) => need.needName === needName); + if (needObject && needObject.notificationSubject && needObject.notificationText) { + notifyForParticipating(uidsNotNotifiedRecently, iid, needObject.notificationSubject, + needObject.notificationText, route); + } + // Try to notify, based on experience-level notification info + else if (experience.name && experience.notificationText) { + notifyForParticipating(uidsNotNotifiedRecently, iid, `Participate in "${experience.name}"!`, + experience.notificationText, route); + } + // Fail to notify, because these parameters are not defined + else { + log.error('notification information cannot be found in the need or experience level'); + return; } + + _.forEach(newUsersMeta, usermeta => { + Notification_log.insert({ + uid: usermeta.uid, + iid: iid, + needName: needName, + timestamp: Date.now() + }); + }); }); }); }; diff --git a/imports/api/OpportunisticCoordinator/server/executor.tests.js b/imports/api/OpportunisticCoordinator/server/executor.tests.js new file mode 100644 index 00000000..613af51a --- /dev/null +++ b/imports/api/OpportunisticCoordinator/server/executor.tests.js @@ -0,0 +1,117 @@ +import { resetDatabase } from 'meteor/xolvio:cleaner'; + +import { runNeedsWithThresholdMet } from "./executor"; +import {Incidents} from "../../OCEManager/OCEs/experiences"; +import {Submissions} from "../../OCEManager/currentNeeds"; +import {Assignments, Availability} from "../databaseHelpers"; + +describe('Executor - Half Half Need when User is Assigned to "Need 1" and "Need 2"', () => { + + const incident_id = Random.id(); + const eid = Random.id(); // doesn't really matter + const userA = Random.id(); + const userB = Random.id(); + const place = ''; // rainy doesn't need to have a place associated with it + const distance = null; + const needName1 = 'Rainy 1'; + const needName2 = 'Rainy 2'; + const detectorId = Random.id(); + const numberNeeded = 2; + const halfhalfNeedTemplate = { + needName: null, + situation: { + detector: detectorId, + number: 1 + }, + numberNeeded: numberNeeded, + notificationDelay: 1, + allowRepeatContributions: false + }; + // userA is available, and already is assigned to the need, but has not participated yet + // userB triggered the coordination process, and is also available + const updatedIncidentsAndNeeds = [ + { + _id: incident_id, + needUserMaps: [ + { + needName: needName1, + users: [ + {uid: userA, place: place, distance: distance}, + {uid: userB, place: place, distance: distance} + ] + }, + { + needName: needName2, + users: [ + {uid: userA, place: place, distance: distance}, + {uid: userB, place: place, distance: distance} + ] + } + ] + } + ]; + + beforeEach(() => { + resetDatabase(); + + let halfhalfNeed1 = JSON.parse(JSON.stringify(halfhalfNeedTemplate)); + halfhalfNeed1.needName = needName1; + let halfhalfNeed2 = JSON.parse(JSON.stringify(halfhalfNeedTemplate)); + halfhalfNeed2.needName = needName2; + Incidents.insert({ + _id: incident_id, + eid: eid, + callbacks: null, // dont need callbacks + contributionTypes: [ + halfhalfNeed1, + halfhalfNeed2 + ] + }); + + Availability.insert(updatedIncidentsAndNeeds[0]); + + // userA IS ALREADY assigned to incident, but has not participated + // userB NOT assigned to incident yet + // runNeedsWithThresholdMet does these types of updates via adminUpdatesForAddingUserToIncident + // and the call to this function comes after checkIfThreshold + Assignments.insert({ + _id: incident_id, + needUserMaps: [ + { + needName: needName1, + users: [ + {uid: userA, place: place, distance: distance} + ] + }, + { + needName: needName2, + users: [ + {uid: userA, place: place, distance: distance}, + {uid: userB, place: place, distance: distance} + ] + } + ], + }); + + // Empty submissions ready to be filled + for (let i = 0; i < numberNeeded; ++i) { + Submissions.insert({ + _id: Random.id(), + eid : eid, + iid : incident_id, + needName : needName1, + uid : null, + }); + Submissions.insert({ + _id: Random.id(), + eid : eid, + iid : incident_id, + needName : needName2, + uid : null, + }); + } + + }); + + it() +}); \ No newline at end of file diff --git a/imports/api/OpportunisticCoordinator/server/identifier.js b/imports/api/OpportunisticCoordinator/server/identifier.js index 70176059..c1c0eac1 100644 --- a/imports/api/OpportunisticCoordinator/server/identifier.js +++ b/imports/api/OpportunisticCoordinator/server/identifier.js @@ -1,27 +1,21 @@ -import { ValidatedMethod } from "meteor/mdg:validated-method"; -import { SimpleSchema } from "meteor/aldeed:simple-schema"; - -import { Assignments } from "../databaseHelpers"; -import { Availability } from "../databaseHelpers"; -import { Incidents } from "../../OCEManager/OCEs/experiences.js"; -import { Submissions } from "../../OCEManager/currentNeeds.js"; -import { Locations } from "../../UserMonitor/locations/locations"; -import { numUnfinishedNeeds } from "../../OCEManager/progressor"; -import { addEmptySubmissionsForNeed } from "../../OCEManager/OCEs/methods.js"; - -import { _addActiveIncidentToUser, _removeActiveIncidentFromUser, _removeIncidentFromUserEntirely } from - "../../UserMonitor/users/methods"; +import {ValidatedMethod} from "meteor/mdg:validated-method"; +import {SimpleSchema} from "meteor/aldeed:simple-schema"; + +import {Assignments, Availability, ParticipatingNow} from "../databaseHelpers"; +import {Incidents} from "../../OCEManager/OCEs/experiences.js"; +import {Submissions} from "../../OCEManager/currentNeeds.js"; +import {Locations} from "../../UserMonitor/locations/locations"; +import {numUnfinishedNeeds} from "../../OCEManager/progressor"; +import {addEmptySubmissionsForNeed} from "../../OCEManager/OCEs/methods.js"; + +import {_removeActiveIncidentFromUser} from "../../UserMonitor/users/methods"; import {doesUserMatchNeed, getNeedDelay} from "../../OCEManager/OCEs/methods"; -import { CONFIG } from "../../config"; -import { log, serverLog } from "../../logs"; -import {notifyForMissingParticipation} from "./noticationMethods"; -import { - flattenAffordanceDict, getPlaceKeys, onePlaceNotThesePlacesSets, - placeSubsetAffordances -} from "../../UserMonitor/detectors/methods"; +import {log, serverLog} from "../../logs"; +import {flattenAffordanceDict} from "../../UserMonitor/detectors/methods"; import {Decommission_log} from "../../Logging/decommission_log"; import {AddedToIncident_log} from "../../Logging/added_to_incident_log"; + export const getNeedObject = (iid, needName) => { let incident = Incidents.findOne(iid); if (incident) { @@ -148,6 +142,21 @@ export const pushUserIntoAssignmentsNeedUserMaps = (iid, needName, uid, place, d ); }; +/** + * Clears current availabilities for a user given a uid. + * @param uid {string} user to clear data for + */ +export const clearAvailabilitiesForUser = (uid) => { + let availabilityObjects = Availability.find().fetch(); + _.forEach(availabilityObjects, (av) => { + // remove user for each need in each + _.forEach(av.needUserMaps, (needEntry) => { + // was a + pullUserFromAvailabilityNeedUserMaps(av._id, needEntry.needName, uid); + }); + }); +}; + /** * Un-assigns a user to an incident if their location no longer matches * Also removes the active experience from the user @@ -201,7 +210,7 @@ let decommissionIfSustained = (userId, incidentId, needName, decommissionDelay) log.warning(`No user exists for uid = ${userId}`); return; } - let activeIncidents = user.profile.activeIncidents; + let activeIncidents = user.activeIncidents(); if (!activeIncidents.includes(incidentId)) { log.info(`No need to decommission { uid: ${userId} } from { iid: ${incidentId} }`); return; @@ -249,7 +258,6 @@ let decommissionIfSustained = (userId, incidentId, needName, decommissionDelay) */ export const adminUpdatesForAddingUserToIncident = (uid, iid, needName) => { _addUserToAssignmentDb(uid, iid, needName); - _addActiveIncidentToUser(uid, iid); // TODO(rlouie): add extra incident/need/place/distance info // _addActiveIncidentNeedPlaceDistanceToUsers(uid, incidentNeedPlaceDistance); @@ -289,7 +297,6 @@ export const adminUpdatesForRemovingUserToIncident = (uid, iid, needName) => { export const adminUpdatesForRemovingUserToIncidentEntirely = (uid, iid, needName) => { //TODO: make this function take a single user not an array _removeUserFromAssignmentDb(uid, iid, needName); - _removeIncidentFromUserEntirely(uid, iid); }; /** @@ -382,6 +389,80 @@ export const getNeedUserMapForNeed = (iid, needName) => { return needUserMap; } }; + + +Meteor.methods({ + pushUserIntoParticipatingNow({iid, needName, uid}) { + new SimpleSchema({ + iid: { type: String }, + needName: { type: String }, + uid: { type: String } + }).validate({iid, needName, uid}); + + pushUserIntoParticipatingNow(iid, needName, uid); + }, + pullUserFromParticipatingNow({iid, needName, uid}) { + new SimpleSchema({ + iid: { type: String }, + needName: { type: String }, + uid: { type: String } + }).validate({iid, needName, uid}); + + pullUserFromParticipatingNow(iid, needName, uid); + }, +}); + +/** + * pushUserIntoParticipatingNow + * + * The list of users in each needUserMap is a counter for who has the participate route open + * This function increments this "semaphore" like counter, or adds users + * @param iid + * @param needName + * @param uid + * @param place + * @param distance + */ +export const pushUserIntoParticipatingNow = (iid, needName, uid) => { + ParticipatingNow.update( + { + _id: iid, + "needUserMaps.needName": needName + }, + { + $push: { + // note: this object is different than {"uid": uid, "place": place, "distance": distance} + "needUserMaps.$.users": { + "uid": uid + } + } + } + ); +}; + + +/** + * pullUserFromParticipatingNow + * + * The list of users in each needUserMap is a counter for who has the participate route open + * This function decrements this "semaphore" like counter, or removes users + * @param iid + * @param needName + * @param uid + */ +export const pullUserFromParticipatingNow = (iid, needName, uid) => { + ParticipatingNow.update( + { + _id: iid, + "needUserMaps.needName": needName + }, + { + $pull: { "needUserMaps.$.users": {"uid" : uid } } + } + ); +}; + + // const locationCursor = Locations.find(); // // /** diff --git a/imports/api/OpportunisticCoordinator/server/identifier.tests.js b/imports/api/OpportunisticCoordinator/server/identifier.tests.js new file mode 100644 index 00000000..3c6d0114 --- /dev/null +++ b/imports/api/OpportunisticCoordinator/server/identifier.tests.js @@ -0,0 +1,111 @@ +import {sustainedAvailabilities} from "../../OCEManager/OCEs/methods"; +import {setIntersection} from "../../custom/arrayHelpers"; + +describe('Sustained (place, need) Match for Availability Dictionary', function() { + let availabilityDictionary = { + "asianFoodCrawlIncident": [ + ['ramen_dojo', 'noodleNeed', 10.0], + ['kongs_chinese', 'noodleNeed', 20.5] + ], + "groceryBuddiesIncident": [ + ['trader_joes', 'groceryNeed', 20.0] + ], + "sunsetTogether": [ + ['', 'sunsetTogetherNeed', undefined], + ['ramen_dojo', 'sunsetTogetherNeed', undefined], + ['kongs_chinese', 'sunsetTogetherNeed', undefined], + ['trader_joes', 'sunsetTogetherNeed', undefined] + ] + }; + + // sustained success - after notification delay + let sustainedAfterAvailDict = { + "asianFoodCrawlIncident": [ + ['ramen_dojo', 'noodleNeed', 3.0] + ] + }; + + // sustained for not place need -- after notification delay + let sustainedWeatherTimeNeedAfterAvailDict = { + "sunsetTogether": [ + ['', 'sunsetTogetherNeed', undefined], + ] + }; + + // not sustained - after notification delay + let notSustainedAfterAvailDict = { + "asianFoodCrawlIncident": [ + ['onsen_oden', 'noodleNeed', 25.0] + ] + }; + + it('Sustained Incident', function() { + let incidentIntersection = setIntersection(Object.keys(availabilityDictionary), Object.keys(sustainedAfterAvailDict)); + console.log(incidentIntersection); + chai.assert.equal( + JSON.stringify(incidentIntersection), + JSON.stringify(["asianFoodCrawlIncident"])); + }); + + it('Sustained [Place, Needs]', function() { + let incident = "asianFoodCrawlIncident"; + + let beforePlacesAndNeeds = availabilityDictionary[incident].map((place_need_dist) => place_need_dist.slice(0,2)); + let afterPlacesAndNeeds = sustainedAfterAvailDict[incident].map((place_need_dist) => place_need_dist.slice(0,2)); + let place_need_intersection = setIntersection( + beforePlacesAndNeeds, afterPlacesAndNeeds); + + chai.assert.equal( + JSON.stringify(place_need_intersection), + JSON.stringify([['ramen_dojo', 'noodleNeed']]) + ) + }); + + // different places - after notification delay + it('NOT Sustained [Place, Needs]', function() { + let incident = "asianFoodCrawlIncident"; + + let place_need_intersection = setIntersection( + availabilityDictionary[incident], + notSustainedAfterAvailDict[incident]); + + chai.assert.equal( + JSON.stringify(place_need_intersection), + JSON.stringify([]) + ) + }); + + it('Sustained AvailDict', function() { + let sustainedAvailDict = sustainedAvailabilities(availabilityDictionary, sustainedAfterAvailDict); + chai.assert.equal( + JSON.stringify(sustainedAvailDict), + JSON.stringify({ + "asianFoodCrawlIncident": [ + ['ramen_dojo', 'noodleNeed', 3.0] + ] + }) + ) + }); + + it('NOT Sustained AvailDict', function() { + let sustainedAvailDict = sustainedAvailabilities(availabilityDictionary, notSustainedAfterAvailDict); + chai.assert.equal( + JSON.stringify(sustainedAvailDict), + JSON.stringify({}) + ); + + chai.assert.equal(Object.keys(sustainedAvailDict).length, 0); + }); + + it('Sustained NonPlace AvailDict', function() { + let sustainedAvailDict = sustainedAvailabilities(availabilityDictionary, sustainedWeatherTimeNeedAfterAvailDict); + chai.assert.equal( + JSON.stringify(sustainedAvailDict), + JSON.stringify({ + "sunsetTogether": [ + ['', 'sunsetTogetherNeed', undefined] + ] + }) + ); + }); +}); diff --git a/imports/api/OpportunisticCoordinator/server/noticationMethods.js b/imports/api/OpportunisticCoordinator/server/noticationMethods.js index 7ae32927..3e54834b 100644 --- a/imports/api/OpportunisticCoordinator/server/noticationMethods.js +++ b/imports/api/OpportunisticCoordinator/server/noticationMethods.js @@ -1,4 +1,4 @@ -import { Meteor } from 'meteor/meteor'; +// import { Meteor } from 'meteor/meteor'; import { Push } from 'meteor/raix:push'; import { log } from '../../logs.js'; import { CONFIG } from '../../config.js'; diff --git a/imports/api/OpportunisticCoordinator/server/publications.js b/imports/api/OpportunisticCoordinator/server/publications.js index 4b34dce9..0cc17f6e 100644 --- a/imports/api/OpportunisticCoordinator/server/publications.js +++ b/imports/api/OpportunisticCoordinator/server/publications.js @@ -1,5 +1,5 @@ -import { Meteor } from 'meteor/meteor'; -import { Assignments } from "../databaseHelpers"; +// import { Meteor } from 'meteor/meteor'; +import {Assignments, ParticipatingNow} from "../databaseHelpers"; import {Availability} from "../databaseHelpers"; import {Notification_log} from "../../Logging/notification_log"; @@ -12,7 +12,7 @@ Meteor.publish('availability.all', function () { }); Meteor.publish('assignments.single', function (assignmentId) { - return Assignments.find(assignmentId); + return Assignments.find({_id: assignmentId}); }); Meteor.publish('assignments.activeUser', function () { @@ -35,3 +35,7 @@ Meteor.publish('assignments.activeUser', function () { Meteor.publish('notification_log.activeIncident', function (iid) { return Notification_log.find({iid: iid}); }); + +Meteor.publish('participating.now.activeIncident', function (iid) { + return ParticipatingNow.find({_id: iid}); +}); \ No newline at end of file diff --git a/imports/api/OpportunisticCoordinator/server/strategizer.js b/imports/api/OpportunisticCoordinator/server/strategizer.js index 79ff8448..87165e83 100644 --- a/imports/api/OpportunisticCoordinator/server/strategizer.js +++ b/imports/api/OpportunisticCoordinator/server/strategizer.js @@ -1,6 +1,14 @@ -import { Submissions } from "../../OCEManager/currentNeeds"; -import { Assignments } from "../databaseHelpers"; -import { getNeedObject } from "./identifier"; +/** + * strategizer -- server side + */ +import {Submissions} from "../../OCEManager/currentNeeds"; +import {Assignments} from "../databaseHelpers"; +import {getNeedObject} from "./identifier"; +import {Experiences} from "../../OCEManager/OCEs/experiences"; +import {createIncidentFromExperience, startRunningIncident} from "../../OCEManager/OCEs/methods"; +// import {Meteor} from "meteor/meteor"; +import {numberSubmissionsRemaining, usersAlreadyAssignedToNeed, usersAlreadySubmittedToNeed} from "../strategizer"; + const util = require('util'); /** @@ -45,13 +53,7 @@ export const checkIfThreshold = updatedIncidentsAndNeeds => { // console.log('incidentMapping: ', util.inspect(incidentMapping, false, null)); let assignment = Assignments.findOne(incidentMapping._id); // console.log('assignment: ', util.inspect(assignment, false, null)); - let usersInIncident = [].concat.apply( - [], - assignment.needUserMaps.map(function(needMap) { - return needMap.users; - }) - ); - // console.log('usersInIncident: ', util.inspect(usersInIncident, false, null)); + incidentsWithUsersToRun[incidentMapping._id] = {}; _.forEach(incidentMapping.needUserMaps, needUserMap => { @@ -62,47 +64,32 @@ export const checkIfThreshold = updatedIncidentsAndNeeds => { //get need object let need = getNeedObject(iid, needName); - // console.log('need: ', util.inspect(need, false, null)); - // Start by never keeping track of previous user submissions to the incident - let previousUids = []; - - // If we are not allowing repeat contributions, then do look at previous user submissions - if (!need.allowRepeatContributions) { - previousUids = Submissions.find({ - iid: incidentMapping._id, - needName: needName - }) - .fetch() - .map(function(x) { - return x.uid; - }); - } + + let usersInNeed = usersAlreadyAssignedToNeed(iid, needName); + // console.log('usersInNeed : ', util.inspect(usersInIncident, false, null)); + + let previousUids = (need.allowRepeatContributions ? [] : usersAlreadySubmittedToNeed(iid, needName)); // console.log('previousUids: ', util.inspect(previousUids, false, null)); let usersNotInIncident = needUserMap.users.filter(function(user) { - return !usersInIncident.find(x => x.uid === user.uid) && !previousUids.find(uid => uid === user.uid); + return !usersInNeed.find(x => x.uid === user.uid) && !previousUids.find(uid => uid === user.uid); }); // console.log('usersNotInIncident: ', util.inspect(usersNotInIncident, false, null)); let assignmentNeed = assignment.needUserMaps.find(function(x) { return x.needName === needName; }); - // console.log('assignmentNeed: ', util.inspect(assignmentNeed, false, null)); - - if (assignmentNeed.users.length === 0) { - if (usersNotInIncident.length >= need.situation.number) { - let newChosenUsers = chooseUsers( - usersNotInIncident, - iid, - assignmentNeed - ); - // console.log('newChoosenUsers: ', util.inspect(newChosenUsers, false, null)); - usersInIncident = usersInIncident.concat(newChosenUsers); - incidentsWithUsersToRun[incidentMapping._id][ - needUserMap.needName - ] = newChosenUsers; - } + + // check for synchronous needs (need.situation.number >= 2) + if (usersNotInIncident.length >= need.situation.number) { + let newChosenUsers = chooseUsers( + usersNotInIncident, + iid, + assignmentNeed + ); + // console.log('newChoosenUsers: ', util.inspect(newChosenUsers, false, null)); + incidentsWithUsersToRun[incidentMapping._id][needUserMap.needName] = newChosenUsers; } }); }); @@ -110,12 +97,9 @@ export const checkIfThreshold = updatedIncidentsAndNeeds => { return incidentsWithUsersToRun; }; +/** my mutex, but not dynamic on page load, but does it during the first assignment (for notification) **/ const chooseUsers = (availableUserMetas, iid, needUserMap) => { - let numberPeopleNeeded = Submissions.find({ - iid: iid, - needName: needUserMap.needName, - uid: null - }).count(); + let numberPeopleNeeded = numberSubmissionsRemaining(iid, needUserMap.needName); let usersWeAlreadyHave = needUserMap.users; @@ -131,3 +115,4 @@ const chooseUsers = (availableUserMetas, iid, needUserMap) => { return chosen; } }; + diff --git a/imports/api/OpportunisticCoordinator/server/strategizer.tests.js b/imports/api/OpportunisticCoordinator/server/strategizer.tests.js new file mode 100644 index 00000000..45ad5574 --- /dev/null +++ b/imports/api/OpportunisticCoordinator/server/strategizer.tests.js @@ -0,0 +1,461 @@ +import { resetDatabase } from 'meteor/xolvio:cleaner'; +import {Incidents} from "../../OCEManager/OCEs/experiences"; +import {Submissions} from "../../OCEManager/currentNeeds"; +import {Assignments, Availability} from "../databaseHelpers"; +import {checkIfThreshold} from "./strategizer"; +import {needAggregator} from "../strategizer"; + +describe('test checkIfThreshold. Single Need, Single UID; allowRepeatContributions: false', () => { + + const incident_id = Random.id(); + const eid = Random.id(); // doesn't really matter + const userA = Random.id(); + const NEEDNAME = 'Coffee Time'; + const updatedIncidentsAndNeeds = [ + { + _id: incident_id, + needUserMaps: [ + { + needName: NEEDNAME, + users: [ + {uid: userA, place: 'placeA', distance: 10.0} + ] + } + ] + } + ]; + const numberNeeded = 4; + beforeEach(() => { + resetDatabase(); + + Incidents.insert({ + _id: incident_id, + eid: eid, + callbacks: null, // dont need callbacks + contributionTypes: [ + { + needName: NEEDNAME, + situation: { + detector: Random.id(), + number: 1 + }, + numberNeeded: numberNeeded, + notificationDelay: 1, + // not including the parameter defaults to false + // allowRepeatContributions: false + } + ] + }); + + // userA is available for incident.need1 + Availability.insert({ + _id: incident_id, + needUserMaps: [ + { + needName: NEEDNAME, users: [ + { uid: userA, place: "place1", distance: 10.0 } + ] + }, + ], + }); + + // userA NOT assigned to incident yet + // runNeedsWithThresholdMet does these types of updates via adminUpdatesForAddingUserToIncident + // and the call to this function comes after checkIfThreshold + Assignments.insert({ + _id: incident_id, + needUserMaps: [ + { needName: NEEDNAME, users: [] }, + ], + }); + + // Empty submissions ready to be filled + for (let i = 0; i < numberNeeded; ++i) { + Submissions.insert({ + _id: Random.id(), + eid : Random.id(), + iid : incident_id, + needName : NEEDNAME, + uid : null, + }); + } + }); + + it('user SHOULD be able to participate on the first try', () => { + let incidentsWithUsersToRun = checkIfThreshold(updatedIncidentsAndNeeds); + + // should look something like this + // { vPnAsWkhjv8EN6n9p: { 'Coffee Time': [ 'tDm59tFq2XBBKQZm5' ] } } + chai.assert.isNotNull(incidentsWithUsersToRun, 'incidentsWithUsersToRun should not be empty'); + chai.assert.isNotNull(incidentsWithUsersToRun[incident_id], 'incidentsWithUsersToRun should contain incident'); + chai.assert.isNotNull(incidentsWithUsersToRun[incident_id][NEEDNAME], 'incidentsWithUsersToRun should contain needName'); + let users_for_need = incidentsWithUsersToRun[incident_id][NEEDNAME]; + let foundUser = users_for_need.find((userMeta) => userMeta.uid === userA); + chai.assert(foundUser, 'incidentsWithUsersToRun should contain userA'); + }); + + it('user SHOULD NOT be allowed to participate twice', () => { + // But user has already participated in the past + Submissions.insert({ + _id: Random.id(), + eid : Random.id(), + iid : incident_id, + needName : NEEDNAME, + uid : userA, + content : { + "this": "is a previous submission" + } + }); + + let incidentsWithUsersToRun = checkIfThreshold(updatedIncidentsAndNeeds); + + // should look something like this + // { vPnAsWkhjv8EN6n9p: {} } + chai.assert.isNotNull(incidentsWithUsersToRun, 'incidentsWithUsersToRun should not be empty'); + chai.assert.isNotNull(incidentsWithUsersToRun[incident_id], 'incidentsWithUsersToRun should contain incident'); + + // object should be empty + let obj = incidentsWithUsersToRun[incident_id]; + if (Object.keys(obj).length === 0 && obj.constructor === Object) { + // TODO(rlouie): do this with multiple needs going on, or multiple users + chai.assert(true, 'incidentsWithUsersToRun should NOT contain needName or the User'); + } else { + chai.assert(false, 'incidentsWithUsersToRun should NOT contain needName or the User'); + } + }); + +}); + +describe('test checkIfThreshold; Single Need, Single UID; allowRepeatContributions: true', () => { + + const incident_id = Random.id(); + const eid = Random.id(); // doesn't really matter + const userA = Random.id(); + const NEEDNAME = 'Coffee Time'; + const updatedIncidentsAndNeeds = [ + { + _id: incident_id, + needUserMaps: [ + { + needName: NEEDNAME, + users: [ + {uid: userA, place: 'placeA', distance: 10.0} + ] + } + ] + } + ]; + const numberNeeded = 4; + beforeEach(() => { + resetDatabase(); + + Incidents.insert({ + _id: incident_id, + eid: eid, + callbacks: null, // dont need callbacks + contributionTypes: [ + { + needName: NEEDNAME, + situation: { + detector: Random.id(), + number: 1 + }, + numberNeeded: numberNeeded, + notificationDelay: 1, + allowRepeatContributions: true + } + ] + }); + + // userA is available for incident.need1 + Availability.insert({ + _id: incident_id, + needUserMaps: [ + { + needName: NEEDNAME, + users: [ + {uid: userA, place: 'placeA', distance: 10.0} + ] + }, + ], + }); + + // userA NOT assigned to incident yet + // runNeedsWithThresholdMet does these types of updates via adminUpdatesForAddingUserToIncident + // and the call to this function comes after checkIfThreshold + Assignments.insert({ + _id: incident_id, + needUserMaps: [ + { needName: NEEDNAME, users: [] }, + ], + }); + + // Empty submissions ready to be filled + for (let i = 0; i < numberNeeded; ++i) { + Submissions.insert({ + _id: Random.id(), + eid : Random.id(), + iid : incident_id, + needName : NEEDNAME, + uid : null, + }); + } + }); + + it('user SHOULD be able to participate on the first try', () => { + let incidentsWithUsersToRun = checkIfThreshold(updatedIncidentsAndNeeds); + + // should look something like this + // { vPnAsWkhjv8EN6n9p: { 'Coffee Time': [ 'tDm59tFq2XBBKQZm5' ] } } + chai.assert.isNotNull(incidentsWithUsersToRun, 'incidentsWithUsersToRun should not be empty'); + chai.assert.isNotNull(incidentsWithUsersToRun[incident_id], 'incidentsWithUsersToRun should contain incident'); + chai.assert.isNotNull(incidentsWithUsersToRun[incident_id][NEEDNAME], 'incidentsWithUsersToRun should contain needName'); + let users_for_need = incidentsWithUsersToRun[incident_id][NEEDNAME]; + let foundUser = users_for_need.find((userMeta) => userMeta.uid === userA); + chai.assert(foundUser, 'incidentsWithUsersToRun should contain userA'); + }); + + it('user ALSO SHOULD be allowed to participate twice', () => { + // But user has already participated in the past + Submissions.insert({ + _id: Random.id(), + eid : Random.id(), + iid : incident_id, + needName : NEEDNAME, + uid : userA, + content : { + "this": "is a previous submission" + } + }); + + let incidentsWithUsersToRun = checkIfThreshold(updatedIncidentsAndNeeds); + + // should look something like this + // { vPnAsWkhjv8EN6n9p: { 'Coffee Time': [ 'tDm59tFq2XBBKQZm5' ] } } + chai.assert.isNotNull(incidentsWithUsersToRun, 'incidentsWithUsersToRun should not be empty'); + chai.assert.isNotNull(incidentsWithUsersToRun[incident_id], 'incidentsWithUsersToRun should contain incident'); + chai.assert.isNotNull(incidentsWithUsersToRun[incident_id][NEEDNAME], 'incidentsWithUsersToRun should contain needName'); + let users_for_need = incidentsWithUsersToRun[incident_id][NEEDNAME]; + let foundUser = users_for_need.find((userMeta) => userMeta.uid === userA); + chai.assert(foundUser, 'incidentsWithUsersToRun should contain userA'); + }); + +}); + +describe('Half Half Rainy Need - with [userA, userB] matching the requirements of the need', () => { + // TODO(rlouie): Include a link to a diagram for this test + + const incident_id = Random.id(); + const eid = Random.id(); // doesn't really matter + const userA = Random.id(); + const userB = Random.id(); + const place = ''; // rainy doesn't need to have a place associated with it + const distance = null; + const needName1 = 'Rainy 1'; + const needName2 = 'Rainy 2'; + const detectorId = Random.id(); + const numberNeeded = 2; + const halfhalfNeedTemplate = { + needName: null, + situation: { + detector: detectorId, + number: 1 + }, + numberNeeded: numberNeeded, + notificationDelay: 1, + allowRepeatContributions: false + }; + // userA is available, and already is assigned to the need, but has not participated yet + // userB triggered the coordination process, and is also available + const updatedIncidentsAndNeeds = [ + { + _id: incident_id, + needUserMaps: [ + { + needName: needName1, + users: [ + {uid: userA, place: place, distance: distance}, + {uid: userB, place: place, distance: distance} + ] + }, + { + needName: needName2, + users: [ + {uid: userA, place: place, distance: distance}, + {uid: userB, place: place, distance: distance} + ] + } + ] + } + ]; + + beforeEach(() => { + resetDatabase(); + + let halfhalfNeed1 = JSON.parse(JSON.stringify(halfhalfNeedTemplate)); + halfhalfNeed1.needName = needName1; + let halfhalfNeed2 = JSON.parse(JSON.stringify(halfhalfNeedTemplate)); + halfhalfNeed2.needName = needName2; + Incidents.insert({ + _id: incident_id, + eid: eid, + callbacks: null, // dont need callbacks + contributionTypes: [ + halfhalfNeed1, + halfhalfNeed2 + ] + }); + + Availability.insert(updatedIncidentsAndNeeds[0]); + + // userA IS ALREADY assigned to need1 and need2, but has not participated + // userB NOT assigned to any of the needs + // runNeedsWithThresholdMet does these types of updates via adminUpdatesForAddingUserToIncident + // and the call to this function comes after checkIfThreshold + Assignments.insert({ + _id: incident_id, + needUserMaps: [ + { + needName: needName1, + users: [ + {uid: userA, place: place, distance: distance} + ] + }, + { + needName: needName2, + users: [ + {uid: userA, place: place, distance: distance}, + ] + } + ], + }); + + // Empty submissions ready to be filled + for (let i = 0; i < numberNeeded; ++i) { + Submissions.insert({ + _id: Random.id(), + eid : eid, + iid : incident_id, + needName : needName1, + uid : null, + }); + Submissions.insert({ + _id: Random.id(), + eid : eid, + iid : incident_id, + needName : needName2, + uid : null, + }); + } + }); + + it('should allow userB to still be assigned to first open half half needs the first time', () => { + let incidentsWithUsersToRun = checkIfThreshold(updatedIncidentsAndNeeds); + + // should look something like this + // { vPnAsWkhjv8EN6n9p: { 'Coffee Time': [ 'tDm59tFq2XBBKQZm5' ] } } + chai.assert.isNotNull(incidentsWithUsersToRun, 'incidentsWithUsersToRun should not be empty'); + chai.assert.isNotNull(incidentsWithUsersToRun[incident_id], 'incidentsWithUsersToRun should contain incident'); + chai.assert.isNotNull(incidentsWithUsersToRun[incident_id][needName1], `incidentsWithUsersToRun should contain ${needName1}`); + let users_for_need1 = incidentsWithUsersToRun[incident_id][needName1]; + let foundUserForNeed1 = users_for_need1.find((userMeta) => userMeta.uid === userB); + chai.assert(foundUserForNeed1, `incidentsWithUsersToRun, ${needName1}, should contain userB`); + // chai.assert.isNotNull(incidentsWithUsersToRun[incident_id][needName2], `incidentsWithUsersToRun should contain ${needName2}`); + // let users_for_need2 = incidentsWithUsersToRun[incident_id][needName2]; + // let foundUserForNeed2 = users_for_need2.find((userMeta) => userMeta.uid === userB); + // chai.assert(foundUserForNeed2, `incidentsWithUsersToRun, ${needName2}, should contain userB`); + }); + + it('should allow userB to still be assigned to second open half half needs the first time', () => { + let incidentsWithUsersToRun = checkIfThreshold(updatedIncidentsAndNeeds); + + // should look something like this + // { vPnAsWkhjv8EN6n9p: { 'Coffee Time': [ 'tDm59tFq2XBBKQZm5' ] } } + chai.assert.isNotNull(incidentsWithUsersToRun, 'incidentsWithUsersToRun should not be empty'); + chai.assert.isNotNull(incidentsWithUsersToRun[incident_id], 'incidentsWithUsersToRun should contain incident'); + // chai.assert.isNotNull(incidentsWithUsersToRun[incident_id][needName1], `incidentsWithUsersToRun should contain ${needName1}`); + // let users_for_need1 = incidentsWithUsersToRun[incident_id][needName1]; + // let foundUserForNeed1 = users_for_need1.find((userMeta) => userMeta.uid === userB); + // chai.assert(foundUserForNeed1, `incidentsWithUsersToRun, ${needName1}, should contain userB`); + chai.assert.isNotNull(incidentsWithUsersToRun[incident_id][needName2], `incidentsWithUsersToRun should contain ${needName2}`); + let users_for_need2 = incidentsWithUsersToRun[incident_id][needName2]; + let foundUserForNeed2 = users_for_need2.find((userMeta) => userMeta.uid === userB); + chai.assert(foundUserForNeed2, `incidentsWithUsersToRun, ${needName2}, should contain userB`); + }); + + // it('user ALSO SHOULD be allowed to participate twice', () => { + // // But user has already participated in the past + // Submissions.insert({ + // _id: Random.id(), + // eid : Random.id(), + // iid : incident_id, + // needName : NEEDNAME, + // uid : userA, + // content : { + // "this": "is a previous submission" + // } + // }); + // + // let incidentsWithUsersToRun = checkIfThreshold(updatedIncidentsAndNeeds); + // + // // should look something like this + // // { vPnAsWkhjv8EN6n9p: { 'Coffee Time': [ 'tDm59tFq2XBBKQZm5' ] } } + // chai.assert.isNotNull(incidentsWithUsersToRun, 'incidentsWithUsersToRun should not be empty'); + // chai.assert.isNotNull(incidentsWithUsersToRun[incident_id], 'incidentsWithUsersToRun should contain incident'); + // chai.assert.isNotNull(incidentsWithUsersToRun[incident_id][NEEDNAME], 'incidentsWithUsersToRun should contain needName'); + // let users_for_need = incidentsWithUsersToRun[incident_id][NEEDNAME]; + // let foundUser = users_for_need.find((userMeta) => userMeta.uid === userA); + // chai.assert(foundUser, 'incidentsWithUsersToRun should contain userA'); + // }); + +}); + + +describe('Dynamic Loading of Exact Participate Need - needAggregator', () => { + + const incident_id = Random.id(); + const eid = Random.id(); // doesn't really matter + const needName1 = 'Rainy 1'; + const needName2 = 'Rainy 2'; + const detectorId = Random.id(); + const numberNeeded = 2; + const halfhalfNeedTemplate = { + needName: null, + situation: { + detector: detectorId, + number: 1 + }, + numberNeeded: numberNeeded, + notificationDelay: 1, + allowRepeatContributions: false + }; + let halfhalfNeed1 = JSON.parse(JSON.stringify(halfhalfNeedTemplate)); + halfhalfNeed1.needName = needName1; + let halfhalfNeed2 = JSON.parse(JSON.stringify(halfhalfNeedTemplate)); + halfhalfNeed2.needName = needName2; + + before(() => { + resetDatabase(); + + Incidents.insert({ + _id: incident_id, + eid: eid, + callbacks: null, // dont need callbacks + contributionTypes: [ + halfhalfNeed1, + halfhalfNeed2 + ] + }); + + }); + + it('should group Half Half Needs based on their common detector', () => { + let incident = Incidents.findOne({_id: incident_id}); + let res = needAggregator(incident); + + chai.assert(JSON.stringify(res), + JSON.stringify({ + [detectorId]: [needName1, needName2] + })) + }); +}); \ No newline at end of file diff --git a/imports/api/OpportunisticCoordinator/strategizer.js b/imports/api/OpportunisticCoordinator/strategizer.js new file mode 100644 index 00000000..50ab5d66 --- /dev/null +++ b/imports/api/OpportunisticCoordinator/strategizer.js @@ -0,0 +1,140 @@ +/** + * strategizer -- server+client side + */ +import {Assignments, Availability, ParticipatingNow} from "./databaseHelpers"; +import {Submissions} from "../OCEManager/currentNeeds"; + + +/** + * usersAlreadyAssignedToNeed + * + * Returns + * @param iid + * @param needName + * @return usersInNeed [Array] array of uids + */ +export const usersAlreadyAssignedToNeed = (iid, needName) => { + let assignment = Assignments.findOne(iid); + let assignmentNeedMap = assignment.needUserMaps.find(function (x) { + return x.needName === needName; + }); + return assignmentNeedMap.users; +}; + + +/** + * usersAlreadySubmittedToNeed + * + * @param iid + * @param needName + * @return previousUids [Array] array of uids + */ +export const usersAlreadySubmittedToNeed = (iid, needName) => { + let previousUids = Submissions.find({ + iid: iid, needName: needName + }).map(function (x) { + return x.uid; + }); + return previousUids; +}; + + +/** + * needAggregator + * + * Helper for Dynamic Loading of Exact Participate Need + * Looks at the need.situation.detectors of an incident, + * returns the needNames should be aggregated + * @param incident, result of a Incident.findOne call + */ +export const needAggregator = (incident) => { + // keys: detectors + // values: needs + let res = {}; + if (!incident) { + console.log('needAggregator: incident is null'); + return res; + } + _.forEach(incident.contributionTypes, (need) => { + if (res[need.situation.detector]) { + res[need.situation.detector].push(need.needName); + } + else { + res[need.situation.detector] = [need.needName]; + } + }); + return res; +}; + + +/** + * numberSubmissionsNeeded + * + * @param iid + * @param needName + * @return {any | * | IDBRequest | void} + */ +export const numberSubmissionsRemaining = (iid, needName) => { + if (!iid) { + console.error(`Error in numberSubmissionsRemaining: param iid undefined`) + } + if (!needName) { + console.error(`Error in numberSubmissionsRemaining: param needName undefined`) + } + let numSubsNeeded = Submissions.find({ + iid: iid, + needName: needName, + uid: null + }).count(); + if (!Number.isInteger(numSubsNeeded)) { + console.error(`Error in numSubmissionsNeeded: numSubsNeeded is not an Integer`); + return; + } + return numSubsNeeded; +}; + +export const needIsAvailableToParticipateNow = (incident, needName) => { + if (!incident) { + console.log(`Error in needAggregator: incident is null\n ${JSON.stringify(incident)}`); + return; + } + if (!needName) { + console.log(`Error in needAggregator: needName is null`); + return; + } + if (!incident.contributionTypes) { + console.log(`Error in needAggregator: incident does not have contribution types\n ${JSON.stringify(incident)}`); + return; + } + const needObject = incident.contributionTypes.find(need => need.needName == needName); + const numberNeeded = numberSubmissionsRemaining(incident._id, needName); + const semaphoreMax = needObject.numberAllowedToParticipateAtSameTime ? + needObject.numberAllowedToParticipateAtSameTime : numberNeeded; + const resourcesStillAvailable = numberUsersParticipatingNow(incident._id, needName) < semaphoreMax; + return resourcesStillAvailable; +}; + +/** + * numberUsersParticipatingNow + * + * @param iid + * @param needName + */ +export const numberUsersParticipatingNow = (iid, needName) => { + let participatingNowInIncident = ParticipatingNow.findOne({_id: iid}); + if (!participatingNowInIncident) { + console.error(`Error in numberUsersParticipatingNow: no doc in ParticipatingNow found for iid ${iid}`); + return; + } + if (!Array.isArray(participatingNowInIncident.needUserMaps)) { + console.error(`Error in numberUsersParticipatingNow: participatingNowInIncident.needUserMaps is not an array`); + return; + } + let needMap = participatingNowInIncident.needUserMaps.find(needUserMap => needUserMap.needName == needName); + if (!Array.isArray(needMap.users)) { + console.error(`Error in numUsersParticipatingNow: needMap.users is not an array`); + return; + } + return needMap.users.length; +}; + diff --git a/imports/api/Testing/ce_examples.js b/imports/api/Testing/ce_examples.js new file mode 100644 index 00000000..d58d2c59 --- /dev/null +++ b/imports/api/Testing/ce_examples.js @@ -0,0 +1,255 @@ +// /** createHalfHalf +// * +// * @param numberInSituation [Integer] number of people that need to be in the same situation at the same time +// * @param notificationDelay [Integer] notificationDelay for all places +// * @returns {{name: string, participateTemplate: string, resultsTemplate: string, contributionTypes: Array, description: string, notificationText: string, callbacks: Array}} +// */ +// const createHalfHalf = function( +// { +// numberInSituation = 1, +// notificationDelay = 90 +// } = {} +// ) { +// let experience = { +// name: 'Half Half Bumped', +// participateTemplate: 'halfhalfParticipate', +// resultsTemplate: 'halfhalfResults', +// contributionTypes: [], +// description: 'Participate in HalfHalf Travel: Capture your side of the story', +// notificationText: 'Participate in HalfHalf Travel: Capture your side of the story', +// callbacks: [] +// }; +// +// +// let completedCallback = function(sub) { +// let submissions = Submissions.find({ +// iid: sub.iid, +// needName: sub.needName +// }).fetch(); +// +// let participants = submissions.map((submission) => { return submission.uid; }); +// +// notify(participants, sub.iid, +// `Two people completed a half half photo`, +// `See the results under ${sub.needName}`, +// '/apicustomresults/' + sub.iid + '/' + sub.eid); +// }; +// +// let places = [ +// ["bar", "at a bar", notificationDelay], +// ["coffee", "at a coffee shop", notificationDelay], +// ["grocery", "at a grocery store", notificationDelay], +// ["restaurant", "at a restaurant", notificationDelay], +// ["train", "commuting", notificationDelay], +// ["exercising", "exercising", notificationDelay] +// ]; +// +// _.forEach(places, (place) => { +// +// let [detectorName, situationDescription, delay] = place; +// +// let need = { +// needName: `half half: ${situationDescription}`, +// situation: { +// detector: DETECTORS[detectorName]._id, +// number: numberInSituation +// }, +// toPass: { +// instruction: `Having a good time ${situationDescription}? Try taking one side of a photo.` +// }, +// numberNeeded: 2, +// notificationDelay: delay +// }; +// +// let callback = { +// trigger: `cb.numberOfSubmissions("${need.needName}") % 2`, +// function: completedCallback.toString(), +// }; +// experience.contributionTypes.push(need); +// experience.callbacks.push(callback) +// }); +// +// return experience; +// }; +// +// const createBumpedThree = function() { +// // console.log(DETECTORS); +// const bumpedThreeCallback = function (sub) { +// let submissions = Submissions.find({ +// iid: sub.iid, +// needName: sub.needName +// }).fetch(); +// +// let participants = submissions.map((submission) => { return submission.uid; }); +// +// notify(participants, sub.iid, 'See images from your group bumped experience!', '', '/apicustomresults/' + sub.iid + '/' + sub.eid); +// +// } +// +// let experience = { +// name: 'Group Bumped', +// participateTemplate: 'bumpedThreeInitial', +// resultsTemplate: 'bumpedThreeResults', +// contributionTypes: [], +// description: 'Share your experience with your friend and their friend!', +// notificationText: 'Share your experience with your friend and their friend!', +// callbacks: [{ +// trigger: `cb.numberOfSubmissions() === 3`, +// function: bumpedThreeCallback.toString(), +// }] +// }; +// +// +// const staticAffordances = ['participantOne', 'participantTwo', 'participantThree']; +// const places = [ +// ["coffee", "at a coffee shop", "Please help us build the story by answering some initial questions about your situation!"], +// ]; +// +// // const needs = places.map(place => { +// // const [detectorName, situationDescription, instruction] = place; +// // return { +// // needName: `Bumped Three ${detectorName}`, +// // situation: { +// // detector: getDetectorId(DETECTORS[detectorName]), +// // number: '1' +// // }, +// // toPass: { +// // situationDescription: `Having a good time ${situationDescription}?`, +// // instruction: `${instruction}` +// // }, +// // numberNeeded: 3, +// // // notificationDelay: 90 uncomment for testing +// // } +// // }); +// +// +// staticAffordances.forEach(participant => { +// experience.contributionTypes = [...experience.contributionTypes, ...addStaticAffordanceToNeeds(participant, ((places) => +// places.map(place => { +// const [detectorName, situationDescription, instruction] = place; +// return { +// needName: `Bumped Three ${detectorName}`, +// situation: { +// detector: getDetectorId(DETECTORS[detectorName]), +// number: 1 +// }, +// toPass: { +// situationDescription: `Having a good time ${situationDescription}?`, +// instruction: `${instruction}` +// }, +// numberNeeded: 3, +// // notificationDelay: 90 uncomment for testing +// } +// }) +// )(places))]; +// }); +// +// return experience; +// } +// +// const sameSituationContributionTypes = function( +// { +// numberInSituation = 1 +// } = {} +// ) { +// return [{ +// needName: 'Warm, Sunny Weather', +// situation: { +// detector: DETECTORS.sunny._id, +// number: numberInSituation +// }, +// toPass: { +// instruction: 'Are you enjoying good weather today? Share a photo of how you are experiencing the sun.' +// }, +// numberNeeded: 50, +// notificationDelay: 1, +// allowRepeatContributions: true, +// }]; +// }; +// +// +// +// const create24hoursContributionTypes = function(toPassConstructor, numberNeeded) { +// let needs = []; +// for (i = 0; i < 24; i++) { +// let need = { +// needName: `hour ${i}`, +// situation: { +// detector: DETECTORS[`hour${i}`]._id, +// number: 1 +// }, +// toPass: toPassConstructor(i), +// numberNeeded: numberNeeded, +// notificationDelay: 1 +// }; +// needs.push(need); +// } +// return needs; +// }; +// +// /** halfhalfRespawnAndNotify: +// * This is a helper function that generates a callback function definition +// * The callback will respawn or create a duplicate of the need that just completed, +// * while also sending notifications to the participants of that need. +// * +// * This function makes strong assumptions about how your OCE contributionTypes are written. +// * i.e. need.needName = 'Name of my need 1' +// * i.e. need.needName = 'Hand Silhouette 1' +// * +// * @param subject [String] subject of notification +// * @param text [String] accompanying subtext of notification +// * @return {any} A function +// */ +// const halfhalfRespawnAndNotify = function(subject, text) { +// functionTemplate = function (sub) { +// let contributionTypes = Incidents.findOne(sub.iid).contributionTypes; +// let need = contributionTypes.find((x) => { +// return x.needName === sub.needName; +// }); +// +// // Convert Need Name i to Need Name i+1 +// let splitName = sub.needName.split(' '); +// let iPlus1 = Number(splitName.pop()) + 1; +// splitName.push(iPlus1); +// let newNeedName = splitName.join(' '); +// +// need.needName = newNeedName; +// addContribution(sub.iid, need); +// +// let participants = Submissions.find({ +// iid: sub.iid, +// needName: sub.needName +// }).map((submission) => { +// return submission.uid; +// }); +// +// notify(participants, sub.iid, '${subject}', '${text}', '/apicustomresults/' + sub.iid + '/' + sub.eid); +// }; +// return eval('`'+functionTemplate.toString()+'`'); +// }; +// +// /* recognizes when experience ends +// let sendNotificationScavenger = function (sub) { +// let uids = Submissions.find({ iid: sub.iid }).fetch().map(function (x) { +// return x.uid; +// }); +// +// notify(uids, sub.iid, 'Wooh! All the scavenger hunt items were found. Click here to see all of them.', '', '/apicustomresults/' + sub.iid + '/' + sub.eid); +// }; +// +// const sendNotificationTwoHalvesCompleted = function(sub) { +// console.log("Another pair of halves completed a photo"); +// +// let submissions = Submissions.find({ +// iid: sub.iid, +// needName: sub.needName +// }).fetch(); +// +// let participants = submissions.map((submission) => { return submission.uid; }); +// +// notify(participants, sub.iid, +// `Two people completed a half half photo`, +// `See the results under ${sub.needName}`, +// '/apicustomresults/' + sub.iid + '/' + sub.eid); +// }; +// */ \ No newline at end of file diff --git a/imports/api/Testing/chi20experiences.js b/imports/api/Testing/chi20experiences.js new file mode 100644 index 00000000..dd935421 --- /dev/null +++ b/imports/api/Testing/chi20experiences.js @@ -0,0 +1,358 @@ +import {getDetectorId} from "./testingconstants"; +import { CONSTANTS } from "./testingconstants"; +import {Submissions} from "../OCEManager/currentNeeds"; +import {notify} from "../OpportunisticCoordinator/server/noticationMethods"; +import {Meteor} from "meteor/meteor"; +import {addContribution} from "../OCEManager/OCEs/methods"; + +let DETECTORS = CONSTANTS.DETECTORS; + +// current model +// 0. to create a new experience, we write a fully defined CE API spec as as static definition +// 1. on file load, the detectors and experiences JSON are created [by accessing the current static definition of DETECTORS or the existing Detectors database] +// 2. on updateOCE, we simply access this static definition and make the database reflect this static definition + +// current obstacle with current model +// 0. across different branches, it's hard to manage all the experiences we are creating +// updateOCE finds on experience name, but the experiences for different groups are named the same + +// findings with using current model +// 0. all the linking logic happens at static definition time, in one file +// 0a. this means we need to make sure on file load, all the links are sorted out + +// what if we used factory method pattern? to create objects not on file load, but based on runtime calls +// 0. this is less of a "fixtures" model, but rather building blocks that can be used at run time +// 1. there could be factory methods for creation/updating of OCEs +// 2. definitions for different studies or deployments are activated through runtime e.g., meteor method calls +// 3. that would allow deployments to differ not based on code, but purely on the database? + + + +let CHI20_Olin_EXPERIENCES = { + halfhalf_sunny_knowsOlin: { + _id: Random.id(), + name: 'Hand Silhouette', + group: 'Olin', + participateTemplate: 'halfhalfParticipate', + resultsTemplate: 'halfhalfResults', + contributionTypes: addStaticAffordanceToNeeds('knowsOlin', [{ + // needName MUST have structure "My Need Name XYZ" + needName: 'Hand Silhouette 1', + situation: { + detector: getDetectorId(DETECTORS.sunny), + number: '1' + }, + toPass: { + instruction: 'Is the weather clear and sunny where you are? Take a photo, holding your hand towards the sky, covering the sun.', + exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-hands-in-front.jpg' + }, + numberNeeded: 2, + numberAllowedToParticipateAtSameTime: 1, + notificationDelay: 1, + }]), + description: 'Use the sun to make a silhouette of your hand', + notificationText: 'View this and other available experiences', + callbacks: [{ + trigger: '(cb.numberOfSubmissions() % 2) === 0', + function: halfhalfRespawnAndNotify('A hand silhouette was completed','View the photo').toString() + }] + }, + halfhalf_grocery_knowsOlin: { + _id: Random.id(), + name: 'Grocery Buddies', + group: 'Olin', + participateTemplate: 'halfhalfParticipate', + resultsTemplate: 'halfhalfResults', + contributionTypes: addStaticAffordanceToNeeds('knowsOlin', [{ + // needName MUST have structure "My Need Name XYZ" + needName: 'Grocery Buddies 1', + notificationSubject: 'Inside a grocery store?', + notificationText: 'Share an experience with others who are also grocery shopping', + situation: { + detector: getDetectorId(DETECTORS.grocery), + number: '1' + }, + toPass: { + instruction: 'Are you at the grocery store? Take a photo, holding a fruit or vegetable outstretched with your hands.', + exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-fruit-in-hand.jpg' + }, + numberNeeded: 2, + numberAllowedToParticipateAtSameTime: 1, + notificationDelay: 90, + }]), + description: 'While shopping for groceries, create a half half photo.', + notificationText: 'View this and other available experiences', + callbacks: [{ + trigger: '(cb.numberOfSubmissions() % 2) === 0', + function: halfhalfRespawnAndNotify('A Grocery Buddies photo completed','View the photo').toString() + }] + }, + halfhalf_bar_knowsOlin: { + _id: Random.id(), + name: 'Cheers', + group: 'Olin', + participateTemplate: 'halfhalfParticipate', + resultsTemplate: 'halfhalfResults', + contributionTypes: addStaticAffordanceToNeeds('knowsOlin', [{ + // needName MUST have structure "My Need Name XYZ" + needName: 'Cheers 1', + notificationSubject: 'Drinking at a bar?', + notificationText: 'Share an experience with others who are also drinking at a bar', + situation: { + detector: getDetectorId(DETECTORS.bar), + number: '1' + }, + toPass: { + instruction: 'What are you drinking at the bar? Take a photo, while raising your glass or bottle in front of you.', + exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-cheers.jpg' + }, + numberNeeded: 2, + numberAllowedToParticipateAtSameTime: 1, + notificationDelay: 90 + }]), + description: 'While enjoying your drink, create a half half photo.', + notificationText: 'View this and other available experiences', + callbacks: [{ + trigger: '(cb.numberOfSubmissions() % 2) === 0', + function: halfhalfRespawnAndNotify('A Cheers photo completed','View the photo').toString() + }] + }, + halfhalf_religious_knowsOlin: { + _id: Random.id(), + name: 'Religious Architecture', + group: 'Olin', + participateTemplate: 'halfhalfParticipate', + resultsTemplate: 'halfhalfResults', + contributionTypes: addStaticAffordanceToNeeds('knowsOlin', [{ + // needName MUST have structure "My Need Name XYZ" + needName: 'Religious Architecture 1', + notificationSubject: 'Visiting a place of worship?', + notificationText: 'Share an experience with others who are also visiting a place of worship', + situation: { + detector: getDetectorId(DETECTORS.castle), + number: '1' + }, + toPass: { + instruction: 'Do you notice the details of the religious building near you? Do so now, by outstretching your hand and pointing out of the elements that stick out to you most.', + exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-religious-building.jpg' + }, + numberNeeded: 2, + numberAllowedToParticipateAtSameTime: 1, + notificationDelay: 90, + }]), + description: 'While visiting a place of worship, create a half half photo.', + notificationText: 'View this and other available experiences', + callbacks: [{ + trigger: '(cb.numberOfSubmissions() % 2) === 0', + function: halfhalfRespawnAndNotify('A Religious Architecture photo completed','View the photo').toString() + }] + }, + halfhalf_sunset_knowsOlin: { + _id: Random.id(), + name: 'Sunset Together', + group: 'Olin', + participateTemplate: 'halfhalfParticipate', + resultsTemplate: 'halfhalfResults', + contributionTypes: addStaticAffordanceToNeeds('knowsOlin', [{ + // needName MUST have structure "My Need Name XYZ" + needName: 'Sunset Together 1', + notificationSubject: 'Can you see the sunset?', + notificationText: 'Share an experience with others who are also watching the sunset', + situation: { + detector: getDetectorId(DETECTORS.sunset), + number: '1' + }, + toPass: { + instruction: 'What does the sunset look like where you are? Find a good view; then, take a photo, with your hands outstretched towards the sun.', + exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-sunset-heart.jpg' + }, + numberNeeded: 2, + numberAllowedToParticipateAtSameTime: 1, + notificationDelay: 1, + }]), + description: 'While looking up at the sunset, create a half half photo.', + notificationText: 'View this and other available experiences', + callbacks: [{ + trigger: '(cb.numberOfSubmissions() % 2) === 0', + function: halfhalfRespawnAndNotify('A Sunset Together photo completed','View the photo').toString() + }] + }, + halfhalf_asian_knowsOlin: { + _id: Random.id(), + name: 'Eating with Chopsticks', + group: 'Olin', + participateTemplate: 'halfhalfParticipate', + resultsTemplate: 'halfhalfResults', + contributionTypes: addStaticAffordanceToNeeds('knowsOlin', [{ + // needName MUST have structure "My Need Name XYZ" + needName: 'Eating with Chopsticks 1', + notificationSubject: 'Eating at an asian restaurant?', + notificationText: 'Share an experience with others who are also eating asian food', + situation: { + detector: getDetectorId(DETECTORS.eating_with_chopsticks), + number: '1' + }, + toPass: { + instruction: 'Are you eating asian food right now? Take a photo of what you are eating, holding your chopsticks.', + exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-holding-chopsticks.jpg' + }, + numberNeeded: 2, + numberAllowedToParticipateAtSameTime: 1, + notificationDelay: 90 + }]), + description: 'While eating asian food, create a half half photo.', + notificationText: 'View this and other available experiences', + callbacks: [{ + trigger: '(cb.numberOfSubmissions() % 2) === 0', + function: halfhalfRespawnAndNotify('An Eating with Chopsticks photo completed','View the photo').toString() + }] + }, + halfhalf_books_knowsOlin: { + _id: Random.id(), + name: 'Book Buddies', + group: 'Olin', + participateTemplate: 'halfhalfParticipate', + resultsTemplate: 'halfhalfResults', + contributionTypes: addStaticAffordanceToNeeds('knowsOlin', [{ + // needName MUST have structure "My Need Name XYZ" + needName: 'Book Buddies 1', + notificationSubject: 'Are you at a library?', + notificationText: 'Share an experience with others who are also at the library', + situation: { + detector: getDetectorId(DETECTORS.library), + number: '1' + }, + toPass: { + instruction: 'Sorry to interrupt your reading! Find the nearest book, and take a photo holding up the book to your face.', + exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-book-face.jpg' + }, + numberNeeded: 2, + numberAllowedToParticipateAtSameTime: 1, + notificationDelay: 90, + }]), + description: 'While reading a book, create a half half photo.', + notificationText: 'View this and other available experiences', + callbacks: [{ + trigger: '(cb.numberOfSubmissions() % 2) === 0', + function: halfhalfRespawnAndNotify('A Book Buddies photo completed','View the photo').toString() + }] + }, + halfhalf_leakmask_knowsOlin: { + _id: Random.id(), + name: 'Leaf Mask', + group: 'Olin', + participateTemplate: 'halfhalfParticipate', + resultsTemplate: 'halfhalfResults', + contributionTypes: addStaticAffordanceToNeeds('knowsOlin', [{ + // needName MUST have structure "My Need Name XYZ" + needName: 'Leaf Mask 1', + notificationSubject: 'Are you at a park?', + notificationText: 'Share an experience with others who are also at a park', + situation: { + detector: getDetectorId(DETECTORS.forest), + number: '1' + }, + toPass: { + instruction: 'Find a leaf in the park. Take a photo of the leaf covering your face, like it was a mask.', + exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-leaf-face.jpg' + }, + numberNeeded: 2, + numberAllowedToParticipateAtSameTime: 1, + notificationDelay: 90 + }]), + description: 'While in the park, create a half half photo.', + notificationText: 'View this and other available experiences', + callbacks: [{ + trigger: '(cb.numberOfSubmissions() % 2) === 0', + function: halfhalfRespawnAndNotify('A Feet to the trees photo completed','View the photo').toString() + }] + }, + halfhalf_puddles_knowsOlin: { + _id: Random.id(), + name: 'Puddle Feet', + group: 'Olin', + participateTemplate: 'halfhalfParticipate', + resultsTemplate: 'halfhalfResults', + contributionTypes: addStaticAffordanceToNeeds('knowsOlin', [{ + // needName MUST have structure "My Need Name XYZ" + needName: 'Puddle Feet 1', + notificationSubject: 'Are you outside while its raining?', + notificationText: 'Share an experience with others who are enjoying or enduring the rain', + situation: { + detector: getDetectorId(DETECTORS.rainy), + number: '1' + }, + toPass: { + instruction: 'Is it raining today? Find a puddle on the ground. Take a photo of yourself, stomping on the puddle!', + exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-puddle-feet.jpg' + }, + numberNeeded: 2, + numberAllowedToParticipateAtSameTime: 1, + notificationDelay: 1, + }]), + description: 'With the puddles on a rainy day, create a half half photo.', + notificationText: 'View this and other available experiences', + callbacks: [{ + trigger: '(cb.numberOfSubmissions() % 2) === 0', + function: halfhalfRespawnAndNotify('A "Puddle Feet" photo completed','View the photo').toString() + }] + }, + halfhalf_coffeesideoflaughs_knowsOlin: { + _id: Random.id(), + name: "Coffee with a side of Laughs", + group: 'Olin', + participateTemplate: 'halfhalfParticipate', + resultsTemplate: 'halfhalfResults', + contributionTypes: addStaticAffordanceToNeeds('knowsOlin', [{ + // needName MUST have structure "My Need Name XYZ" + needName: "Coffee with a side of Laughs 1", + notificationSubject: 'Inside a coffee shop?', + notificationText: 'Share an experience with others who are also at a coffee shop', + situation: { + detector: getDetectorId(DETECTORS.coffee), // any place that has cups (cafes + bars + restaurants) + number: '1' + }, + toPass: { + instruction: `Do you have a cup or glass you are drinking? Take a photo with it in the middle of the picture. You can even try to pour some extra "cream" into it too!`, + exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-teasing-lotion-in-a-cup.jpg' + }, + numberNeeded: 2, + notificationDelay: 90, + }]), + description: 'While drinking coffee at a cafe, create a half half photo.', + notificationText: 'View this and other available experiences', + callbacks: [{ + trigger: '(cb.numberOfSubmissions() % 2) === 0', + function: halfhalfRespawnAndNotify("A 'Coffee with a side of Laughs' photo completed",'View the photo').toString() + }] + }, + halfhalf_bigbites_knowsOlin: { + _id: Random.id(), + name: "Big Bites", + group: 'Olin', + participateTemplate: 'halfhalfParticipate', + resultsTemplate: 'halfhalfResults', + contributionTypes: addStaticAffordanceToNeeds('knowsOlin', [{ + needName: "Big Bites 1", // Any restaurant that would serve something you'd eat with your hands (burrito, tacos, hotdogs, sandwiches, wraps, burgers, tradamerican, newamerican ) + notificationSubject: 'Eating at a restaurant?', + notificationText: 'Share an experience with others who are enjoying big bites of their meal', + situation: { + detector: getDetectorId(DETECTORS.big_bite_restaurant), + number: '1' + }, + toPass: { + instruction: `Are you eating food that would require a big bite right now? Take a photo of yourself holding up your food to the middle of the screen.`, + exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-big-bite.jpg' + }, + numberNeeded: 2, + numberAllowedToParticipateAtSameTime: 1, + notificationDelay: 90, // https://www.quora.com/Whats-the-average-time-that-customers-wait-between-entering-a-restaurant-and-getting-served + }]), + description: 'While eating some non-trivially sized food, create a half half photo.', + notificationText: 'View this and other available experiences', + callbacks: [{ + trigger: '(cb.numberOfSubmissions() % 2) === 0', + function: halfhalfRespawnAndNotify("A 'Big Bites' photo completed",'View the photo').toString() + }] + }, +}; diff --git a/imports/api/Testing/cn.js b/imports/api/Testing/cn.js new file mode 100644 index 00000000..17d02f91 --- /dev/null +++ b/imports/api/Testing/cn.js @@ -0,0 +1,63 @@ +export const cn = () => { + let title = 'Murder Mystery'; + let description = "You've been invited to participate in a murder mystery!"; + let notification = 'Help us catch a killer!'; + let setting = ['coffee', 'at a coffee shop']; + let templates = ['CNstart', 'CNchat']; + let question1 = { + question: 'Name an interesting option on the menu', + responseType: 'text', + responseData: 'menu' + } + let question2 = { + question: 'What did you order?', + responseType: 'text', + responseData: 'order' + } + let question3 = { + question: 'How busy is the coffee shop right now?', + responseType: 'dropdown', + responseData: ['not busy at all', 'a little busy', 'somewhat busy', 'pretty busy', 'very busy'] + } + let questions = [question1, question2, question3]; + let char1 = { + roleName: 'murderer', + instruction: 'Try to avoid being caught and weasel your way out of the clues!', + context: ['very busy'], + max: 1 //ensures only one murderer gets cast, even if multiple people satisfy this context + } + let char2 = { + roleName: 'innocent', + instruction: 'Try to prove your innocence and find the real murderer!', + context: ['not busy at all', 'a little busy', 'somewhat busy', 'pretty busy'], + max: 3 + } + let characters = [char1, char2]; + let prompt1 = { + prompt: 'We have our first clue. The murderer is in a very busy coffee shop! As a group, discuss and try to figure out which one of you is the murderer. Send photos of the coffee shop you are in to provide evidence of your innocence. You will receive a new clue every three minutes.', + info: '', + timing: 10 //seconds after casting occurs + } + let prompt2 = { + prompt: "Here's the second clue. The murderer ordered ", + info: 'order', + timing: 200 + } + let prompt3 = { + prompt: "Here's the last clue. The murderer is in a coffee shop that sells ", + info: 'menu', + timing: 400, + } + let prompt4 = { + prompt: "Now it's time to cast your vote! Who do you think the murderer is? Send your response in the chat within the next 30 seconds.", + info: '', + timing: 600 + } + let prompt5 = { + prompt: "Now that your votes are cast, let's find out who's right. The murderer is ", + info: 'user', + timing: 630 + } + let prompts = [prompt1, prompt2, prompt3, prompt4, prompt5]; + return [title, description, notification, setting, templates, questions, characters, prompts]; +} \ No newline at end of file diff --git a/imports/api/Testing/conversion.js b/imports/api/Testing/conversion.js new file mode 100644 index 00000000..24028b59 --- /dev/null +++ b/imports/api/Testing/conversion.js @@ -0,0 +1,60 @@ +// //CE has needs +// //CN has characters, which each have a context attribute, which is set to needs +// +// //CE has uids for participants +// //CN hs characters, which each have a participant attribute, which is set to a uid or null +// +// //CE has iids +// //CN has stories whose instances have iids +// +// //CE experiences callback into each other +// //CN story chapters callback into each other until the end condition is reached. +// +// character { +// name: string +// specific: boolean +// /* +// if specific == true: +// character will be referred to as character.name when referred to in text +// else: +// character will be referred to by the participant's name +// */ +// participant: uid //assigned based on context, or manually set by author +// context: detector + static affordances +// /* +// if participant is set manually by the author before the story begins OR recast == false and participant != null (the participant has already been cast at least once) +// add the participant to the context so that the system knows to look for that specific person in the context +// */ +// recast: boolean +// /* +// if recast == true: +// participant is reset to null after the chapter ends +// */ +// } +// +// /* +// general flow of casting +// get affordances from context tracker +// match participants to characters who have those contexts +// set participant attribute of character through uid +// experience runs +// if recast is true, reset participant attributes +// iid remains the same +// restart cycle of context tracker +// */ +// IMPORTANT: can characters in the same chapter have different contexts, or do they all need to have the same one? +// +// //prompts require a response from participants. +// prompt { +// question: string +// response: +// string, +// picture, +// range +// //response usually maps to a story variable +// } +// +// text can usually be written as string + var + string +// +// //getting variable information from detectors +// var restaurant = character.participant.situation.detector \ No newline at end of file diff --git a/imports/api/Testing/createNewExperiences.js b/imports/api/Testing/createNewExperiences.js index 4993be7b..7d660ed3 100644 --- a/imports/api/Testing/createNewExperiences.js +++ b/imports/api/Testing/createNewExperiences.js @@ -1,4 +1,4 @@ -import { Meteor } from "meteor/meteor"; +// import { Meteor } from "meteor/meteor"; import { Submissions } from "../OCEManager/currentNeeds"; import { Detectors } from "../UserMonitor/detectors/detectors"; diff --git a/imports/api/Testing/endToEndSimple.test.js b/imports/api/Testing/endToEndSimple.test.js index de6fca16..62f64419 100644 --- a/imports/api/Testing/endToEndSimple.test.js +++ b/imports/api/Testing/endToEndSimple.test.js @@ -64,9 +64,8 @@ describe('Simple End To End', function () { let iid = incident._id; let user = findUserByUsername(USERNAME); - console.log('user.profile.activeIncidents', user.profile.activeIncidents); //user has incident as an active incident - chai.assert(user.profile.activeIncidents.includes(iid), 'active incident not added to user profile'); + chai.assert(user.activeIncidents().includes(iid), 'active incident not added to user profile'); //assignments has user assigned let assignmentEntry = Assignments.findOne({ _id: iid }); @@ -115,9 +114,8 @@ describe('Simple End To End', function () { let iid = incident._id; let user = findUserByUsername(USERNAME); - console.log('user.profile.activeIncidents', user.profile.activeIncidents); //user has incident as an active incident - chai.assert(user.profile.activeIncidents.includes(iid), 'decommissioned prematurely - active incident not added to user profile'); + chai.assert(user.activeIncidents().includes(iid), 'decommissioned prematurely - active incident not added to user profile'); //assignments has user assigned let assignmentEntry = Assignments.findOne({ _id: iid }); @@ -148,9 +146,8 @@ describe('Simple End To End', function () { let iid = incident._id; let user = findUserByUsername(USERNAME); - console.log('user.profile.activeIncidents', user.profile.activeIncidents); //user has incident as an active incident - chai.assert(user.profile.activeIncidents.includes(iid), 'remain assigned while back in vicinity -- active incident not added to user profile'); + chai.assert(user.activeIncidents().includes(iid), 'remain assigned while back in vicinity -- active incident not added to user profile'); //assignments has user assigned let assignmentEntry = Assignments.findOne({ _id: iid }); @@ -191,7 +188,7 @@ describe('Simple End To End', function () { Meteor.setTimeout(function () { try { let user = findUserByUsername(USERNAME); - chai.assert.isFalse(user.profile.activeIncidents.includes(iid), 'active incident not removed from user profile'); + chai.assert.isFalse(user.activeIncidents().includes(iid), 'active incident not removed from user profile'); chai.assert(user.profile.pastIncidents.includes(iid), 'past incident not added to user profile'); done(); } catch (err) { done(err); } @@ -231,9 +228,8 @@ describe('Simple End To End', function () { let iid = incident._id; let user = findUserByUsername(USERNAME); - console.log('user.profile.activeIncidents', user.profile.activeIncidents); //user has incident as an active incident - chai.assert(user.profile.activeIncidents.includes(iid), 'active incident not added to user profile'); + chai.assert(user.activeIncidents().includes(iid), 'active incident not added to user profile'); //assignments has user assigned let assignmentEntry = Assignments.findOne({ _id: iid }); diff --git a/imports/api/Testing/mincn.js b/imports/api/Testing/mincn.js new file mode 100644 index 00000000..d4c20406 --- /dev/null +++ b/imports/api/Testing/mincn.js @@ -0,0 +1,36 @@ +export const MurderMystery = () => { + let title = 'Murder Mystery'; + let setting = ['coffee', 'at a coffee shop']; + let question1 = { + question: 'How busy is the coffee shop right now?', + responseType: 'dropdown', + responseData: ['not busy at all', 'a little busy', 'somewhat busy', 'pretty busy', 'very busy'] + } + let question2 = { + question: 'What did you order?', + responseType: 'text', + responseData: 'order' + } + let char1 = { + roleName: 'murderer', + instruction: 'Try to avoid being caught and weasel your way out of the clues!', + context: max(question1.responseData) + } + let char2 = { + roleName: 'innocent', + instruction: 'Try to prove your innocence and find the real murderer!', + context: !max(question1.responseData) + } + let characters = [char1, char2]; + let prompt1 = { + prompt: 'We have our first clue. The murderer is in a ' + murderer.question1.responseData + ' coffee shop!', + timing: 10 //seconds after casting occurs + } + let prompt2 = { + prompt: "Here's the second clue. The murderer ordered " + murderer.question2.responseData, + timing: 80 + } + let prompts = [prompt1, prompt2, prompt3, prompt4, prompt5]; + return [title, description, notification, setting, templates, questions, characters, prompts]; +} + diff --git a/imports/api/Testing/storybook.js b/imports/api/Testing/storybook.js deleted file mode 100644 index ec3374f9..00000000 --- a/imports/api/Testing/storybook.js +++ /dev/null @@ -1,57 +0,0 @@ -let Affinder = []; -let needComplete = function(){}; -let sendTimelapseReadyNotification = function(){}; -let page = [] - -let exp; -exp = { - name: 'Storybook', - - needs: [ - { - needName: 'page0', - situation: - { - detector: Affinder.lookup('Harry Potter Castle'), - number: 1 - }, - passToTemplate: {sentence: 'Harry looked up at the towering castle!'}, - numberNeeded: 1 - }], - - callbacks: [ - { - trigger: "newSubmission() && (numberOfSubmissions() <= 7)", - callbackFunction: - function (newSubmission) { - let newNeed = { - "needName": page + i, - "situation": { - "detector": Affinder.lookup(newSubmission.content.nextSituation), - "number": 1 - }, - ... - }; - addNeed(experienceId, newNeed); - } - - ], - - participateTemplate: 'storyPage', - resultsTemplate: 'book', - -}; - - - - -v = callbackFunction; - - - - - - - - -; \ No newline at end of file diff --git a/imports/api/Testing/sunsetOCE.js b/imports/api/Testing/sunsetOCE.js deleted file mode 100644 index 1c7f25cc..00000000 --- a/imports/api/Testing/sunsetOCE.js +++ /dev/null @@ -1,27 +0,0 @@ -{ - name: 'Sunset', - participateTemplate -: - 'uploadPhoto', - resultsTemplate -: - 'sunset', - needs -: - [{ - needName: 'sunset', situation: {detector: DETECTORS.sunset._id, number: '1'}, - toPass: {instruction: 'Take a photo of the sunset!'}, numberNeeded: 20 - }], - description -: - 'Create a timelapse of the sunset with others around the country', - notificationText -: - 'Take a photo of the sunset!', - callbacks -: - [{ - trigger: 'cb.incidentFinished()', - function: sendNotificationSunset.toString() - }] -} diff --git a/imports/api/Testing/testingconstants.js b/imports/api/Testing/testingconstants.js index bf34a6d8..4035a3de 100644 --- a/imports/api/Testing/testingconstants.js +++ b/imports/api/Testing/testingconstants.js @@ -1,13 +1,9 @@ -import { Meteor } from "meteor/meteor"; - +// import { Meteor } from "meteor/meteor"; import { Submissions } from "../OCEManager/currentNeeds"; - import { addContribution } from '../OCEManager/OCEs/methods'; -import {Detectors} from "../UserMonitor/detectors/detectors"; -import {notify, notifyUsersInIncident, notifyUsersInNeed} from "../OpportunisticCoordinator/server/noticationMethods"; -import {Incidents} from "../OCEManager/OCEs/experiences"; -import {Schema} from "../schema"; -import {serverLog} from "../logs"; +import { Detectors } from "../UserMonitor/detectors/detectors"; +import { Incidents } from "../OCEManager/OCEs/experiences"; +import { cn } from "./cn"; let LOCATIONS = { 'park': { @@ -37,27 +33,6 @@ let LOCATIONS = { }; let USERS = { - garrett: { - username: 'garrett', - email: 'garret@email.com', - password: 'password', - profile: { - firstName: 'Garrett', - lastName: 'Hedman', - staticAffordances: { - mechanismRich: true - } - } - }, - garretts_brother: { - username: 'garretts_brother', - email: 'garretts_brother@email.com', - password: 'password', - profile: { - firstName: 'Barrett', // theres an inside joke to this one - lastName: 'Hedman' - } - }, meg: { username: 'meg', email: 'meg@email.com', @@ -67,15 +42,6 @@ let USERS = { lastName: 'Grasse' } }, - megs_sister: { - username: 'megs_sister', - email: 'megs_sister@email.com', - password: 'password', - profile: { - firstName: 'Sister of Meg', - lastName: 'Grasse' - } - }, andrew: { username: 'andrew', email: 'andrew@email.com', @@ -93,166 +59,32 @@ let USERS = { firstName: 'Josh', lastName: 'Shi' } - }, - nagy: { - username: 'nagy', - email: 'nagy@email.com', - password: 'password', - profile: { - firstName: 'Nagy', - lastName: 'Hakim' - } - }, - bonnie: { - username: 'bonnie', - email: 'bonnie@email.com', - password: 'password', - profile: { - firstName: 'Bonnie', - lastName: 'Ishiguro' - } } }; let DETECTORS = { - field: { - _id: 'XeepEbMjjW8yPzSAo', - description: 'fields', - variables: ['var stadiumsarenas;', - 'var baseballfields;', - 'var parks;', - 'var playgrounds;' - ], - rules: ['stadiumsarenas || ((parks || playgrounds) || baseballfields);'] - }, - niceish_day: { - _id: 'x7EgLErQx3qmiemqt', - description: 'niceish_day', - variables: ['var clouds;', 'var clear;', 'var daytime;'], - rules: ['daytime && (clouds || clear);'] - }, - night: { - _id: 'Wth3TB9Lcf6me6vgy', - description: 'places where it\'s nighttime,', - variables: ['var nighttime;'], - rules: ['(nighttime);'] - }, - sunset: { - _id: '44EXNzHS7oD2rbF68', - description: 'places where it\'s sunset,', - variables: ['var sunset;'], - rules: ['(sunset);'] - }, daytime: { _id: 'tyZMZvPKkkSPR4FpG', description: 'places where it\'s daytime,', variables: ['var daytime;'], rules: ['daytime;'] }, - library: { - _id: '5LqfPRajiQRe9BwBT', - description: 'libraries and other books', - variables: [ - 'var libraries;', - 'var usedbooks;', - 'var bookstores;' - ], - rules: ['(libraries || bookstores);'] - }, - gym: { - _id: '3XqHN8A4EpCZRpegS', - description: ' gym', - variables: ['var gyms;'], - rules: [' gyms;'] - }, - produce: { - _id: 'xDtnmQW3PBMuqq9pW', - description: 'places to find fruits and veggies', - variables: ['var communitygardens;', - 'var intlgrocery;', - 'var ethicgrocery;', - 'var markets;', - 'var grocery;', - 'var farmersmarket;', - 'var organic_stores;' - ], - rules: ['communitygardens || ((intlgrocery || ethicgrocery) || ((markets || grocery) || (farmersmarket || organic_stores)));'] - }, - rainbow: { - _id: 'ksxGTXMaSpCFdmqqN', - description: 'rainbow flag', - variables: ['var gaybars;'], - rules: ['gaybars;'] - }, - drugstore: { - _id: 'k8KFfv3ATtbg2tnFB', - description: 'drugstores', - variables: ['var drugstores;', 'var pharmacy;'], - rules: ['(drugstores || pharmacy);'] - }, - costume_store: { - _id: 'ECPk2mjuHJtrMotGg', - description: 'costume_store', - variables: ['var costumes;', 'var partysupplies;'], - rules: ['(partysupplies || costumes);'] - }, - irish: { - _id: '5CJGGtjqyY89n55XP', - description: 'irish', - variables: ['var irish_pubs;', 'var irish;'], - rules: ['(irish_pubs || irish);'] - }, - hair_salon: { - _id: 'S8oZZwAWpFo5qGq87', - description: 'hairsalon', - variables: ['var menshair;', - 'var hairstylists;', - 'var hair_extensions;', - 'var blowoutservices;', - 'var hair;', - 'var barbers;' - ], - rules: ['menshair || ((hairstylists || hair_extensions) || ((hair || barbers) || blowoutservices));'] - }, - gas_station: { - _id: 'CctuBr3GtSXPkzNDQ', - description: 'gas station', - variables: ['var servicestations;'], - rules: ['servicestations;'] - }, coffee: { _id: 'saxQsfSaBiHHoSEYK', description: 'coffee', variables: ['var coffeeroasteries;', - 'var coffee;', - 'var cafes;', - 'var coffeeshops;', - 'var coffeeteasupplies;' + 'var coffee;', + 'var cafes;', + 'var coffeeshops;', + 'var coffeeteasupplies;' ], - rules: ['(coffeeroasteries || coffee) || ((coffeeshops || coffeeteasupplies) || cafes);'] + rules: ['(coffeeroasteries || coffee) || ((coffeeshops || coffeeteasupplies) || cafes)'] }, - bank: { - _id: 'qR9s4EtPngjZeEp9u', - description: 'banks', - variables: ['var banks;'], - rules: ['banks;'] - }, - beer: { - _id: 'zrban5i9M6adgwMaK', - description: 'beer', - variables: ['var beergardens;', - 'var beertours;', - 'var sportsbars;', - 'var bars;', - 'var irish_pubs;', - 'var breweries;', - 'var divebars;', - 'var beerbar;', - 'var beergarden;', - 'var pubs;', - 'var beer_and_wine;' - ], - rules: ['(beergardens || beertours) || ((sportsbars || bars) || ((irish_pubs || breweries) || ((divebars || beerbar) || ((pubs || beer_and_wine) || beergarden))));'] + busy: { + _id: 'saxQsfSaBiHHoSEZX', + description: 'user reports to be busy', + variables: ['var busy;'], + rules: ['busy;'] }, train: { _id: '2wH5bFr77ceho5BgF', @@ -260,106 +92,6 @@ let DETECTORS = { variables: ['var publictransport;', 'var trainstations;', 'var trains;'], rules: ['(trainstations || trains) || publictransport;'] }, - forest: { - _id: 'dhQf4PLNAGLy8QDJe', - description: 'forests', - variables: ['var campgrounds;', - 'var parks;', - 'var zoos;', - 'var hiking;', - 'var gardens;' - ], - rules: ['(campgrounds || parks) || ((hiking || gardens) || zoos);'] - }, - dinning_hall: { - _id: 'sSK7rbbC9sHQBN94Y', - description: 'dinninghalls', - variables: ['var diners;', - 'var restaurants;', - 'var cafeteria;', - 'var food_court;' - ], - rules: ['(diners || restaurants || cafeteria || food_court);'] - }, - castle: { - _id: 'gDcxZQ49QrwxzY7Ye', - description: 'castles', - variables: ['var mini_golf;', - 'var buddhist_temples;', - 'var religiousschools;', - 'var synagogues;', - 'var hindu_temples;', - 'var weddingchappels;', - 'var churches;', - 'var mosques;' - ], - rules: ['((mini_golf || ((buddhist_temples || religiousschools) || ((synagogues || hindu_temples) || (weddingchappels || churches)))) || mosques);'] - }, - bar: { - _id: '6urWtr6Tasohdb43u', - description: 'bars', - variables: ['var beergardens;', - 'var beertours;', - 'var champagne_bars;', - 'var cocktailbars;', - 'var sportsbars;', - 'var bars;', - 'var barcrawl;', - 'var pianobars;', - 'var brasseries;', - 'var irish_pubs;', - 'var tikibars;', - 'var nightlife;', - 'var breweries;', - 'var divebars;', - 'var poolhalls;', - 'var island_pub;', - 'var beerbar;', - 'var speakeasies;', - 'var irish;', - 'var pubs;', - 'var beer_and_wine;', - 'var distilleries;', - 'var beergarden;', - 'var clubcrawl;', - 'var gaybars;', - 'var whiskeybars;' - ], - rules: ['((champagne_bars || cocktailbars) || ((barcrawl || pianobars) || ((tikibars || nightlife) || ((poolhalls || island_pub) || ((speakeasies || irish) || ((clubcrawl || pubs) || (gaybars || whiskeybars))))))) || ((beergardens || beertours) || ((sportsbars || bars) || ((brasseries || irish_pubs) || ((breweries || divebars) || ((poolhalls || beerbar) || ((pubs || beer_and_wine) || (distilleries || beergarden)))))));'] - }, - grocery: { - _id: 'N5H9w632dbyhqHEsi', - description: 'grocery shopping', - variables: ['var intlgrocery;', - 'var ethicgrocery;', - 'var markets;', - 'var wholesalers;', - 'var pharmacy;', - 'var grocery;', - 'var farmersmarket;', - 'var convenience;', - 'var importedfood;', - 'var herbsandspices;', - 'var drugstores;', - 'var seafoodmarkets;', - 'var marketstalls;', - 'var organic_stores;', - 'var publicmarkets;' - ], - rules: ['(intlgrocery || ethicgrocery) || ((markets || wholesalers) || ((pharmacy || grocery) || ((farmersmarket || convenience) || ((importedfood || herbsandspices) || ((drugstores || seafoodmarkets) || ((organic_stores || publicmarkets) || marketstalls))))));'] - }, - lake: { - _id: '9iEpW4mb4ysHY5thP', - description: 'lake', - variables: ['var lakes;'], - rules: ['(lakes);'] - }, - rainy: { - _id: 'puLHKiGkLCJWpKc62', - description: 'rainy', - variables: ['var rain;'], - rules: ['(rain);'] - }, sunny: { _id: '6vyrBtdDAyRArMasj', description: 'clear', @@ -372,365 +104,23 @@ let DETECTORS = { variables: ['var clouds;', 'var daytime;'], rules: ['(clouds && daytime);'] }, - restaurant: { - _id: 'tR4e2c7PPjWACwX87', - description: 'eating restaurant', - variables: ['var italian;', - 'var generic_restaurant;', - 'var lunch_places;', - 'var asian_places;', - 'var pastashops;', - 'var pizza;', - 'var spanish;', - 'var newcanadian;', - 'var scottish;', - 'var greek;', - 'var taiwanese;', - 'var hkcafe;', - 'var sandwiches;', - 'var delis;', - 'var dimsum;', - 'var shanghainese;', - 'var dominican;', - 'var burmese;', - 'var indonesian;', - 'var restaurants;', - 'var uzbek;', - 'var cambodian;', - 'var vegan;', - 'var indpak;', - 'var food_court;', - 'var delicatessen;', - 'var cheesesteaks;', - 'var himalayan;', - 'var thai;', - 'var buffets;', - 'var cantonese;', - 'var catering;', - 'var tuscan;', - 'var hotdog;', - 'var salad;', - 'var hungarian;', - 'var persian;', - 'var hotel_bar;', - 'var mediterranean;', - 'var asianfusion;', - 'var malaysian;', - 'var kosher;', - 'var modern_european;', - 'var gluten_free;', - 'var singaporean;', - 'var chinese;', - 'var szechuan;', - 'var panasian;', - 'var steak;', - 'var seafood;', - 'var pakistani;', - 'var vegetarian;', - 'var tapasmallplates;', - 'var african;', - 'var soup;', - 'var halal;', - 'var basque;', - 'var french;', - 'var bangladeshi;', - 'var wraps;', - 'var japacurry;', - 'var cafes;', - 'var hakka;' - ], - rules: ['italian = (pastashops || pizza) || ((sandwiches || delis) || ((italian || restaurants) || ((delicatessen || cheesesteaks) || ((catering || tuscan) || (hotdog || salad)))));', - 'generic_restaurant = (spanish || newcanadian) || ((dimsum || shanghainese) || ((uzbek || cambodian) || ((himalayan || italian) || ((hungarian || persian) || ((kosher || modern_european) || ((steak || seafood) || ((tapasmallplates || african) || ((basque || chinese) || (french || bangladeshi)))))))));', - 'lunch_places = (scottish || greek) || ((dominican || sandwiches) || ((vegan || indpak) || ((thai || delis) || ((hotel_bar || mediterranean) || ((gluten_free || buffets) || ((pakistani || vegetarian) || ((soup || halal) || ((delicatessen || wraps) || ((japacurry || catering) || ((cafes || hakka) || salad))))))))));', - 'asian_places = (taiwanese || hkcafe) || ((burmese || indonesian) || ((dimsum || food_court) || ((buffets || cantonese) || ((asianfusion || malaysian) || ((singaporean || chinese) || (szechuan || panasian))))));', - '(italian || generic_restaurant) || (asian_places || lunch_places);' - ] - }, - exercising: { - _id: '6eY5Z5vrfHcNrefM6', - description: 'exercising', - variables: ['var boxing;', - 'var kickboxing;', - 'var amateursportsteams;', - 'var religiousschools;', - 'var muaythai;', - 'var gyms;', - 'var physicaltherapy;', - 'var fencing;', - 'var tennis;', - 'var healthtrainers;', - 'var poledancingclasses;', - 'var badminton;', - 'var beachvolleyball;', - 'var football;', - 'var bootcamps;', - 'var pilates;', - 'var dancestudio;', - 'var brazilianjiujitsu;', - 'var trampoline;', - 'var cyclingclasses;', - 'var cardioclasses;', - 'var barreclasses;', - 'var intervaltraininggyms;', - 'var sports_clubs;', - 'var weightlosscenters;', - 'var active;', - 'var aerialfitness;', - 'var communitycenters;', - 'var yoga;', - 'var squash;', - 'var surfing;', - 'var circuittraininggyms;', - 'var fitness;', - 'var martialarts;' - ], - rules: ['(((amateursportsteams || religiousschools) || ((physicaltherapy || fencing) || ((beachvolleyball || football) || tennis))) || ((boxing || kickboxing) || ((muaythai || gyms) || ((badminton || healthtrainers) || ((bootcamps || pilates) || ((trampoline || dancestudio) || ((cyclingclasses || cardioclasses) || ((barreclasses || sports_clubs) || ((active || weightlosscenters) || ((yoga || aerialfitness) || ((surfing || fitness) || (martialarts || circuittraininggyms)))))))))))) || ((boxing || kickboxing) || ((muaythai || gyms) || ((healthtrainers || poledancingclasses) || ((bootcamps || pilates) || ((dancestudio || brazilianjiujitsu) || ((cyclingclasses || cardioclasses) || ((barreclasses || intervaltraininggyms) || ((sports_clubs || weightlosscenters) || ((aerialfitness || communitycenters) || ((squash || surfing) || ((fitness || martialarts) || circuittraininggyms)))))))))));'] - }, - eating_japanese: { - _id: "vpP7boQqvLzxhDxjg", - description: "eating a japanese meal", - variables: [ - "var sushi;", - "var japanese;", - "var tonkatsu;", - "var teppanyaki;", - "var tempura;", - "var ramen;", - "var izakaya;", - "var udon;" - ], - rules: ["(sushi || japanese) || ((tonkatsu || teppanyaki) || ((tempura || ramen) || (izakaya || udon)));"] - }, - eating_with_chopsticks: { - _id: "5Ay2Ys9DAH2PcPS4a", - description: "eating with chopsticks", - variables: [ - "var korean;", - "var hawaiian;", - "var japacurry;", - "var sushi;", - "var singaporean;", - "var hakka;", - "var laotian;", - "var cambodian;", - "var japanese;", - "var tonkatsu;", - "var chinese;", - "var taiwanese;", - "var vietnamese;", - "var indonesian;", - "var panasian;", - "var thai;", - "var noodles;", - "var hotpot;", - "var tcm;", - "var cantonese;", - "var asianfusion;", - "var dimsum;", - "var shanghainese;", - "var burmese;", - "var teppanyaki;", - "var tempura;", - "var szechuan;", - "var hkcafe;", - "var ramen;", - "var izakaya;", - "var malaysian;", - "var udon;" - ], - rules: [ - "(((japacurry || sushi) || ((japanese || tonkatsu) || ((noodles || hotpot) || ((asianfusion || korean) || ((teppanyaki || tempura) || ((ramen || izakaya) || (malaysian || udon))))))) || (korean || hawaiian)) || (((singaporean || hakka) || ((chinese || taiwanese) || ((tcm || cantonese) || ((dimsum || shanghainese) || ((szechuan || hkcafe) || burmese))))) || ((laotian || cambodian) || ((vietnamese || indonesian) || (panasian || thai))));" - ] - }, hour0: { _id: "v2ANTJr1I7wle3Ek8", description: "during 00:00", variables: ["var hour;"], rules: ["hour == 0"] - }, - hour1: { - _id: "kDIB1oQOnKktS1j4Z", - description: "during 01:00", - variables: ["var hour;"], - rules: ["hour == 1"] - }, - hour2: { - _id: "ZId1ezjZGAkfbpcWB", - description: "during 02:00", - variables: ["var hour;"], - rules: ["hour == 2"] - }, - hour3: { - _id: "qZRVcySQpf2g6xcfA", - description: "during 03:00", - variables: ["var hour;"], - rules: ["hour == 3"] - }, - hour4: { - _id: "3JSnJAmYQzJFgqJpD", - description: "during 04:00", - variables: ["var hour;"], - rules: ["hour == 4"] - }, - hour5: { - _id: "iosGAkRVqT0zYlHmA", - description: "during 05:00", - variables: ["var hour;"], - rules: ["hour == 5"] - }, - hour6: { - _id: "RxDnq3KRXKQjLHymw", - description: "during 06:00", - variables: ["var hour;"], - rules: ["hour == 6"] - }, - hour7: { - _id: "rnQQ9xRK4LyqPNSnN", - description: "during 07:00", - variables: ["var hour;"], - rules: ["hour == 7"] - }, - hour8: { - _id: "WRaFXtU7Igw6mjpzd", - description: "during 08:00", - variables: ["var hour;"], - rules: ["hour == 8"] - }, - hour9: { - _id: "7IlqQnNFaAoJmDLy6", - description: "during 09:00", - variables: ["var hour;"], - rules: ["hour == 9"] - }, - hour10: { - _id: "K5Y0rpCXcxAdPIkBA", - description: "during 10:00", - variables: ["var hour;"], - rules: ["hour == 10"] - }, - hour11: { - _id: "a5DzoZ3nb6fKQDaRn", - description: "during 11:00", - variables: ["var hour;"], - rules: ["hour == 11"] - }, - hour12: { - _id: "htseIlmY5c7Q9Ihnh", - description: "during 12:00", - variables: ["var hour;"], - rules: ["hour == 12"] - }, - hour13: { - _id: "t5CT9YiIQvsZufVq8", - description: "during 13:00", - variables: ["var hour;"], - rules: ["hour == 13"] - }, - hour14: { - _id: "zepMCtTEOlELnXOM3", - description: "during 14:00", - variables: ["var hour;"], - rules: ["hour == 14"] - }, - hour15: { - _id: "aHwbbglrhLeQqDYK6", - description: "during 15:00", - variables: ["var hour;"], - rules: ["hour == 15"] - }, - hour16: { - _id: "tcftEov84sDlZHx1B", - description: "during 16:00", - variables: ["var hour;"], - rules: ["hour == 16"] - }, - hour17: { - _id: "53puB2TSVHxsHtbZ2", - description: "during 17:00", - variables: ["var hour;"], - rules: ["hour == 17"] - }, - hour18: { - _id: "Jdz8DFUyC37jqROOq", - description: "during 18:00", - variables: ["var hour;"], - rules: ["hour == 18"] - }, - hour19: { - _id: "tV0Jt9xgGkME1MBla", - description: "during 19:00", - variables: ["var hour;"], - rules: ["hour == 19"] - }, - hour20: { - _id: "LtUoOKZMm0ovNnsmX", - description: "during 20:00", - variables: ["var hour;"], - rules: ["hour == 20"] - }, - hour21: { - _id: "X9YChduJTWV9UXVez", - description: "during 21:00", - variables: ["var hour;"], - rules: ["hour == 21"] - }, - hour22: { - _id: "NFNWR5VMUse3B8j0B", - description: "during 22:00", - variables: ["var hour;"], - rules: ["hour == 22"] - }, - hour23: { - _id: "NvegeW31LiB8Zm77M", - description: "during 23:00", - variables: ["var hour;"], - rules: ["hour == 23"] - }, - eating_pizza: { - _id: "D5QSW6S4mNUsxZPq7", - description: "eating pizza", - variables: ["var pizza;"], - rules: ["(pizza);"] - }, - big_bite_restaurant: { - _id : "kJQ8sCFbWddhMviMX", - description : "hand-held meals eaten with big bites", - variables : [ - "var mexican;", - "var foodstands;", - "var cafes;", - "var delicatessen;", - "var driveintheater;", - "var cheesesteaks;", - "var hotdog;", - "var salvadoran;", - "var colombian;", - "var delis;", - "var wraps;", - "var hotdogs;", - "var burgers;", - "var tacos;", - "var tex_mex;", - "var newmexican;", - "var foodtrucks;", - "var bagels;", - "var comfortfood;", - "var sandwiches;", - "var argentine;", - "var bakeries;", - "var cuban;" - ], - rules : [ - "(((cafes || delicatessen) || ((delis || wraps) || ((bagels || comfortfood) || ((sandwiches || argentine) || (bakeries || cuban))))) || ((driveintheater || cheesesteaks) || ((hotdogs || burgers) || hotdog))) || ((mexican || foodstands) || ((salvadoran || colombian) || ((tacos || tex_mex) || (newmexican || foodtrucks))));" - ] } }; export const getDetectorId = (detector) => { let db_detector = Detectors.findOne({description: detector.description}); if (db_detector) { + console.log('getting db detector for', detector.description, 'which is', db_detector._id); + console.log(db_detector) return db_detector._id; } else { + console.log('getting detector for', detector.description, 'which is', detector._id); + return detector._id; } }; @@ -745,839 +135,310 @@ Meteor.methods({ throw new Meteor.Error('getDetectorId.keynotfound', `Detector by the name '${name}' was not found in CONSTANTS.DETECTORS`); } - - console.log('CONSTANTS.DETECTORS: ' + CONSTANTS.DETECTORS[name]._id); - console.log('db.detectors preferably: ' + getDetectorId(CONSTANTS.DETECTORS[name])) - } }); -/** - * Create Storytime Helper. - * - * @param version [number] determines which detector comes first - * @return {{_id: string, name: string, participateTemplate: string, resultsTemplate: string, contributionTypes: *[], description: string, notificationText: string, callbacks: *[]}} - */ -function createStorytime(version) { - // setup places and detectors for storytime - let places = ["niceish_day", "beer", "train", "forest", "dinning_hall", "castle", "field", "gym"]; - let detectorIds = places.map((x) => { return Random.id(); }); - let detectorNames = []; - let dropdownText = [ - 'Swirling Clouds', - 'Drinking butterbeer', - 'Hogwarts Express', - 'Forbidden Forest', - 'Dinner at the Great Hall', - 'Hogwarts Castle', - 'Quidditch Pitch', - 'Training in the Room of Requirement ', - ]; - - _.forEach(places, (place, i) => { - let newVars = JSON.parse(JSON.stringify(DETECTORS[place]['variables'])); - newVars.push(`var participatedInStorytime${version};`); - newVars.push(`var mechanismRich;`); - let newRules = JSON.parse(JSON.stringify(DETECTORS[place]['rules'])); - // modify last detector rule - // when rules has a flat structure where rules.length == 1, last rule is the predicate - // i.e. ['(diners || restaurants || cafeteria || food_court);'] - // when rules have a nested structure where rules.length > 1, last rule is the predicate - // i.e. ['worship_places = (buddhist_temples || churches);', '(worship_places || landmarks);'] - let lastRule = newRules.pop(); - // each rule has a `;` at end, i.e. (rain && park); - // in order to modify the rule, must add predicate preceding the rule - let lastRuleNoSemicolon = lastRule.split(';')[0]; - lastRule = `(mechanismRich && (!participatedInStorytime${version} && (${lastRuleNoSemicolon})));`; - newRules.push(lastRule); - - let detectorName = `${place}_storytime${version}_mechanismRich`; - detectorNames.push(detectorName); +//the CN compiler; takes in author-defined syntax +const convertCNtoCE = function(script) { + + let storyName = script[0] + let storyDescription = script[1] + let storyNotification = script[2] + let generalContext = script[3] + let templates = script[4] + let preStoryInfo = script[5] + let characterRoles = script[6] + let prompts = script[7] + let questions = [] + + //(createPreStoryQuestions) create series of questions based on what information the author specified they wanted preStoryInfo + for (info in preStoryInfo) { + let temp = {}; + temp.question = info.question; + //if the question requires a short answer response + if (info.responseType == "text") { + temp.responseType = "text" + temp.responseData = info.responseData + //if the question has a list of choices to choose from + } else { + temp.responseData = [] + for (choice in info.responseData) { + temp.responseData.push(choice) + } + } + questions.push(temp); + } - DETECTORS[detectorName] = { - '_id': detectorIds[i], - 'description': `${DETECTORS[place].description} storytime${version} mechanismRich`, - 'variables': newVars, - 'rules': newRules - }; - }); + //console.log("questions: " + questions) - // Don't assume the Random detectorIds we created actually exist - detectorIds = detectorNames.map((name) => { return getDetectorId(DETECTORS[name]); }); - let DROPDOWN_OPTIONS = _.zip(dropdownText, detectorIds); - // create story starting point - let sentences = [ - 'Ron looked up at the clouds swirling above him.', - 'Hermoine looked into her goblet, hardly realizing the unusual color of the concoction she was being forced to drink.', - 'Harry prepared himself for a lunge, and then dove forward towards the Platform 9 3/4 wall.', - 'The wizard looked down at their feet, hardly believing the magical plants growing in the Forbidden Forest.', - 'Any young wizard who has their first meal in the Hogwarts Great Hall has to be surprised by the type of food on the menu.', - 'Hogwarts castle had looked so good in photos, but this new wizard looked up at it unimpressed.', - 'Harry Potter saw the snitch diving towards the ground. He aimed his broom towards the grassy ground and followed, reaching his hand out to grab it.', - 'The new wizard of Dumbledore\'s Army was training very hard in the Room of Requirement.' + let values = [ + 'not busy at all', + 'a little busy', + 'somewhat busy', + 'pretty busy', + 'very busy' ]; - let firstSentence = sentences[version]; - let [firstSituation, firstDetector] = DROPDOWN_OPTIONS[version]; - // notify users when story is complete - let sendNotification = function (sub) { - let uids = Submissions.find({iid: sub.iid}).fetch().map(function (x) { - return x.uid; - }); - - notify(uids, sub.iid, 'Our story is finally complete. Click here to read it!', - '', '/apicustomresults/' + sub.iid + '/' + sub.eid); - }; - /** - * NOTE: if callback depends on any variables defined outside of its scope, we must use some solution so that - * the variables values are substituted into the callback.toString() - * - * For a dynamic code generation solution, - * @see https://stackoverflow.com/questions/29182244/convert-a-string-to-a-template-string - * @see https://medium.com/@oprearocks/serializing-object-methods-using-es6-template-strings-and-eval-c77c894651f0 - * @param sub - */ - let storytimeCallback = function (sub) { - Meteor.users.update({ - _id: sub.uid - }, { - $set: { - ['profile.staticAffordances.participatedInStorytime${version}']: true - } - }); + let dropdownText = [ + 'not busy at all', + 'a little busy', + 'somewhat busy', + 'pretty busy', + 'busy' + ]; - // set affordances for storytime - let affordance = sub.content.affordance; + let DROPDOWN_OPTIONS = _.zip(dropdownText); - // HACKY TEMPLATE DYNAMIC CODE GENERATION - let options = eval('${JSON.stringify(DROPDOWN_OPTIONS)}'); + //callback function, occurs once the pre-story contexts have been submitted + const MurderMysteryCallback = function (sub) { + let submissions = Submissions.find({ + iid: sub.iid, + needName: sub.needName + }).fetch(); - let [situation, detectorId] = options.find(function(x) { - return x[1] === affordance; - }); + let contribution = Incidents.findOne({ _id: sub.iid}); - // options = options.filter(function (x) { - // return x[1] !== affordance; - // }); + //console.log("in callback") + //console.log(submissions.length) - // add need if not all pages are done - let needName = 'page' + Random.id(3); - if (cb.numberOfSubmissions() === 7) { - needName = 'pageFinal' - } + //update the UI to fit the one after the callback + experience = Experiences.update({ + "_id": sub.eid + }, { + "$set": { + "participateTemplate": contribution.contributionTypes[0].toPass.template + } + }) - // create and add contribution - let contribution = { - needName: needName, - situation: { - detector: affordance, - number: '1' - }, - toPass: { - instruction: sub.content.sentence, - situation: situation, - previousUserId: sub.uid, - dropdownChoices: { - name: 'affordance', - options: options + let casted = false; + let maxCast = [0, 0]; //todo: make array dynamic, so array size = number of author-defined characters. this is used for enforcing author-defined max # for each character + + //iterate through each submission, casting a character and creating its respective detector for each one + for (let i = 0; i < submissions.length; i++) { + console.log("busyness: " + submissions[i].content.busy) + var key = submissions[i].content.busy; + var affordances = {} + affordances[key] = true; + //console.log("staticAffordances: " + affordances.key) + Meteor.users.update({ + _id: submissions[i].uid + }, { + $set: { + ['profile.staticAffordances.' + key]: true + } - }, - numberNeeded: 1, - notificationDelay: 90 - }; + }); - addContribution(sub.iid, contribution); - }; + //find instance of CN that the submission came from + let instance = Incidents.findOne(submissions[i].iid); + //console.log("detector ID: " + instance.contributionTypes[0].situation.detector) + let detector_id = instance.contributionTypes[0].situation.detector + //console.log("detector rules: " + Detectors.findOne(detector_id).rules) + let rules = Detectors.findOne(detector_id).rules; - // FIXME(rlouie): Can't have more than version 0,1,2 - let exp_names = [ - "A Ron Weasley Story", - "A Hermoine Granger Story", - "A Harry Potter Story" - ]; + let participant = Meteor.users.findOne(submissions[i].uid); - // create and return storytime experience - return { - _id: Random.id(), - name: exp_names[version], - participateTemplate: 'storyPage', - resultsTemplate: 'storybook', - contributionTypes: [{ - needName: 'pageOne', - situation: { - detector: firstDetector, - number: '1' - }, - toPass: { - instruction: firstSentence, - firstSentence: firstSentence, - situation: firstSituation, - dropdownChoices: { - name: 'affordance', - options: DROPDOWN_OPTIONS - } - }, - numberNeeded: 1, - notificationDelay: 90 - }], - description: 'We\'re writing a Harry Potter spin-off story', - notificationText: 'View this and other available experiences', - callbacks: [ - { - trigger: 'cb.newSubmission() && (cb.numberOfSubmissions() <= 7)', - // substitute any variables used outside of the callback function scope - function: eval('`' + storytimeCallback.toString() + '`'), - }, - { - trigger: 'cb.incidentFinished()', - function: sendNotification.toString() - }] - }; -} + let others = Meteor.users.find({ + "profile.pastIncidents": submissions[i].iid + }).fetch() -const createIndependentStorybook = () => { + let other_participants = [] - let place_situation_delay = [ - ["niceish_day",'Swirling Clouds', 5], - ["beer", 'Drinking butterbeer', 90], - ["train", 'Hogwarts Express', 90], - ["forest",'Forbidden Forest', 90], - ["dinning_hall",'Dinner at the Great Hall', 90], - ["castle",'Hogwarts Castle', 90], - ["field",'Quidditch Pitch', 90], - ["gym",'Training in the Room of Requirement', 90] - ]; + //console.log("others length: " + others.length) - return { - _id: Random.id(), - name: 'Humans of Hogwarts', - participateTemplate: 'storyPage_noInterdependence', - resultsTemplate: 'storyBook_noInterdependence', - contributionTypes: addStaticAffordanceToNeeds('mechanismPoor', (function(place_situation_delay) { - return place_situation_delay.map((x) => { - let [place, situation, delay] = x; - return { - needName: situation, - situation: { - detector: getDetectorId(DETECTORS[place]), - number: '1', - }, - toPass: { - situation: situation - }, - numberNeeded: 2, - notificationDelay: delay + //find every other participant in the experience + for (let i = 0; i < others.length; i++) { + let other = others[i] + //console.log("other: " + other.profile.firstName) + //console.log("participant: " + participant.profile.firstName) + //in the future, need to account for when participants have the same first name (use UID instead) + if (other.profile.firstName != participant.profile.firstName) { + other_participants.push(other.profile.firstName) + //console.log("other: " + other.profile.firstName + " " + other_participants.length) } - }); - })(place_situation_delay)), - description: 'We\'re writing a Harry Potter spin-off story', - notificationText: 'View this and other available experiences', - callbacks: [ - { - trigger: 'cb.newSubmission()', - function: (notifyUsersInIncident('Someone added to Humans of Hogwarts', - 'View photos and lines others have created')).toString() - }, - { - trigger: 'cb.incidentFinished()', - function: (notifyUsersInIncident('Humans of Hogwarts has finished', - "View everyone's photos and lines that were contributed")).toString() - }] - }; -}; - -function createBumped() { - let experience = { - name: 'Bumped', - participateTemplate: 'bumped', - resultsTemplate: 'bumpedResults', - contributionTypes: [], - description: 'You just virtually bumped into someone!', - notificationText: 'You just virtually bumped into someone!', - callbacks: [] - }; - - let bumpedCallback = function (sub) { - console.log("calling the bumped callback!!!"); - - let otherSub = Submissions.findOne({ - uid: { - $ne: sub.uid - }, - iid: sub.iid, - needName: sub.needName - }); + } + //console.log("participants: " + participant); + + //check to see how busy the user is + let characterRoles = contribution.contributionTypes[0].toPass.characterRoles; + let character = [] + let cast = false; + //iterate through each character role, and find the role that best fits the current submission or participant, while also respecting max number of characters + for (let k = 0; k < characterRoles.length; k++) { + for (let j = 0; j < characterRoles[k].context.length; j++){ + //console.log("within inner loop " + j + " " + k) + if (participant.profile.staticAffordances[characterRoles[k].context[j]] && maxCast[k] < characterRoles[k].max) { + maxCast[k]++; + console.log("Casting " + participant.profile.firstName + " as a " + characterRoles[k].roleName); + rules = submissions[i].content.busy + " && " + rules; + character.push([rules, characterRoles[k].roleName, contribution.contributionTypes[0].toPass.template, characterRoles[k].instruction, participant._id, other_participants]) + cast = true; + break; + } + else if (maxCast[k] == characterRoles[k].max) { + character.push([rules, characterRoles[1].roleName, contribution.contributionTypes[0].toPass.template, characterRoles[1].instruction, participant._id, other_participants]) + console.log("Casting " + participant.profile.firstName + " as a " + characterRoles[1].roleName); + cast = true; + } + } + if (cast) { + break; + }; + } - notify([sub.uid, otherSub.uid], sub.iid, 'See a photo from who you virtually bumped into!', '', '/apicustomresults/' + sub.iid + '/' + sub.eid); - }; + //console.log("character length" + character.length) - let relationships = ['lovesDTR', 'lovesGarrett', 'lovesMeg', 'lovesMaxine']; - let places = [ - ["bar", "at a bar"], // like Cheers! - ["coffee", "at a coffee shop"], - ["grocery", "at a grocery store"], - ["restaurant", "at a restaurant"], - ["train", "commuting"], - ["exercising", "exercising"] - ]; - _.forEach(relationships, (relationship) => { - _.forEach(places, (place) => { + let extraAffordances = [] - let newVars = JSON.parse(JSON.stringify(DETECTORS[place[0]]['variables'])); - newVars.push('var ' + relationship + ';'); + extraAffordances.push(submissions[i].content.busy) - let newRules = JSON.parse(JSON.stringify(DETECTORS[place[0]]['rules'])); - // modify last detector rule - // when rules has a flat structure where rules.length == 1, last rule is the predicate - // i.e. ['(diners || restaurants || cafeteria || food_court);'] - // when rules have a nested structure where rules.length > 1, last rule is the predicate - // i.e. ['worship_places = (buddhist_temples || churches);', '(worship_places || landmarks);'] - let lastRule = newRules.pop(); - // each rule has a `;` at end, i.e. (rain && park); - // in order to modify the rule, must add relationship predicate preceding the rule - let lastRuleNoSemicolon = lastRule.split(';')[0]; - lastRule = `(${relationship} && (${lastRuleNoSemicolon}));`; - newRules.push(lastRule); + //create the detector for the character + _.forEach(character, (charac) => { - let detector = { - '_id': Random.id(), - 'description': DETECTORS[place[0]].description + relationship, - 'variables': newVars, - 'rules': newRules - }; - DETECTORS[place[0] + relationship] = detector; + let [detectorName, role, template, instruction, user, others] = charac; - for (let i = 0; i < 1; i++) { - let need = { - needName: place[0] + relationship + i, + var need = { + needName: "Murder Mystery" + role, situation: { - detector: detector._id, - number: '2' + detector: detectorName, + number: 2 }, + participateTemplate: template, toPass: { - instruction: 'You are at a ' + place[1] + ' at the same time as ' + role: role, + instruction: instruction, + user: user, + dropdownChoices: { + name: others, + options: others + } }, - numberNeeded: 2, - notificationDelay: 90 - }; - - let callback = { - trigger: 'cb.numberOfSubmissions(\'' + place[0] + relationship + i + '\') === 2', - function: bumpedCallback.toString(), + numberNeeded: 2 }; + //next_experience.contributionTypes.push(need) + addContribution(sub.iid, need); + //console.log("Finished participant number " + instance.contributionTypes.length) + + Meteor.call("sendWhisper", role, user, instruction, (error, response) => { + if (error) { + alert(error.reason); + } else { + //Cookie.set("name", response.name); + //$input.val(""); + } + }) + + let prompts = contribution.contributionTypes[0].toPass.prompts; + + //when casting the murderer, set up the series of hints that will be sent out at various points + if (submissions[i].content.busy == "very busy" && casted == false) { + + casted = true; + + for (let z = 0; z < prompts.length-1; z++) { + if (prompts[z].info != "") { + prompts[z].info = submissions[i].content[prompts[z].info] + "!"; + } + let info = prompts[z].prompt + prompts[z].info; + Meteor.setTimeout(function() {Meteor.call("sendPrompt", info, (error, response) => { + if (error) { + alert(error.reason); + } else { + //Cookie.set("name", response.name); + //$input.val(""); + } + })}, prompts[z].timing * 1000) + } + + //the last prompt is the ending of the story, which will reveal who the murderer is + let ending = prompts[prompts.length-1].prompt + participant.profile.firstName + "!"; + + Meteor.setTimeout(function() {Meteor.call("sendPrompt", ending, (error, response) => { + if (error) { + alert(error.reason); + } else { + //Cookie.set("name", response.name); + //$input.val(""); + } + })}, prompts[prompts.length-1].timing * 1000) + } + }); + } + } - experience.contributionTypes.push(need); - experience.callbacks.push(callback) - } - }) - }); - - return experience; -} - -/** createHalfHalf - * - * @param numberInSituation [Integer] number of people that need to be in the same situation at the same time - * @param notificationDelay [Integer] notificationDelay for all places - * @returns {{name: string, participateTemplate: string, resultsTemplate: string, contributionTypes: Array, description: string, notificationText: string, callbacks: Array}} - */ -const createHalfHalf = function( - { - numberInSituation = 1, - notificationDelay = 90 - } = {} -) { + //create a general experience instance, specify how many people are needed before the story begins let experience = { - name: 'Half Half Bumped', - participateTemplate: 'halfhalfParticipate', - resultsTemplate: 'halfhalfResults', - contributionTypes: [], - description: 'Participate in HalfHalf Travel: Capture your side of the story', - notificationText: 'Participate in HalfHalf Travel: Capture your side of the story', - callbacks: [] - }; - - - let completedCallback = function(sub) { - console.log("Another pair of halves completed a photo"); - - let submissions = Submissions.find({ - iid: sub.iid, - needName: sub.needName - }).fetch(); - - let participants = submissions.map((submission) => { return submission.uid; }); - - notify(participants, sub.iid, - `Two people completed a half half photo`, - `See the results under ${sub.needName}`, - '/apicustomresults/' + sub.iid + '/' + sub.eid); - }; - - let places = [ - ["bar", "at a bar", notificationDelay], - ["coffee", "at a coffee shop", notificationDelay], - ["grocery", "at a grocery store", notificationDelay], - ["restaurant", "at a restaurant", notificationDelay], - ["train", "commuting", notificationDelay], - ["exercising", "exercising", notificationDelay] - ]; - - _.forEach(places, (place) => { - - let [detectorName, situationDescription, delay] = place; - - let need = { - needName: `half half: ${situationDescription}`, - situation: { - detector: DETECTORS[detectorName]._id, - number: numberInSituation - }, - toPass: { - instruction: `Having a good time ${situationDescription}? Try taking one side of a photo.` - }, - numberNeeded: 2, - notificationDelay: delay - }; - - let callback = { - trigger: `cb.numberOfSubmissions("${need.needName}") % 2`, - function: completedCallback.toString(), + name: storyName, + participateTemplate: templates[0], + resultsTemplate: templates[1], + contributionTypes: [ + ], + description: storyDescription, + notificationText: storyNotification, + callbacks: [{ + trigger: 'cb.newSubmission() && (cb.numberOfSubmissions() == 3)', + // substitute any variables used outside of the callback function scope + function: eval('`' + MurderMysteryCallback.toString() + '`'), + }] }; - experience.contributionTypes.push(need); - experience.callbacks.push(callback) - }); - return experience; -}; + const staticAffordances = ['cn']; + const places = [ + [generalContext[0], generalContext[1], storyNotification], + ]; + +//create the initial detector/contribution type for people to submit their pre-story context +staticAffordances.forEach(affordance => { + experience.contributionTypes = [...experience.contributionTypes, ...addStaticAffordanceToNeeds(affordance, ((places) => + places.map(place => { + const [detectorName, situationDescription, instruction] = place; + return { + needName: `${experience.name} ${detectorName}`, + situation: { + detector: getDetectorId(DETECTORS[detectorName]), + number: 3 //to send notifications for the questions template + }, + participateTemplate: templates[0], + toPass: { + prompts: prompts, + characterRoles: characterRoles, + template: templates[1], + situationDescription: `Having a good time ${situationDescription}?`, + instruction: `${instruction}`, + questions: preStoryInfo, + dropdownChoices: { + name: values, + options: DROPDOWN_OPTIONS + } + }, + numberNeeded: 3, //to start the experience chat (CNchat template) + // notificationDelay: 90 uncomment for testing + } + }) + )(places))]; +}); +return experience; +} /** - * - * @param numberInSituation [Number] Controls asynchronous vs synchronous. Defaults to Asynchronous. - * @return {*[]} + * Following is the code that an author has to write to create a CN. + * This is the concise syntax that is then compiled into a working CN. + * */ -const sameSituationContributionTypes = function( - { - numberInSituation = 1 - } = {} -) { - return [{ - needName: 'Warm, Sunny Weather', - situation: { - detector: DETECTORS.sunny._id, - number: numberInSituation - }, - toPass: { - instruction: 'Are you enjoying good weather today? Share a photo of how you are experiencing the sun.' - }, - numberNeeded: 50, - notificationDelay: 1, - allowRepeatContributions: true, - }, { - needName: 'Shopping for groceries', - notificationSubject: 'Inside a grocery store?', - notificationText: 'Participate in an experience where you and others are "Grocery Shopping"', - situation: { - detector: DETECTORS.grocery._id, - number: numberInSituation - }, - toPass: { - instruction: 'Are you shopping for groceries? Share a photo of what you are buying or looking at.' - }, - numberNeeded: 50, - notificationDelay: 90, - allowRepeatContributions: true, - }, { - needName: 'Visiting a Cafe', - situation: { - detector: DETECTORS.coffee._id, - number: numberInSituation - }, - toPass: { - instruction: 'Are you at a cafe? Share a photo of yourself with what you purchased, or what you are doing.' - }, - numberNeeded: 50, - notificationDelay: 90, - allowRepeatContributions: true, - }, { - needName: 'Going out for drinks', - situation: { - detector: DETECTORS.bar._id, - number: numberInSituation - }, - toPass: { - instruction: 'Are you out drinking at the bar? Share a photo of yourself at this bar.', - }, - numberNeeded: 50, - notificationDelay: 90, - allowRepeatContributions: true, - }, { - needName: 'Eating Japanese Food', - situation: { - detector: DETECTORS.eating_japanese._id, - number: numberInSituation - }, - toPass: { - instruction: 'Are you eating Japanese food? Share a photo of yourself dining at this restaurant.' - }, - numberNeeded: 50, - notificationDelay: 90, - allowRepeatContributions: true, - }, { - needName: 'Religious Worship', - situation: { - detector: DETECTORS.castle._id, - number: numberInSituation - }, - toPass: { - instruction: 'Are you at a center for religious worship? Share a photo of something around you.' - }, - numberNeeded: 50, - notificationDelay: 90, - allowRepeatContributions: true, - }, { - needName: 'Enjoy sunset', - situation: { - detector: DETECTORS.sunset._id, - number: numberInSituation - }, - toPass: { - instruction: 'Are you out during sunset? Share a photo of what the sky looks like where you are.' - }, - numberNeeded: 50, - notificationDelay: 1, - allowRepeatContributions: true, - }, { - needName: 'Eating Asian Food', - situation: { - detector: DETECTORS.eating_with_chopsticks._id, - number: numberInSituation - }, - toPass: { - instruction: 'Are you eating at an asian restaurant? Share a photo of yourself dining out right now.' - }, - numberNeeded: 50, - notificationDelay: 90, - allowRepeatContributions: true, - }, { - needName: 'Studying at the library', - situation: { - detector: DETECTORS.library._id, - number: numberInSituation - }, - toPass: { - instruction: 'Are you spending part of the day reading? Share a photo of what you are doing.' - }, - numberNeeded: 50, - notificationDelay: 90, - allowRepeatContributions: true, - }, { - needName: 'Greenery', - situation: { - detector: DETECTORS.forest._id, - number: numberInSituation - }, - toPass: { - instruction: 'Are you spending time at a park? Share a photo of what is going on around you.' - }, - numberNeeded: 50, - notificationDelay: 90, - allowRepeatContributions: true, - }, { - needName: 'Rainy Day', - situation: { - detector: DETECTORS.rainy._id, - number: numberInSituation - }, - toPass: { - instruction: 'Is it raining today? Share a photo of what it looks like outside.' - }, - numberNeeded: 50, - notificationDelay: 1, - allowRepeatContributions: true, - }, { - needName: "Eating some 'Za", - situation: { - detector: DETECTORS.eating_pizza._id, - number: '1' - }, - toPass: { - instruction: 'Are you eating pizza today? Share a photo of yourself at the pizza restaurant.', - }, - numberNeeded: 50, - notificationDelay: 90, - allowRepeatContributions: true, - }, { - needName: "Eating out", - situation: { - detector: DETECTORS.restaurant._id, - number: '1' - }, - toPass: { - instruction: 'Are you eating out today? Share a photo of yourself at the restaurant.', - }, - numberNeeded: 50, - notificationDelay: 90, - allowRepeatContributions: true, - }, { - needName: "Eating Big Bites", - situation: { - detector: DETECTORS.big_bite_restaurant._id, - number: '1' - }, - toPass: { - instruction: 'Are you eating burritos, sandwiches, or burgers today? Share a photo of yourself at the restaurant.', - }, - numberNeeded: 50, - notificationDelay: 90, - allowRepeatContributions: true, - }]; -}; -const halfhalfEmbodiedContributionTypes = function() { - return [{ - needName: 'Hand Silhouette', - situation: { - detector: DETECTORS.sunny._id, - number: '1' - }, - toPass: { - instruction: 'Take a photo, holding your hand towards the sky, covering the sun.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-hands-in-front.jpg' - }, - numberNeeded: 50, - notificationDelay: 1, - }, { - needName: 'I eat with my hands', - situation: { - detector: DETECTORS.grocery._id, - number: '1' - }, - toPass: { - instruction: 'Take a photo, holding a fruit or vegetable outstretched with your hands.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-fruit-in-hand.jpg' - }, - numberNeeded: 50, - notificationDelay: 90, - }, { - needName: 'Coffee Date', - situation: { - detector: DETECTORS.coffee._id, - number: '1' - }, - toPass: { - instruction: 'Are you at a cafe? Take a photo, holding your cup, mug, or plate towards the center of the screen.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-cafe.jpg' - }, - numberNeeded: 50, - notificationDelay: 90, - }, { - needName: 'Raise a glass', - situation: { - detector: DETECTORS.bar._id, - number: '1' - }, - toPass: { - instruction: 'What are you drinking? Take a photo, while raising your glass or bottle in front of you.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-cheers.jpg' - }, - numberNeeded: 50, - notificationDelay: 90, - }, { - needName: 'Itadakimasu (I humbly receive this meal)', - situation: { - detector: DETECTORS.eating_japanese._id, - number: '1' - }, - toPass: { - instruction: 'Take a photo, while holding chopsticks in your hand, saying "Itadakimasu" which translates to "I humbly receive this meal"', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-itadakimasu.jpg' - }, - numberNeeded: 50, - notificationDelay: 90, - }, { - needName: 'Religious Architecture', - situation: { - detector: DETECTORS.castle._id, - number: '1' - }, - toPass: { - instruction: 'Do you notice the details of religious buildings? Do so now, by outstretching your hand and pointing out of the elements that stick out to you most.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-religious-building.jpg' - }, - numberNeeded: 50, - notificationDelay: 90 - }, { - needName: 'Touch a sunset', - situation: { - detector: DETECTORS.sunset._id, - number: '1' - }, - toPass: { - instruction: 'What does the sunset look like where you are? Find a good view of the sunset. Then, take a photo, with your hands outstretched towards the sun.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-sunset-heart.jpg' - }, - numberNeeded: 50, - notificationDelay: 1, - }, { - needName: 'Eating with Chopsticks', - situation: { - detector: DETECTORS.eating_with_chopsticks._id, - number: '1' - }, - toPass: { - instruction: 'What can you pick up using chopsticks? Take a photo of what you are eating, holding your chopsticks.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-holding-chopsticks.jpg' - }, - numberNeeded: 50, - notificationDelay: 90, - }, { - needName: 'reading a book', - situation: { - detector: DETECTORS.library._id, - number: '1' - }, - toPass: { - instruction: 'Sorry to interrupt your reading! Find the nearest book, and take a photo holding up the book to your face.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-book-face.jpg' - }, - numberNeeded: 50, - notificationDelay: 90 - }, { - needName: 'Hold a plant', - situation: { - detector: DETECTORS.forest._id, - number: '1' - }, - toPass: { - instruction: 'Find a plant in the park or garden. Take a photo, with your hand shaped as a half-circle.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-hand-circles-flower.jpg' - }, - numberNeeded: 50, - notificationDelay: 90 - }, { - needName: 'Feet towards the trees', - situation: { - detector: DETECTORS.forest._id, - number: '1' - }, - toPass: { - instruction: 'Find a patch of grass to lay your back on. Then, raise your feet. Take a photo of your foot stretching high into the sky', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-feet-towards-trees.jpg' - }, - numberNeeded: 50, - notificationDelay: 90 - }, { - needName: 'Leaf Mask', - situation: { - detector: DETECTORS.forest._id, - number: '1' - }, - toPass: { - instruction: 'Find a leaf in the park. Take a photo of the leaf covering your face, like it was a mask.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-leaf-face.jpg' - }, - numberNeeded: 50, - notificationDelay: 90 - }, { - needName: 'Puddles', - situation: { - detector: DETECTORS.rainy._id, - number: '1' - }, - toPass: { - instruction: 'Is it raining today? Find a puddle on the ground. Take a photo of yourself, stomping on the puddle!', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-puddle-feet.jpg' - }, - numberNeeded: 50, - notificationDelay: 1, - }, { - needName: "Slice of 'Za", - situation: { - detector: DETECTORS.eating_pizza._id, - number: '1' - }, - toPass: { - instruction: `Did you order pizza? Hold up a slice of 'Za and take a photo of half the slice!`, - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-pizza-slice.jpg' - }, - numberNeeded: 50, - notificationDelay: 90 - }, { - needName: "Do you take cream with that", - situation: { - detector: DETECTORS.coffee._id, // any place that has cups (cafes + bars + restaurants) - number: '1' - }, - toPass: { - instruction: `Do you have a cup or glass you are drinking? Take a photo with it in the middle of the picture. You can even try to pour some extra "cream" into it too!`, - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-teasing-lotion-in-a-cup.jpg' - }, - numberNeeded: 50, - notificationDelay: 90 - }, { - needName: "Eat from the same bowl", // bowl? Plate? (basically all restaurants) - situation: { - detector: DETECTORS.restaurant._id, - number: '1' - }, - toPass: { - instruction: `Are you eating out right now? Take a photo of yourself holding up a bowl or plate to the middle of the screen.`, - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-bowls-up.jpg' - }, - numberNeeded: 50, - notificationDelay: 90 - }, { - needName: "Big Bites", // Any restaurant that would serve something you'd eat with your hands (burrito, tacos, hotdogs, sandwiches, wraps, burgers, tradamerican, newamerican ) - situation: { - detector: DETECTORS.big_bite_restaurant._id, - number: '1' - }, - toPass: { - instruction: `Are you eating food that would require a big bite right now? Take a photo of yourself holding up your food to the middle of the screen.`, - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-big-bite.jpg' - }, - numberNeeded: 50, - }, { - needName: "Friday Night", - situation: { - detector: DETECTORS.big_bite_restaurant._id, - number: '1' - }, - toPass: { - instruction: `Are you eating out right now? Take a photo of yourself holding up a bowl or plate to the middle of the screen.`, - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-bowls-up.jpg' - }, - numberNeeded: 50, - }] -}; -const create24hoursContributionTypes = function(toPassConstructor, numberNeeded) { - let needs = []; - for (i = 0; i < 24; i++) { - let need = { - needName: `hour ${i}`, - situation: { - detector: DETECTORS[`hour${i}`]._id, - number: 1 - }, - toPass: toPassConstructor(i), - numberNeeded: numberNeeded, - notificationDelay: 1 - }; - needs.push(need); - } - return needs; -}; /** + * Following are CE helper functions: + * * Side effect: Changes the global DETECTORS object, adding another detector with key "detectorKey_staticAffordance" * * @param staticAffordance * @param detectorKey * @returns newDetectorKey */ -const addStaticAffordanceToDetector = function(staticAffordance, detectorKey) { + + const addStaticAffordanceToDetector = function(staticAffordance, detectorKey) { let newVars = JSON.parse(JSON.stringify(DETECTORS[detectorKey]['variables'])); newVars.push(`var ${staticAffordance};`); let newRules = JSON.parse(JSON.stringify(DETECTORS[detectorKey]['rules'])); @@ -1603,6 +464,7 @@ const addStaticAffordanceToDetector = function(staticAffordance, detectorKey) { 'rules': newRules }; } + console.log( DETECTORS[newDetectorKey].description, DETECTORS[newDetectorKey]._id); return newDetectorKey; }; @@ -1613,7 +475,7 @@ const addStaticAffordanceToDetector = function(staticAffordance, detectorKey) { * @param contributionTypes [Array] list of all the needs by which to modify * @return */ -const addStaticAffordanceToNeeds = function(staticAffordance, contributionTypes) { + const addStaticAffordanceToNeeds = function(staticAffordance, contributionTypes) { return _.map(contributionTypes, (need) => { let detectorKey; _.forEach(_.keys(DETECTORS), (key) => { @@ -1622,21 +484,22 @@ const addStaticAffordanceToNeeds = function(staticAffordance, contributionTypes) } }); // WILL THROW ERROR if we don't find the matching detector id + let newDetectorKey = addStaticAffordanceToDetector(staticAffordance, detectorKey); - need.situation.detector = getDetectorId(DETECTORS[newDetectorKey]); + need.situation.detector = DETECTORS[newDetectorKey]._id; + console.log('adding to need', newDetectorKey, DETECTORS[newDetectorKey]._id); return need; }); }; /** - * * @param contributionTypes * @param triggerTemplate [String] should be written as a string, with ES6 templating syntax * i.e. "cb.newSubmission(\"${need.needName}\")" * If using templating syntax, you have access to the each individual need object * @param sendNotificationFn */ -const notifCbForMultiNeeds = function(contributionTypes, triggerTemplate, sendNotificationFn) { + const notifCbForMultiNeeds = function(contributionTypes, triggerTemplate, sendNotificationFn) { return contributionTypes.map((need) => { return { trigger: eval('`' + triggerTemplate + '`'), @@ -1645,1298 +508,8 @@ const notifCbForMultiNeeds = function(contributionTypes, triggerTemplate, sendNo }); }; -/** halfhalfRespawnAndNotify: - * This is a helper function that generates a callback function definition - * The callback will respawn or create a duplicate of the need that just completed, - * while also sending notifications to the participants of that need. - * - * This function makes strong assumptions about how your OCE contributionTypes are written. - * i.e. need.needName = 'Name of my need 1' - * i.e. need.needName = 'Hand Silhouette 1' - * - * @param subject [String] subject of notification - * @param text [String] accompanying subtext of notification - * @return {any} A function - */ -const halfhalfRespawnAndNotify = function(subject, text) { - functionTemplate = function (sub) { - let contributionTypes = Incidents.findOne(sub.iid).contributionTypes; - let need = contributionTypes.find((x) => { - return x.needName === sub.needName; - }); - - // Convert Need Name i to Need Name i+1 - let splitName = sub.needName.split(' '); - let iPlus1 = Number(splitName.pop()) + 1; - splitName.push(iPlus1); - let newNeedName = splitName.join(' '); - - need.needName = newNeedName; - addContribution(sub.iid, need); - - let participants = Submissions.find({ - iid: sub.iid, - needName: sub.needName - }).map((submission) => { - return submission.uid; - }); - - notify(participants, sub.iid, '${subject}', '${text}', '/apicustomresults/' + sub.iid + '/' + sub.eid); - }; - return eval('`'+functionTemplate.toString()+'`'); -}; - -const sendNotificationNew24HourPhotoAlbumSub = function(sub) { - let uids = Submissions.find({ iid: sub.iid }).fetch().map(function (x) { - return x.uid; - }); - - notify(uids, sub.iid, 'Someone added to the 24 hour photo album. Click here to see progress on the album.', '', '/apicustomresults/' + sub.iid + '/' + sub.eid); -}; - -let sendNotificationScavenger = function (sub) { - let uids = Submissions.find({ iid: sub.iid }).fetch().map(function (x) { - return x.uid; - }); - - notify(uids, sub.iid, 'Wooh! All the scavenger hunt items were found. Click here to see all of them.', '', '/apicustomresults/' + sub.iid + '/' + sub.eid); -}; - -let sendNotificationSunset = function (sub) { - let uids = Submissions.find({ iid: sub.iid }).fetch().map(function (x) { - return x.uid; - }); - - notify(uids, sub.iid, 'Our sunset timelapse is complete! Click here to see it.', '', '/apicustomresults/' + sub.iid + '/' + sub.eid); -}; - -let sendNotificationFoodFight = function (sub) { - let uids = Submissions.find({ iid: sub.iid }).fetch().map(function (x) { - return x.uid; - }); - notify(uids, sub.iid, 'Wooh! Both participants have attacked each other with food pics', '', '/apicustomresults/' + sub.iid + '/' + sub.eid); -}; - -const sendNotificationTwoHalvesCompleted = function(sub) { - console.log("Another pair of halves completed a photo"); - - let submissions = Submissions.find({ - iid: sub.iid, - needName: sub.needName - }).fetch(); - - let participants = submissions.map((submission) => { return submission.uid; }); - - notify(participants, sub.iid, - `Two people completed a half half photo`, - `See the results under ${sub.needName}`, - '/apicustomresults/' + sub.iid + '/' + sub.eid); -}; - let EXPERIENCES = { - // bumped: createBumped(), - storyTime: createStorytime(0), - storyTime1: createStorytime(1), - storyTime2: createStorytime(2), - // storyTime3: createStorytime(3), - // storyTime4: createStorytime(4), - // storyTime5: createStorytime(5), - // storyTime6: createStorytime(6), - // storyTime7: createStorytime(7), - // independentStorybook: createIndependentStorybook(), - // sunset: { - // _id: Random.id(), - // name: 'Sunset', - // participateTemplate: 'uploadPhoto', - // resultsTemplate: 'sunset', - // contributionTypes: [{ - // needName: 'sunset', - // situation: { - // detector: DETECTORS.sunset._id, - // number: '1' - // }, - // toPass: { - // instruction: 'Take a photo of the sunset!' - // }, - // numberNeeded: 20, - // notificationDelay: 1, - // }], - // description: 'Create a timelapse of the sunset with others around the country', - // notificationText: 'Take a photo of the sunset!', - // callbacks: [{ - // trigger: 'cb.incidentFinished()', - // function: sendNotificationSunset.toString() - // }] - // }, - // halfhalf24: { - // _id: Random.id(), - // name: 'Half Half over 24 hours', - // participateTemplate: 'halfhalfParticipate', - // resultsTemplate: 'halfhalfResults', // FIXME(rlouie): should be a template grouped by time - // contributionTypes: create24hoursContributionTypes( - // function(i) { - // let zpad_i = ("00" + i).slice(-2); - // let toPass = { - // instruction: `This experience is for testing the Half Half Photo Experience! Take a picture of what you are doing today at hour ${zpad_i}:00 today.` - // }; - // return toPass; - // }, - // 10 - // ), - // description: 'Create a photo collage of what you and others are doing at each of the hours in a day', - // notificationText: 'Take a photo of what you are doing at this hour', - // callbacks: [{ - // trigger: 'cb.newSubmission()', - // function: sendNotificationNew24HourPhotoAlbumSub.toString() - // }] - // }, - // halfhalfAsynch: createHalfHalf(), - // halfhalfSynch: createHalfHalf({numberInSituation: 2}), - // halfhalfDay: { - // _id: Random.id(), - // name: 'Half Half Daytime', - // participateTemplate: 'halfhalfParticipate', - // resultsTemplate: 'halfhalfResults', - // contributionTypes: [{ - // needName: 'half half: daytime', // FIXME: make more semantically meaningful - // situation: { - // detector: DETECTORS.daytime._id, // For testing during workday - // number: '1' - // }, - // toPass: { - // instruction: 'Take a photo of like Half Half Travel!' - // }, - // numberNeeded: 50, // arbitrarily high for a study - // notificationDelay: 1, - // }], - // description: 'Create adventures that meet halfway! Ready to live in a parallel with someone else?', - // notificationText: 'Participate in Half Half Travel!', - // callbacks: [{ - // trigger: 'cb.numberOfSubmissions("half half: daytime") % 2 === 0', - // function: sendNotificationTwoHalvesCompleted.toString() - // }] - // }, - // halfhalfNight: { - // _id: Random.id(), - // name: 'Half Half Nighttime', - // participateTemplate: 'halfhalfParticipate', - // resultsTemplate: 'halfhalfResults', - // contributionTypes: [{ - // needName: 'half half: nighttime', // FIXME: make more semantically meaningful - // situation: { - // detector: DETECTORS.night._id, // For testing during evening - // number: '1' - // }, - // toPass: { - // instruction: 'Take a photo of like Half Half Travel!' - // }, - // numberNeeded: 50, // arbitrarily high for a study - // notificationDelay: 1, // no need to delay if its daytime outside - // }], - // description: 'Create adventures that meet halfway! Ready to live in a parallel with someone else?', - // notificationText: 'Participate in Half Half Travel!', - // callbacks: [{ - // trigger: 'cb.numberOfSubmissions("half half: nighttime") % 2 === 0', - // function: sendNotificationTwoHalvesCompleted.toString() - // }] - // }, - halfhalf_sunny: { - _id: Random.id(), - name: 'Hand Silhouette', - participateTemplate: 'halfhalfParticipate', - resultsTemplate: 'halfhalfResults', - contributionTypes: addStaticAffordanceToNeeds('mechanismRich', [{ - // needName MUST have structure "My Need Name XYZ" - needName: 'Hand Silhouette 1', - situation: { - detector: getDetectorId(DETECTORS.sunny), - number: '1' - }, - toPass: { - instruction: 'Is the weather clear and sunny where you are? Take a photo, holding your hand towards the sky, covering the sun.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-hands-in-front.jpg' - }, - numberNeeded: 2, - notificationDelay: 1, - }]), - description: 'Use the sun to make a silhouette of your hand', - notificationText: 'View this and other available experiences', - callbacks: [{ - trigger: '(cb.numberOfSubmissions() % 2) === 0', - function: halfhalfRespawnAndNotify('A hand silhouette was completed','View the photo').toString() - }] - }, - halfhalf_grocery: { - _id: Random.id(), - name: 'Grocery Buddies', - participateTemplate: 'halfhalfParticipate', - resultsTemplate: 'halfhalfResults', - contributionTypes: addStaticAffordanceToNeeds('mechanismRich', [{ - // needName MUST have structure "My Need Name XYZ" - needName: 'Grocery Buddies 1', - notificationSubject: 'Inside a grocery store?', - notificationText: 'Share an experience with others who are also grocery shopping', - situation: { - detector: getDetectorId(DETECTORS.grocery), - number: '1' - }, - toPass: { - instruction: 'Are you at the grocery store? Take a photo, holding a fruit or vegetable outstretched with your hands.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-fruit-in-hand.jpg' - }, - numberNeeded: 2, - notificationDelay: 90, - }]), - description: 'While shopping for groceries, create a half half photo.', - notificationText: 'View this and other available experiences', - callbacks: [{ - trigger: '(cb.numberOfSubmissions() % 2) === 0', - function: halfhalfRespawnAndNotify('A Grocery Buddies photo completed','View the photo').toString() - }] - }, - halfhalf_coffee: { - _id: Random.id(), - name: 'Coffee Date', - participateTemplate: 'halfhalfParticipate', - resultsTemplate: 'halfhalfResults', - contributionTypes: addStaticAffordanceToNeeds('mechanismRich', [{ - // needName MUST have structure "My Need Name XYZ" - needName: 'Coffee Date 1', - notificationSubject: 'Inside a coffee shop?', - notificationText: 'Share an experience with others who are also at a coffee shop', - situation: { - detector: getDetectorId(DETECTORS.coffee), - number: '1' - }, - toPass: { - instruction: 'Are you at a cafe? Take a photo, holding your cup, mug, or plate towards the center of the screen.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-cafe.jpg' - }, - numberNeeded: 2, - notificationDelay: 90, - }]), - description: 'While enjoying a cafe beverage, create a half half photo.', - notificationText: 'View this and other available experiences', - callbacks: [{ - trigger: '(cb.numberOfSubmissions() % 2) === 0', - function: halfhalfRespawnAndNotify('A Coffee Date photo completed','View the photo').toString() - }] - }, - halfhalf_bar: { - _id: Random.id(), - name: 'Cheers', - participateTemplate: 'halfhalfParticipate', - resultsTemplate: 'halfhalfResults', - contributionTypes: addStaticAffordanceToNeeds('mechanismRich', [{ - // needName MUST have structure "My Need Name XYZ" - needName: 'Cheers 1', - notificationSubject: 'Drinking at a bar?', - notificationText: 'Share an experience with others who are also drinking at a bar', - situation: { - detector: getDetectorId(DETECTORS.bar), - number: '1' - }, - toPass: { - instruction: 'What are you drinking at the bar? Take a photo, while raising your glass or bottle in front of you.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-cheers.jpg' - }, - numberNeeded: 2, - notificationDelay: 90 - }]), - description: 'While enjoying your drink, create a half half photo.', - notificationText: 'View this and other available experiences', - callbacks: [{ - trigger: '(cb.numberOfSubmissions() % 2) === 0', - function: halfhalfRespawnAndNotify('A Cheers photo completed','View the photo').toString() - }] - }, - // halfhalf_japanese: { - // _id: Random.id(), - // name: 'Itadakimasu', - // participateTemplate: 'halfhalfParticipate', - // resultsTemplate: 'halfhalfResults', - // contributionTypes: addStaticAffordanceToNeeds('mechanismRich', [{ - // // needName MUST have structure "My Need Name XYZ" - // needName: 'Itadakimasu 1', - // situation: { - // detector: getDetectorId(DETECTORS.eating_japanese), - // number: '1' - // }, - // toPass: { - // instruction: 'Take a photo, while holding chopsticks in your hand, saying "Itadakimasu" which translates to "I humbly receive this meal"', - // exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-itadakimasu.jpg' - // }, - // numberNeeded: 2, - // notificationDelay: 90, - // }]), - // description: 'While eating Japanese Food, create a half half photo.', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: '(cb.numberOfSubmissions() % 2) === 0', - // function: halfhalfRespawnAndNotify('A Itadakimasu photo completed','View the photo').toString() - // }] - // }, - halfhalf_religious: { - _id: Random.id(), - name: 'Religious Architecture', - participateTemplate: 'halfhalfParticipate', - resultsTemplate: 'halfhalfResults', - contributionTypes: addStaticAffordanceToNeeds('mechanismRich', [{ - // needName MUST have structure "My Need Name XYZ" - needName: 'Religious Architecture 1', - notificationSubject: 'Visiting a place of worship?', - notificationText: 'Share an experience with others who are also visiting a place of worship', - situation: { - detector: getDetectorId(DETECTORS.castle), - number: '1' - }, - toPass: { - instruction: 'Do you notice the details of the religious building near you? Do so now, by outstretching your hand and pointing out of the elements that stick out to you most.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-religious-building.jpg' - }, - numberNeeded: 2, - notificationDelay: 90, - }]), - description: 'While visiting a place of worship, create a half half photo.', - notificationText: 'View this and other available experiences', - callbacks: [{ - trigger: '(cb.numberOfSubmissions() % 2) === 0', - function: halfhalfRespawnAndNotify('A Religious Architecture photo completed','View the photo').toString() - }] - }, - halfhalf_sunset: { - _id: Random.id(), - name: 'Sunset Together', - participateTemplate: 'halfhalfParticipate', - resultsTemplate: 'halfhalfResults', - contributionTypes: addStaticAffordanceToNeeds('mechanismRich', [{ - // needName MUST have structure "My Need Name XYZ" - needName: 'Sunset Together 1', - notificationSubject: 'Can you see the sunset?', - notificationText: 'Share an experience with others who are also watching the sunset', - situation: { - detector: getDetectorId(DETECTORS.sunset), - number: '1' - }, - toPass: { - instruction: 'What does the sunset look like where you are? Find a good view; then, take a photo, with your hands outstretched towards the sun.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-sunset-heart.jpg' - }, - numberNeeded: 2, - notificationDelay: 1, - }]), - description: 'While looking up at the sunset, create a half half photo.', - notificationText: 'View this and other available experiences', - callbacks: [{ - trigger: '(cb.numberOfSubmissions() % 2) === 0', - function: halfhalfRespawnAndNotify('A Sunset Together photo completed','View the photo').toString() - }] - }, - halfhalf_asian: { - _id: Random.id(), - name: 'Eating with Chopsticks', - participateTemplate: 'halfhalfParticipate', - resultsTemplate: 'halfhalfResults', - contributionTypes: addStaticAffordanceToNeeds('mechanismRich', [{ - // needName MUST have structure "My Need Name XYZ" - needName: 'Eating with Chopsticks 1', - notificationSubject: 'Eating at an asian restaurant?', - notificationText: 'Share an experience with others who are also eating asian food', - situation: { - detector: getDetectorId(DETECTORS.eating_with_chopsticks), - number: '1' - }, - toPass: { - instruction: 'Are you eating asian food right now? Take a photo of what you are eating, holding your chopsticks.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-holding-chopsticks.jpg' - }, - numberNeeded: 2, - notificationDelay: 90 - }]), - description: 'While eating asian food, create a half half photo.', - notificationText: 'View this and other available experiences', - callbacks: [{ - trigger: '(cb.numberOfSubmissions() % 2) === 0', - function: halfhalfRespawnAndNotify('An Eating with Chopsticks photo completed','View the photo').toString() - }] - }, - halfhalf_books: { - _id: Random.id(), - name: 'Book Buddies', - participateTemplate: 'halfhalfParticipate', - resultsTemplate: 'halfhalfResults', - contributionTypes: addStaticAffordanceToNeeds('mechanismRich', [{ - // needName MUST have structure "My Need Name XYZ" - needName: 'Book Buddies 1', - notificationSubject: 'Are you at a library?', - notificationText: 'Share an experience with others who are also at the library', - situation: { - detector: getDetectorId(DETECTORS.library), - number: '1' - }, - toPass: { - instruction: 'Sorry to interrupt your reading! Find the nearest book, and take a photo holding up the book to your face.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-book-face.jpg' - }, - numberNeeded: 2, - notificationDelay: 90, - }]), - description: 'While reading a book, create a half half photo.', - notificationText: 'View this and other available experiences', - callbacks: [{ - trigger: '(cb.numberOfSubmissions() % 2) === 0', - function: halfhalfRespawnAndNotify('A Book Buddies photo completed','View the photo').toString() - }] - }, - // halfhalf_plantcircle: { - // _id: Random.id(), - // name: 'Hold a plant', - // participateTemplate: 'halfhalfParticipate', - // resultsTemplate: 'halfhalfResults', - // contributionTypes: addStaticAffordanceToNeeds('mechanismRich', [{ - // // needName MUST have structure "My Need Name XYZ" - // needName: 'Hold a plant 1', - // situation: { - // detector: getDetectorId(DETECTORS.forest), - // number: '1' - // }, - // toPass: { - // instruction: 'Find a plant in the park or garden. Take a photo, with your hand shaped as a half-circle.', - // exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-hand-circles-flower.jpg' - // }, - // numberNeeded: 2, - // notificationDelay: 90, - // }]), - // description: 'While in the park, create a half half photo.', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: '(cb.numberOfSubmissions() % 2) === 0', - // function: halfhalfRespawnAndNotify('A Plant Circle photo completed','View the photo').toString() - // }] - // }, - // halfhalf_feettotrees: { - // _id: Random.id(), - // name: 'Feet to the trees', - // participateTemplate: 'halfhalfParticipate', - // resultsTemplate: 'halfhalfResults', - // contributionTypes: addStaticAffordanceToNeeds('mechanismRich', [{ - // // needName MUST have structure "My Need Name XYZ" - // needName: 'Feet to the trees 1', - // situation: { - // detector: getDetectorId(DETECTORS.forest), - // number: '1' - // }, - // toPass: { - // instruction: 'Find a patch of grass to lay your back on. Then, raise your feet. Take a photo of your foot stretching high into the sky', - // exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-feet-towards-trees.jpg' - // }, - // numberNeeded: 2, - // notificationDelay: 5, - // }]), - // description: 'While in the park, create a half half photo.', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: '(cb.numberOfSubmissions() % 2) === 0', - // function: halfhalfRespawnAndNotify('A Feet to the trees photo completed','View the photo').toString() - // }] - // }, - halfhalf_leakmask: { - _id: Random.id(), - name: 'Leaf Mask', - participateTemplate: 'halfhalfParticipate', - resultsTemplate: 'halfhalfResults', - contributionTypes: addStaticAffordanceToNeeds('mechanismRich', [{ - // needName MUST have structure "My Need Name XYZ" - needName: 'Leaf Mask 1', - notificationSubject: 'Are you at a park?', - notificationText: 'Share an experience with others who are also at a park', - situation: { - detector: getDetectorId(DETECTORS.forest), - number: '1' - }, - toPass: { - instruction: 'Find a leaf in the park. Take a photo of the leaf covering your face, like it was a mask.', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-leaf-face.jpg' - }, - numberNeeded: 2, - notificationDelay: 90 - }]), - description: 'While in the park, create a half half photo.', - notificationText: 'View this and other available experiences', - callbacks: [{ - trigger: '(cb.numberOfSubmissions() % 2) === 0', - function: halfhalfRespawnAndNotify('A Feet to the trees photo completed','View the photo').toString() - }] - }, - halfhalf_puddles: { - _id: Random.id(), - name: 'Puddle Feet', - participateTemplate: 'halfhalfParticipate', - resultsTemplate: 'halfhalfResults', - contributionTypes: addStaticAffordanceToNeeds('mechanismRich', [{ - // needName MUST have structure "My Need Name XYZ" - needName: 'Puddle Feet 1', - notificationSubject: 'Are you outside while its raining?', - notificationText: 'Share an experience with others who are enjoying or enduring the rain', - situation: { - detector: getDetectorId(DETECTORS.rainy), - number: '1' - }, - toPass: { - instruction: 'Is it raining today? Find a puddle on the ground. Take a photo of yourself, stomping on the puddle!', - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-puddle-feet.jpg' - }, - numberNeeded: 2, - notificationDelay: 1, - }]), - description: 'With the puddles on a rainy day, create a half half photo.', - notificationText: 'View this and other available experiences', - callbacks: [{ - trigger: '(cb.numberOfSubmissions() % 2) === 0', - function: halfhalfRespawnAndNotify('A "Puddle Feet" photo completed','View the photo').toString() - }] - }, - // halfhalf_pizza: { - // _id: Random.id(), - // name: "Slice of 'Za", - // participateTemplate: 'halfhalfParticipate', - // resultsTemplate: 'halfhalfResults', - // contributionTypes: addStaticAffordanceToNeeds('mechanismRich', [{ - // // needName MUST have structure "My Need Name XYZ" - // needName: "Slice of 'Za 1", - // situation: { - // detector: getDetectorId(DETECTORS.eating_pizza), - // number: '1' - // }, - // toPass: { - // instruction: `Did you order pizza? Hold up a slice of 'Za and take a photo of half the slice!`, - // exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-pizza-slice.jpg' - // }, - // numberNeeded: 2, - // notificationDelay: 90, - // }]), - // description: 'While eating pizza, create a half half photo.', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: '(cb.numberOfSubmissions() % 2) === 0', - // function: halfhalfRespawnAndNotify("A \"Slice of 'Za\" photo completed",'View the photo').toString() - // }] - // }, - // halfhalf_creamwiththat: { - // _id: Random.id(), - // name: "Want cream with that", - // participateTemplate: 'halfhalfParticipate', - // resultsTemplate: 'halfhalfResults', - // contributionTypes: addStaticAffordanceToNeeds('mechanismRich', [{ - // // needName MUST have structure "My Need Name XYZ" - // needName: "Want cream with that 1", - // situation: { - // detector: getDetectorId(DETECTORS.coffee), // any place that has cups (cafes + bars + restaurants) - // number: '1' - // }, - // toPass: { - // instruction: `Do you have a cup or glass you are drinking? Take a photo with it in the middle of the picture. You can even try to pour some extra "cream" into it too!`, - // exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-teasing-lotion-in-a-cup.jpg' - // }, - // numberNeeded: 2, - // notificationDelay: 60, - // }]), - // description: 'While drinking coffee at a cafe, create a half half photo.', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: '(cb.numberOfSubmissions() % 2) === 0', - // function: halfhalfRespawnAndNotify("A 'Want cream with that' photo completed",'View the photo').toString() - // }] - // }, - // halfhalf_eatfromsamebowl: { - // _id: Random.id(), - // name: "Share a plate", - // participateTemplate: 'halfhalfParticipate', - // resultsTemplate: 'halfhalfResults', - // contributionTypes: addStaticAffordanceToNeeds('mechanismRich', [{ - // // needName MUST have structure "My Need Name XYZ" - // needName: "Share a plate 1", // bowl? Plate? (basically all restaurants) - // situation: { - // detector: getDetectorId(DETECTORS.restaurant), - // number: '1' - // }, - // toPass: { - // instruction: `Are you eating out right now? Take a photo of yourself holding up a bowl or plate to the middle of the screen.`, - // exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-bowls-up.jpg' - // }, - // numberNeeded: 2, - // notificationDelay: 60, - // }]), - // description: 'While eating out at a restaurant, create a half half photo.', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: '(cb.numberOfSubmissions() % 2) === 0', - // function: halfhalfRespawnAndNotify("A 'Share a Plate' photo completed",'View the photo').toString() - // }] - // }, - halfhalf_bigbites: { - _id: Random.id(), - name: "Big Bites", - participateTemplate: 'halfhalfParticipate', - resultsTemplate: 'halfhalfResults', - contributionTypes: addStaticAffordanceToNeeds('mechanismRich', [{ - needName: "Big Bites 1", // Any restaurant that would serve something you'd eat with your hands (burrito, tacos, hotdogs, sandwiches, wraps, burgers, tradamerican, newamerican ) - notificationSubject: 'Eating at a restaurant?', - notificationText: 'Share an experience with others who are enjoying big bites of their meal', - situation: { - detector: getDetectorId(DETECTORS.big_bite_restaurant), - number: '1' - }, - toPass: { - instruction: `Are you eating food that would require a big bite right now? Take a photo of yourself holding up your food to the middle of the screen.`, - exampleImage: 'https://s3.us-east-2.amazonaws.com/ce-platform/oce-example-images/half-half-embodied-mimicry-big-bite.jpg' - }, - numberNeeded: 2, - notificationDelay: 90, // https://www.quora.com/Whats-the-average-time-that-customers-wait-between-entering-a-restaurant-and-getting-served - }]), - description: 'While eating some non-trivially sized food, create a half half photo.', - notificationText: 'View this and other available experiences', - callbacks: [{ - trigger: '(cb.numberOfSubmissions() % 2) === 0', - function: halfhalfRespawnAndNotify("A 'Big Bites' photo completed",'View the photo').toString() - }] - }, - // halfhalfEmbodiedMimicry: { - // _id: Random.id(), - // name: 'Body Mirror', - // participateTemplate: 'halfhalfParticipate', - // resultsTemplate: 'halfhalfResults', - // contributionTypes: halfhalfEmbodiedContributionTypes(), - // description: 'With your environment as the shared canvas, pose your body to be the mirror image of a friend', - // notificationText: 'Your situation made you available to participate in Body Mirror!', - // callbacks: notifCbForMultiNeeds( - // halfhalfEmbodiedContributionTypes(), - // "cb.numberOfSubmissions(\"${need.needName}\") % 2 === 0", - // sendNotificationTwoHalvesCompleted) - // }, - // situationaware_sunny: { - // _id: Random.id(), - // name: 'Sunny Days', - // participateTemplate: 'uploadPhoto', - // resultsTemplate: 'photosByCategories', - // contributionTypes: addStaticAffordanceToNeeds('mechanismPoor', [{ - // needName: 'Sunny Days', - // situation: { - // detector: getDetectorId(DETECTORS.sunny), - // number: '1' - // }, - // toPass: { - // instruction: 'Are you enjoying good weather today? Share a photo of how you are experiencing the sun.' - // }, - // numberNeeded: 50, - // notificationDelay: 1, - // allowRepeatContributions: true, - // }]), - // description: 'Appreciate the small moments with others who are doing the same', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: 'cb.newSubmission()', - // function: notifyUsersInNeed('New moment for Sunny Days', 'View the photo').toString() - // }] - // }, - // situationaware_grocery: { - // _id: Random.id(), - // name: 'Feed yourself', - // participateTemplate: 'uploadPhoto', - // resultsTemplate: 'photosByCategories', - // contributionTypes: addStaticAffordanceToNeeds('mechanismPoor', [{ - // needName: 'Feed yourself', - // situation: { - // detector: getDetectorId(DETECTORS.grocery), - // number: 1 - // }, - // toPass: { - // instruction: 'Are you shopping for groceries? Share a photo of what you are buying or looking at.' - // }, - // numberNeeded: 50, - // notificationDelay: 90, - // allowRepeatContributions: true, - // }]), - // description: 'Appreciate the small moments with others who are doing the same', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: 'cb.newSubmission()', - // function: notifyUsersInNeed('New moment for Feed yourself', 'View the photo').toString() - // }] - // }, - // situationaware_cafe: { - // _id: Random.id(), - // name: 'Cafe Days', - // participateTemplate: 'uploadPhoto', - // resultsTemplate: 'photosByCategories', - // contributionTypes: addStaticAffordanceToNeeds('mechanismPoor', [{ - // needName: 'Cafe Days', - // situation: { - // detector: getDetectorId(DETECTORS.coffee), - // number: 1 - // }, - // toPass: { - // instruction: 'Are you at a cafe? Share a photo of yourself with what you purchased, or what you are doing.' - // }, - // numberNeeded: 50, - // notificationDelay: 90, - // allowRepeatContributions: true, - // }]), - // description: 'Appreciate the small moments with others who are doing the same', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: 'cb.newSubmission()', - // function: notifyUsersInNeed('New moment for Cafe Days', 'View the photo').toString() - // }] - // }, - // situationaware_bar: { - // _id: Random.id(), - // name: 'Hit the Bars', - // participateTemplate: 'uploadPhoto', - // resultsTemplate: 'photosByCategories', - // contributionTypes: addStaticAffordanceToNeeds('mechanismPoor', [{ - // needName: 'Hit the Bars', - // situation: { - // detector: getDetectorId(DETECTORS.bar), - // number: 1 - // }, - // toPass: { - // instruction: 'Are you out drinking at the bar? Share a photo of yourself at this bar.', - // }, - // numberNeeded: 50, - // notificationDelay: 90, - // allowRepeatContributions: true, - // }]), - // description: 'Appreciate the small moments with others who are doing the same', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: 'cb.newSubmission()', - // function: notifyUsersInNeed('New moment for Hit the Bars', 'View the photo').toString() - // }] - // }, - // situationaware_japanese: { - // _id: Random.id(), - // name: 'Eating Japanese Food', - // participateTemplate: 'uploadPhoto', - // resultsTemplate: 'photosByCategories', - // contributionTypes: addStaticAffordanceToNeeds('mechanismPoor', [{ - // needName: 'Eating Japanese Food', - // situation: { - // detector: getDetectorId(DETECTORS.eating_japanese), - // number: 1 - // }, - // toPass: { - // instruction: 'Are you eating Japanese food? Share a photo of yourself dining at this restaurant.' - // }, - // numberNeeded: 50, - // notificationDelay: 90, - // allowRepeatContributions: true, - // }]), - // description: 'Appreciate the small moments with others who are doing the same', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: 'cb.newSubmission()', - // function: notifyUsersInNeed('New moment for Eating Japanese Food', 'View the photo').toString() - // }] - // }, - // situationaware_religious: { - // _id: Random.id(), - // name: 'Religious Worship', - // participateTemplate: 'uploadPhoto', - // resultsTemplate: 'photosByCategories', - // contributionTypes: addStaticAffordanceToNeeds('mechanismPoor', [{ - // needName: 'Religious Worship', - // situation: { - // detector: getDetectorId(DETECTORS.castle), - // number: 1 - // }, - // toPass: { - // instruction: 'Are you at a center for religious worship? Share a photo of something around you.' - // }, - // numberNeeded: 50, - // notificationDelay: 30, - // allowRepeatContributions: true, - // }]), - // description: 'Appreciate the small moments with others who are doing the same', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: 'cb.newSubmission()', - // function: notifyUsersInNeed('New moment for Religious Worship', 'View the photo').toString() - // }] - // }, - // situationaware_sunset: { - // _id: Random.id(), - // name: 'Catch the sunset', - // participateTemplate: 'uploadPhoto', - // resultsTemplate: 'photosByCategories', - // contributionTypes: addStaticAffordanceToNeeds('mechanismPoor', [{ - // needName: 'Catch the sunset', - // situation: { - // detector: getDetectorId(DETECTORS.sunset), - // number: 1 - // }, - // toPass: { - // instruction: 'Are you out during sunset? Share a photo of what the sky looks like where you are.' - // }, - // numberNeeded: 50, - // notificationDelay: 1, - // allowRepeatContributions: true, - // }]), - // description: 'Appreciate the small moments with others who are doing the same', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: 'cb.newSubmission()', - // function: notifyUsersInNeed('New moment for Catch the sunset', 'View the photo').toString() - // }] - // }, - // situationaware_asian: { - // _id: Random.id(), - // name: 'Eating Asian Food', - // participateTemplate: 'uploadPhoto', - // resultsTemplate: 'photosByCategories', - // contributionTypes: addStaticAffordanceToNeeds('mechanismPoor', [{ - // needName: 'Eating Asian Food', - // situation: { - // detector: getDetectorId(DETECTORS.eating_with_chopsticks), - // number: 1 - // }, - // toPass: { - // instruction: 'Are you eating at an asian restaurant? Share a photo of yourself dining out right now.' - // }, - // numberNeeded: 50, - // notificationDelay: 90, - // allowRepeatContributions: true, - // }]), - // description: 'Appreciate the small moments with others who are doing the same', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: 'cb.newSubmission()', - // function: notifyUsersInNeed('New moment for Eating Asian Food', 'View the photo').toString() - // }] - // }, - // situationaware_books: { - // _id: Random.id(), - // name: 'Reading a book', - // participateTemplate: 'uploadPhoto', - // resultsTemplate: 'photosByCategories', - // contributionTypes: addStaticAffordanceToNeeds('mechanismPoor', [{ - // needName: 'Reading a book', - // situation: { - // detector: getDetectorId(DETECTORS.library), - // number: 1 - // }, - // toPass: { - // instruction: 'Are you spending part of the day reading? Share a photo of what you are doing.' - // }, - // numberNeeded: 50, - // notificationDelay: 90, - // allowRepeatContributions: true, - // }]), - // description: 'Appreciate the small moments with others who are doing the same', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: 'cb.newSubmission()', - // function: notifyUsersInNeed('New moment for Reading a book', 'View the photo').toString() - // }] - // }, - // situationaware_parks: { - // _id: Random.id(), - // name: 'I love parks', - // participateTemplate: 'uploadPhoto', - // resultsTemplate: 'photosByCategories', - // contributionTypes: addStaticAffordanceToNeeds('mechanismPoor', [{ - // needName: 'I love parks', - // situation: { - // detector: getDetectorId(DETECTORS.forest), - // number: 1 - // }, - // toPass: { - // instruction: 'Are you at a park? Share a photo of what is going on around you.' - // }, - // numberNeeded: 50, - // notificationDelay: 15, - // allowRepeatContributions: true, - // }]), - // description: 'Appreciate the small moments with others who are doing the same', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: 'cb.newSubmission()', - // function: notifyUsersInNeed('New moment for I love parks', 'View the photo').toString() - // }] - // }, - // situationaware_rainy: { - // _id: Random.id(), - // name: 'Rainy Day', - // participateTemplate: 'uploadPhoto', - // resultsTemplate: 'photosByCategories', - // contributionTypes: addStaticAffordanceToNeeds('mechanismPoor', [{ - // needName: 'Rainy Day', - // situation: { - // detector: getDetectorId(DETECTORS.rainy), - // number: 1 - // }, - // toPass: { - // instruction: 'Is it raining today? Share a photo of what it looks like outside.' - // }, - // numberNeeded: 50, - // notificationDelay: 1, - // allowRepeatContributions: true, - // }]), - // description: 'Appreciate the small moments with others who are doing the same', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: 'cb.newSubmission()', - // function: notifyUsersInNeed('New moment for Rainy Day', 'View the photo').toString() - // }] - // }, - // situationaware_pizza: { - // _id: Random.id(), - // name: "Eating some 'Za", - // participateTemplate: 'uploadPhoto', - // resultsTemplate: 'photosByCategories', - // contributionTypes: addStaticAffordanceToNeeds('mechanismPoor', [{ - // needName: "Eating some 'Za", - // situation: { - // detector: getDetectorId(DETECTORS.eating_pizza), - // number: '1' - // }, - // toPass: { - // instruction: 'Are you eating pizza today? Share a photo of yourself at the pizza restaurant.', - // }, - // numberNeeded: 50, - // notificationDelay: 60, - // allowRepeatContributions: true, - // }]), - // description: 'Appreciate the small moments with others who are doing the same', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: 'cb.newSubmission()', - // function: notifyUsersInNeed('New moment for Eating some \'Za', 'View the photo').toString() - // }] - // }, - // situationaware_eatout: { - // _id: Random.id(), - // name: "Eating out", - // participateTemplate: 'uploadPhoto', - // resultsTemplate: 'photosByCategories', - // contributionTypes: addStaticAffordanceToNeeds('mechanismPoor', [{ - // needName: "Eating out", - // situation: { - // detector: getDetectorId(DETECTORS.restaurant), - // number: '1' - // }, - // toPass: { - // instruction: 'Are you eating out today? Share a photo of yourself at the restaurant.', - // }, - // numberNeeded: 50, - // notificationDelay: 60, - // allowRepeatContributions: true, - // }]), - // description: 'Appreciate the small moments with others who are doing the same', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: 'cb.newSubmission()', - // function: notifyUsersInNeed('New moment for Eating out', 'View the photo').toString() - // }] - // }, - // situationaware_bigbite: { - // _id: Random.id(), - // name: "Eating Big Bites", - // participateTemplate: 'uploadPhoto', - // resultsTemplate: 'photosByCategories', - // contributionTypes: addStaticAffordanceToNeeds('mechanismPoor', [{ - // needName: "Eating Big Bites", - // situation: { - // detector: getDetectorId(DETECTORS.big_bite_restaurant), - // number: '1' - // }, - // toPass: { - // instruction: 'Are you eating burritos, sandwiches, or burgers today? Share a photo of yourself at the restaurant.', - // }, - // numberNeeded: 50, - // notificationDelay: 60, - // allowRepeatContributions: true, - // }]), - // description: 'Appreciate the small moments with others who are doing the same', - // notificationText: 'View this and other available experiences', - // callbacks: [{ - // trigger: 'cb.newSubmission()', - // function: notifyUsersInNeed('New moment for Eating Big Bites', 'View the photo').toString() - // }] - // }, - sameSituationAwareness: { - _id: Random.id(), - name: 'Our Small Moments', - participateTemplate: 'uploadPhoto', - resultsTemplate: 'photosByCategories', - contributionTypes: sameSituationContributionTypes(), - description: 'During the small moments, we might be experiencing more together than we let on.', - notificationText: 'Your situation made you available to participate in Our Small Moments!', - callbacks: notifCbForMultiNeeds( - sameSituationContributionTypes(), - "cb.newSubmission(\"${need.needName}\")", - notifyUsersInNeed('Someone added to Our Small Moments', 'See the results under ${sub.needName}')) - }, - scavengerHunt: { - _id: Random.id(), - name: 'St. Patrick\'s Day Scavenger Hunt', - participateTemplate: 'scavengerHuntParticipate', - resultsTemplate: 'scavengerHunt', - contributionTypes: [{ - needName: 'beer', - situation: { - detector: DETECTORS.beer._id, - number: '1' - }, - toPass: { - instruction: 'Can you take a photo of beer?' - }, - numberNeeded: 1, - notificationDelay: 30, // 30 seconds for debugging - }, { - needName: 'greenProduce', - situation: { - detector: DETECTORS.produce._id, - number: '1' - }, - toPass: { - instruction: 'Can you take a photo of green vegetables? #leprechaunfood' - }, - numberNeeded: 1, - notificationDelay: 20, // 20 seconds for debugging - }, { - needName: 'coins', - situation: { - detector: DETECTORS.drugstore._id, - number: '1' - }, - toPass: { - instruction: 'Can you take a photo of chocolate gold coins on display?' - }, - numberNeeded: 1, - notificationDelay: 10, // 10 seconds for debugging - }, { - needName: 'leprechaun', - situation: { - detector: DETECTORS.costume_store._id, - number: '1' - }, - toPass: { - instruction: 'Can you take a photo of a Leprechaun costume?' - }, - numberNeeded: 1, - notificationDelay: 15, // 15 seconds for debugging - }, { - needName: 'irishSign', - situation: { - detector: DETECTORS.irish._id, - number: '1' - }, - toPass: { - instruction: 'Can you take a photo of an Irish sign?' - }, - numberNeeded: 1, - notificationDelay: 1, // 1 seconds for debugging (passing by) - }, { - needName: 'trimmings', - situation: { - detector: DETECTORS.hair_salon._id, - number: '1' - }, - toPass: { - instruction: 'Can you take a photo of some Leprechaun beard trimmings?' - }, - numberNeeded: 1, - notificationDelay: 10, // 10 seconds for debugging - }, { - needName: 'liquidGold', - situation: { - detector: DETECTORS.gas_station._id, - number: '1' - }, - toPass: { - instruction: 'Can you take a photo of liquid gold that Leprechauns use to power their vehicles?' - }, - numberNeeded: 1, - notificationDelay: 10, // 10 seconds for debugging - }, { - needName: 'potOfGold', - situation: { - detector: DETECTORS.bank._id, - number: '1' - }, - toPass: { - instruction: 'Can you take a photo of a bank where Leprechauns hide their pots of gold?' - }, - numberNeeded: 1, - notificationDelay: 10, // 10 seconds for debugging - }, { - needName: 'rainbow', - situation: { - detector: DETECTORS.rainbow._id, - number: '1' - }, - toPass: { - instruction: 'Can you take a photo of a rainbow flag?' - }, - numberNeeded: 1, - notificationDelay: 10, // 10 seconds for debugging - }], - description: 'Find an item for a scavenger hunt', - notificationText: 'Help us complete a St. Patrick\'s day scavenger hunt', - callbacks: [{ - trigger: 'cb.incidentFinished()', - function: sendNotificationScavenger.toString() - }] - }, - // natureHunt: { - // _id: Random.id(), - // name: 'Nature Scavenger Hunt', - // participateTemplate: 'scavengerHuntParticipate', - // resultsTemplate: 'scavengerHunt', - // contributionTypes: [{ - // needName: 'tree', - // situation: { - // detector: DETECTORS.forest._id, - // number: '1' - // }, - // toPass: { - // instruction: 'Can you take a photo of a tree?' - // }, - // numberNeeded: 1, - // notificationDelay: 10, // 10 seconds for debugging - // }, { - // needName: 'leaf', - // situation: { - // detector: DETECTORS.forest._id, - // number: '1' - // }, - // toPass: { - // instruction: 'Can you take a photo of a leaf?' - // }, - // numberNeeded: 1, - // notificationDelay: 10, // 10 seconds for debugging - // }, { - // needName: 'grass', - // situation: { - // detector: DETECTORS.field._id, - // number: '1' - // }, - // toPass: { - // instruction: 'Can you take a photo of the grass?' - // }, - // numberNeeded: 1, - // notificationDelay: 10, // 10 seconds for debugging - // }, { - // needName: 'lake', - // situation: { - // detector: DETECTORS.lake._id, - // number: '1' - // }, - // toPass: { - // instruction: 'Can you take a photo of the lake?' - // }, - // numberNeeded: 1, - // notificationDelay: 10, // 10 seconds for debugging - // }, { - // needName: 'moon', - // situation: { - // detector: DETECTORS.night._id, - // number: '1' - // }, - // toPass: { - // instruction: 'Can you take a photo of the moon?' - // }, - // numberNeeded: 1, - // notificationDelay: 1, // 1 seconds for debugging - // }, { - // needName: 'sun', - // situation: { - // detector: DETECTORS.sunny._id, - // number: '1' - // }, - // toPass: { - // instruction: 'Can you take a photo of the sun?' - // }, - // numberNeeded: 1, - // notificationDelay: 1, // 1 seconds for debugging - // }, { - // needName: 'blueSky', - // situation: { - // detector: DETECTORS.sunny._id, - // number: '1' - // }, - // toPass: { - // instruction: 'Can you take a photo of the blue sky?' - // }, - // numberNeeded: 1, - // notificationDelay: 1, // 1 seconds for debugging - // }, { - // needName: 'clouds', - // situation: { - // detector: DETECTORS.cloudy._id, - // number: '1' - // }, - // toPass: { - // instruction: 'Can you take a photo of the clouds?' - // }, - // numberNeeded: 1, - // notificationDelay: 1, // 1 seconds for debugging - // }, { - // needName: 'puddle', - // situation: { - // detector: DETECTORS.rainy._id, - // number: '1' - // }, - // toPass: { - // instruction: 'Can you take a photo of the puddle?' - // }, - // numberNeeded: 1, - // notificationDelay: 1, // 1 seconds for debugging - // }], - // description: 'Find an item for a scavenger hunt', - // notificationText: 'Help us out with our nature scavenger hunt', - // callbacks: [{ - // trigger: 'cb.incidentFinished()', - // function: sendNotificationScavenger.toString() - // }] - // }, - // foodfight: { - // _id: Random.id(), - // name: "Food Fight!", - // participateTemplate: "scavengerHuntParticipate", - // resultsTemplate: "scavengerHunt", - // contributionTypes: [ - // { - // needName: "foodPhoto", - // situation: { - // detector: DETECTORS.restaurant._id, - // number: 1 - // }, - // toPass: { - // instruction: "Can you take a photo of what you're eating?" - // }, - // numberNeeded: 1 - // }, - // { - // needName: "foodPhoto", - // situation: { - // detector: DETECTORS.restaurant._id, - // number: 1 - // }, - // toPass: { - // instruction: "Can you take a photo of what you're eating?" - // }, - // numberNeeded: 1 - // } - // ], - // description: "Food fight!", - // notificationText: "Food fight!", - // callbacks: [{ - // trigger: "cb.incidentFinished()", - // function: sendNotificationFoodFight.toString() - // }] - // } + cn: convertCNtoCE(cn()) }; export const CONSTANTS = { @@ -2945,46 +518,6 @@ export const CONSTANTS = { // Comment out if you would like to only test specific experiences // 'EXPERIENCES': (({ halfhalfEmbodiedMimicry }) => ({ halfhalfEmbodiedMimicry }))(EXPERIENCES), 'EXPERIENCES': EXPERIENCES, + // 'EXPERIENCES': TRIADIC_EXPERIENCES, 'DETECTORS': DETECTORS }; - -// Meteor.call('locations.updateUserLocationAndAffordances', { -// uid: Accounts.findUserByUsername('b@gmail.com')._id, -// lat: 42.054902, //lakefill -// lng: -87.670197 -// }); -// Meteor.call('locations.updateUserLocationAndAffordances', { -// uid: Accounts.findUserByUsername('c@gmail.com')._id, -// lat: 42.056975, //ford -// lng: -87.676575 -// }); -// Meteor.call('locations.updateUserLocationAndAffordances', { -// uid: Accounts.findUserByUsername('d@gmail.com')._id, -// lat: 42.059273, //garage -// lng: -87.673794 -// }); -// Meteor.call('locations.updateUserLocationAndAffordances', { -// uid: Accounts.findUserByUsername('e@gmail.com')._id, -// lat: 42.044314, //nevins -// lng: -87.682157 -// }); -// -// Meteor.call('locations.updateUserLocationAndAffordances', { -// uid: Accounts.findUserByUsername('g@gmail.com')._id, -// lat: 42.044314, //nevins -// lng: -87.682157 -// }); -// Meteor.call('locations.updateUserLocationAndAffordances', { -// uid: Accounts.findUserByUsername('h@gmail.com')._id, -// lat: 42.045398, //pubs -// lng: -87.682431 -// }); -// Meteor.call('locations.updateUserLocationAndAffordances', { -// uid: Accounts.findUserByUsername('i@gmail.com')._id, -// lat: 42.047621, //grocery, whole foods -// lng: -87.679488 -// }); -// Meteor.call('locations.updateUserLocationAndAffordances', { -// uid: Accounts.findUserByUsername('j@gmail.com')._id, -// lat: 42.042617, //beach -// lng: -87.671474 \ No newline at end of file diff --git a/imports/api/UserMonitor/detectors/server/publications.js b/imports/api/UserMonitor/detectors/server/publications.js index 93cb07ab..df8f81b1 100644 --- a/imports/api/UserMonitor/detectors/server/publications.js +++ b/imports/api/UserMonitor/detectors/server/publications.js @@ -1,4 +1,4 @@ -import { Meteor } from 'meteor/meteor'; +// import { Meteor } from 'meteor/meteor'; import { MongoInternals } from 'meteor/mongo'; import { Detectors } from '../detectors.js'; diff --git a/imports/api/UserMonitor/locations/methods.js b/imports/api/UserMonitor/locations/methods.js index aec8389a..279dd4cb 100644 --- a/imports/api/UserMonitor/locations/methods.js +++ b/imports/api/UserMonitor/locations/methods.js @@ -1,13 +1,16 @@ -import { Meteor } from "meteor/meteor"; +// import { Meteor } from "meteor/meteor"; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { SimpleSchema } from 'meteor/aldeed:simple-schema'; import { log } from '../../logs.js'; import { Locations } from './locations.js'; -import { findMatchesForUser, getNeedDelay, clearAvailabilitiesForUser } from +import { findMatchesForUser, getNeedDelay} from '../../OCEManager/OCEs/methods' import { runCoordinatorAfterUserLocationChange } from '../../OpportunisticCoordinator/server/executor' -import { decomissionFromAssignmentsIfAppropriate } from "../../OpportunisticCoordinator/server/identifier"; +import { + clearAvailabilitiesForUser, + decomissionFromAssignmentsIfAppropriate +} from "../../OpportunisticCoordinator/server/identifier"; import { getAffordancesFromLocation } from '../detectors/methods'; import { CONFIG } from "../../config"; import { Location_log } from "../../Logging/location_log"; @@ -47,6 +50,7 @@ export const onLocationUpdate = (uid, location, callback) => { // attempt to find a user with the given uid let user = Meteor.users.findOne({_id: uid}); + serverLog.call({message: `user ${user}`}); if (user) { @@ -67,6 +71,7 @@ export const onLocationUpdate = (uid, location, callback) => { // get affordances and begin coordination process getAffordancesFromLocation(uid, location, retrievePlaces, function (uid, bgLocationObject, affordances) { + log.info(`success: getAffordancesFromLocation`); // get affordances via affordance aware let user = Meteor.users.findOne({_id: uid}); if (!user) { @@ -76,6 +81,7 @@ export const onLocationUpdate = (uid, location, callback) => { let userAffordances = user.profile.staticAffordances; affordances = Object.assign({}, affordances, userAffordances); affordances = affordances !== null ? affordances : {}; + serverLog.call({message: `affordances ${affordances}`}); // blocking, since everything in system works off of Locations collection updateLocationInDb(uid, bgLocationObject, affordances); @@ -204,15 +210,6 @@ export const userNotifiedTooRecently = (user) => { return (now - lastNotified) < waitTimeAfterNotified; }; -/** - * Checks if user has an active incident, meaning they were assigned to an incident - * - * @param user {Object} has Meteor.users Schema - * @return {boolean} whether user is currently assigned to an experience or not - */ -export const userIsAssignedAlready = (user) => { - return user.profile.activeIncidents.length > 0; -}; /** * Computes distance between a start and end location in meters using the haversine forumla. diff --git a/imports/api/UserMonitor/locations/server/publications.js b/imports/api/UserMonitor/locations/server/publications.js index 671d772e..49eca322 100644 --- a/imports/api/UserMonitor/locations/server/publications.js +++ b/imports/api/UserMonitor/locations/server/publications.js @@ -1,4 +1,4 @@ -import { Meteor } from 'meteor/meteor'; +// import { Meteor } from 'meteor/meteor'; import { Locations } from '../locations.js'; Meteor.publish('locations.all', function () { diff --git a/imports/api/UserMonitor/users/methods.js b/imports/api/UserMonitor/users/methods.js index 01f3f0cb..a1b28489 100644 --- a/imports/api/UserMonitor/users/methods.js +++ b/imports/api/UserMonitor/users/methods.js @@ -1,28 +1,39 @@ -import { Meteor } from 'meteor/meteor'; +// import { Meteor } from 'meteor/meteor'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { SimpleSchema } from 'meteor/aldeed:simple-schema'; +import { Assignments } from "../../OpportunisticCoordinator/databaseHelpers"; -export const findUserByUsername = function (username) { - return Meteor.users.findOne({ 'username': username }); +// via dburles:collection-helpers +Meteor.users.helpers({ + /** + * usage in Meteor client/server code: Meteor.users.findOne().activeIncidents() + * usage in Meteor Blaze template JS code: N/A -- had difficulty doing this + * + * @returns: activeIncidents {Array} array of incident iids e.g. [iid1, iid2] + */ + activeIncidents() { + return getUserActiveIncidents(this._id); + } +}); + +/** + * activeIncidents are the ones in which a user is assigned. + * + * @param uid + * @return activeIncidents {Array} array of incident iids e.g. [iid1, iid2] or empty array [] + */ +export const getUserActiveIncidents = (uid) => { + return Assignments.find({"needUserMaps.users.uid": uid}, {fields: {_id: 1}}).fetch().map(doc => doc._id); }; -export const _addActiveIncidentToUser = function (uid, iid) { - Meteor.users.update({ - _id: uid - }, { - $addToSet: { - 'profile.activeIncidents': iid - } - }); +export const findUserByUsername = function (username) { + return Meteor.users.findOne({ 'username': username }); }; export const _removeActiveIncidentFromUser = function (uid, iid) { Meteor.users.update({ _id: uid }, { - $pull: { - 'profile.activeIncidents': iid - }, $addToSet: { 'profile.pastIncidents': iid } @@ -31,19 +42,6 @@ export const _removeActiveIncidentFromUser = function (uid, iid) { // TODO(rlouie): remove the active incident/need/place/dist info too }; -export const _removeIncidentFromUserEntirely = function (uid, iid) { - Meteor.users.update({ - _id: uid - }, { - $pull: { - 'profile.activeIncidents': iid - } - }); - - // TODO(rlouie): remove the active incident/need/place/dist info too -}; - - export const getEmails = new ValidatedMethod({ name: 'users.getEmails', validate: new SimpleSchema({ diff --git a/imports/api/UserMonitor/users/server/publications.js b/imports/api/UserMonitor/users/server/publications.js index 0ec58046..0efdfe75 100644 --- a/imports/api/UserMonitor/users/server/publications.js +++ b/imports/api/UserMonitor/users/server/publications.js @@ -1,4 +1,4 @@ -import { Meteor } from 'meteor/meteor'; +// import { Meteor } from 'meteor/meteor'; import { Users } from '../users.js'; Meteor.publish('users.all', function () { diff --git a/imports/api/UserMonitor/users/users.js b/imports/api/UserMonitor/users/users.js index c7e4957c..cf6af084 100644 --- a/imports/api/UserMonitor/users/users.js +++ b/imports/api/UserMonitor/users/users.js @@ -1,4 +1,4 @@ -import { Meteor } from 'meteor/meteor'; +// import { Meteor } from 'meteor/meteor'; import { SimpleSchema } from 'meteor/aldeed:simple-schema'; import { _ } from 'meteor/underscore'; import { Schema } from '../../schema.js'; diff --git a/imports/api/UserMonitor/users/users.tests.js b/imports/api/UserMonitor/users/users.tests.js new file mode 100644 index 00000000..3d04c485 --- /dev/null +++ b/imports/api/UserMonitor/users/users.tests.js @@ -0,0 +1,87 @@ +// import {Meteor} from 'meteor/meteor'; +import {resetDatabase} from 'meteor/xolvio:cleaner'; +import {Assignments} from "../../OpportunisticCoordinator/databaseHelpers"; +import {findUserByUsername, getUserActiveIncidents} from "./methods"; +import {insertTestUser} from "../../OpportunisticCoordinator/populateDatabase"; + +describe('users activeIncidents computed from Assignments', () => { + const iid1 = Random.id(); + const iid2 = Random.id(); + const usernameA = 'garrett'; + const usernameB = 'garretts_brother'; + const needName1 = 'Rainy 1'; + const needName2 = 'Rainy 2'; + let userA; + let userB; + + beforeEach(() => { + resetDatabase(); + + insertTestUser(usernameA); + insertTestUser(usernameB); + const testUserA = findUserByUsername(usernameA); + const testUserB = findUserByUsername(usernameB); + userA = testUserA._id; + userB = testUserB._id; + Assignments.insert({ + _id: iid1, + needUserMaps: [ + { + needName: needName1, + users: [ + {uid: userA} + ] + }, + ], + }); + Assignments.insert({ + _id: iid2, + needUserMaps: [ + { + needName: needName2, + users: [ + {uid: userA}, + {uid: userB}, + ] + } + ], + }); + + }); + + it('should show userA be assigned to two incidents via getUserActiveIncidents', () => { + + const activeIncidents = getUserActiveIncidents(userA); + console.log(`activeIncidents: \n ${JSON.stringify(activeIncidents)}`); + chai.assert(activeIncidents.includes(iid1)); + chai.assert(activeIncidents.includes(iid2)); + + }); + + it('should show userB be assigned to one incident via getUserActiveIncidents', () => { + + const activeIncidents = getUserActiveIncidents(userB); + console.log(`activeIncidents: \n ${JSON.stringify(activeIncidents)}`); + + chai.assert(activeIncidents.includes(iid2)); + + }); + + it('should show userA be assigned to two incidents via activeIncidents collection helper', () => { + + const activeIncidents = Meteor.users.findOne(userA).activeIncidents(); + console.log(`activeIncidents: \n ${JSON.stringify(activeIncidents)}`); + chai.assert(activeIncidents.includes(iid1)); + chai.assert(activeIncidents.includes(iid2)); + + }); + + it('should show userB be assigned to one incident via activeIncidents collection helper', () => { + + const activeIncidents = Meteor.users.findOne(userB).activeIncidents(); + console.log(`activeIncidents: \n ${JSON.stringify(activeIncidents)}`); + + chai.assert(activeIncidents.includes(iid2)); + + }); +}); diff --git a/imports/api/custom/arrayHelpers.js b/imports/api/custom/arrayHelpers.js new file mode 100644 index 00000000..32171d0a --- /dev/null +++ b/imports/api/custom/arrayHelpers.js @@ -0,0 +1,17 @@ +export const setIntersection = function (A, B) { + let A_with_string_elements = A.map((e) => { + return JSON.stringify(e) + }); + let B_with_string_elements = B.map((e) => { + return JSON.stringify(e) + }); + let beforeSet = new Set(A_with_string_elements); + let afterSet = new Set(B_with_string_elements); + + let intersection = new Set( + [...beforeSet].filter(x => afterSet.has(x))); + + return Array.from(intersection).map((e) => { + return JSON.parse(e) + }); +}; \ No newline at end of file diff --git a/imports/api/detectors/methods.js b/imports/api/detectors/methods.js new file mode 100644 index 00000000..e7d1003e --- /dev/null +++ b/imports/api/detectors/methods.js @@ -0,0 +1,69 @@ +// import { Meteor } from 'meteor/meteor'; +import { Detectors } from './detectors' +import {serverLog} from "../logs"; + + +/** + * Gets affordances based on location, then calls a callback + * @param {float} lat + * @param {float} lng + * @param {function} callback - which calls with single argument affordances + */ +export const getAffordancesFromLocation = function (lat, lng, callback) { + let request = require('request'); + let url = 'http://affordanceaware.herokuapp.com/location_keyvalues/' + lat.toString() + '/' + lng.toString(); + request(url, Meteor.bindEnvironment(function (error, response, body) { + if (!error && response.statusCode === 200) { + let affordances = JSON.parse(body); + if (affordances !== Object(affordances)) { + serverLog.call({message: "Locations/methods expected type Object but did not receive an Object, doing nothing"}); + + } + callback(affordances); + }else{ + serverLog.call({message: "ERROR WITH ACCORDANCE AWARE "}); + + } + })); +}; + +export const matchAffordancesWithDetector = function (affordances, detectorId) { + const detector = Detectors.findOne({ _id: detectorId }); + // console.log("dectectorId = " + detectorId); + // console.log("dectector = " + detector); + let doesUserMatchSituation = applyDetector(affordances, + detector.variables, + detector.rules); + return doesUserMatchSituation; +}; + +/** + * Evaluates given the affordances of a user, if they match the definition given + * by the detector. + * @param {Object} userAffordances: key value pairs of (userAffordances: values) + * @param {[String]} varDecl - variable declarations of the affordance keys used + * @param {[String]} rules - context rules as Javascript logical operations + * @return {Boolean} doesUserMatchSituation + */ +applyDetector = function (userAffordances, varDecl, rules) { + let affordancesAsJavascriptVars = keyvalues2vardecl(userAffordances); + let mergedAffordancesWithRules = varDecl.concat(affordancesAsJavascriptVars) + .concat(rules) + .join('\n'); + let doesUserMatchSituation = eval(mergedAffordancesWithRules); + return doesUserMatchSituation; +}; + +/** + * @param {Object} obj - key values that come from /location_keyvalues/{lat}/{lng} + * @return {[String]} vardecl - each element has the form "var key = value;" + */ +keyvalues2vardecl = function (obj) { + let vardecl = []; + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + vardecl.push("var " + key + " = " + obj[key] + ";") + } + } + return vardecl; +}; \ No newline at end of file diff --git a/imports/api/locations/methods.js b/imports/api/locations/methods.js new file mode 100644 index 00000000..e2a06cdc --- /dev/null +++ b/imports/api/locations/methods.js @@ -0,0 +1,175 @@ +import { ValidatedMethod } from "meteor/mdg:validated-method"; +import { SimpleSchema } from "meteor/aldeed:simple-schema"; +import { log } from "../logs.js"; +import { Locations } from "./locations.js"; + +import { findMatchesForUser } from "../experiences/methods"; +import { runCoordinatorAfterUserLocationChange } from "../coordinator/server/methods"; +import { updateAssignmentDbdAfterUserLocationChange } from "../coordinator/methods"; +import { getAffordancesFromLocation } from "../detectors/methods"; +import { CONFIG } from "../config"; +import { Availability } from "../coordinator/availability"; +// import { Meteor } from "meteor/meteor"; +import { Location_log } from "./location_log"; +import { serverLog } from "../logs"; + +Meteor.methods({ + triggerUpdate(lat, lng, uid) { + onLocationUpdate(uid, lat, lng, function() { + serverLog.call({ + message: "triggering manual location update for: " + uid + }); + }); + } +}); + +/** + * Saves location in DB and sends data to sendToMatcher function. + * Run on updates from LocationTracking package. + * + * @param uid {string} uid of user who's location just changed + * @param lat {float} latitude of new location + * @param lng {float} longitude of new location + */ +export const onLocationUpdate = (uid, lat, lng, callback) => { + //TODO: this could def be a clearner call or its own function + let availabilityObjects = Availability.find().fetch(); + _.forEach(availabilityObjects, av => { + _.forEach(av.needUserMaps, needEntry => { + Availability.update( + { + _id: av._id, + "needUserMaps.needName": needEntry.needName + }, + { + $pull: { "needUserMaps.$.uids": uid } + } + ); + }); + }); + + getAffordancesFromLocation(lat, lng, function(affordances) { + let user = Meteor.users.findOne(uid); + if (user) { + let userAffordances = user.profile.staticAffordances; + affordances = Object.assign({}, affordances, userAffordances); + updateLocationInDb(uid, lat, lng, affordances); + callback(); + + Meteor.setTimeout(function() { + let newAffs = Locations.findOne({ uid: user._id }).affordances; + let sharedKeys = _.intersection( + Object.keys(newAffs), + Object.keys(affordances) + ); + + let sharedAffs = []; + _.forEach(sharedKeys, key => { + sharedAffs[key] = newAffs[key]; + }); + + updateAssignmentDbdAfterUserLocationChange(uid, sharedAffs); + sendToMatcher(uid, sharedAffs); + }, 5 * 0); + } + }); +}; + +/** + * Finds the matches (findMatchesFunction in User::Experience Matcher) for the user for a user's location update and + * sends found matches to the coordinator. + * + * @param uid {string} uid of user who's location just changed + * @param lat {float} latitude of new location + * @param lng {float} longitude of new location + */ +const sendToMatcher = (uid, affordances) => { + // should check whether a user is available before sending to coordinator + // TODO: replace false with config.debug global setting + let userCanParticipate = userIsAvailableToParticipate(uid); + + if (userCanParticipate) { + let availabilityDictionary = findMatchesForUser(uid, affordances); + runCoordinatorAfterUserLocationChange(uid, availabilityDictionary); + } +}; + +// TODO: implement this +/** + * Returns whether a user can participate based on when they were last notified/last participated. + * Debug mode shortens the time between experiences for easier debugging. + * + * @param uid {string} uid of user who's location just changed + * @param debug {boolean} choose to run in debug mode or not + * @returns {boolean} whether a user can participate in an experience + */ +const userIsAvailableToParticipate = uid => { + let time = 60 * 1000; + + if (CONFIG.MODE === "DEV") { + time = time * 2; + } else if (CONFIG.MODE === "PROD") { + time = time * 65; + } else { + time = time * 65; + } + // console.log("last notif:", uid, Meteor.users.findOne(uid).profile.lastNotified); + // console.log("now: ", Date.now(), "time: ", time); + // console.log("dif:", (Date.now() - Meteor.users.findOne(uid).profile.lastNotified) ); + // + // console.log("IS USER AVAIABLE TO PARTICPATE", (Date.now() - Meteor.users.findOne(uid).profile.lastNotified) > time) + + return Date.now() - Meteor.users.findOne(uid).profile.lastNotified > time; +}; + +/** + * Updates the location for a user in the database. + * + * @param uid {string} uid of user who's location just changed + * @param lat {float} latitude of new location + * @param lng {float} longitude of new location + * @param affordances {object} affordances key/value dictionary + */ +const updateLocationInDb = (uid, lat, lng, affordances) => { + const entry = Locations.findOne({ uid: uid }); + if (entry) { + Locations.update( + entry._id, + { + $set: { + lat: lat, + lng: lng, + timestamp: Date.now(), + affordances: affordances + } + }, + err => { + if (err) { + log.error("Locations/methods, can't update a location", err); + } + } + ); + } else { + Locations.insert( + { + uid: uid, + lat: lat, + lng: lng, + timestamp: Date.now(), + affordances: affordances + }, + err => { + if (err) { + log.error("Locations/methods, can't add a new location", err); + } + } + ); + } + Location_log.insert({ + uid: uid, + lat: lat, + lng: lng, + timestamp: Date.now(), + affordances: affordances + }); +}; diff --git a/imports/api/logs.js b/imports/api/logs.js index 7be3612c..4f8abdb1 100644 --- a/imports/api/logs.js +++ b/imports/api/logs.js @@ -1,4 +1,4 @@ -import { Meteor } from 'meteor/meteor'; +// import { Meteor } from 'meteor/meteor'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { SimpleSchema } from 'meteor/aldeed:simple-schema'; import 'meteor/nooitaf:colors'; diff --git a/imports/api/messages/messages.js b/imports/api/messages/messages.js new file mode 100644 index 00000000..4f37b6c6 --- /dev/null +++ b/imports/api/messages/messages.js @@ -0,0 +1,3 @@ +import { Mongo } from 'meteor/mongo'; + +export const Messages = new Mongo.Collection('messages'); \ No newline at end of file diff --git a/imports/api/messages/server/methods.js b/imports/api/messages/server/methods.js new file mode 100644 index 00000000..ab6e2be2 --- /dev/null +++ b/imports/api/messages/server/methods.js @@ -0,0 +1,89 @@ +// import { Meteor } from 'meteor/meteor'; +import { check, Match } from 'meteor/check'; + +import { Messages } from '../messages.js'; + +Meteor.methods({ + + sendMessage: function (data) { + //console.log("sending Message") + + check(data, { + message: String, //the message to send + name: Match.Optional(String), //if the user already has a name + image: String, + }); + + let participant = Meteor.users.findOne({ + "_id": data.name, + }) + + // if (data.name=="") { + // throw new Meteor.Error("no name"); + // } + console.log(participant.profile.firstName + ": " + data.message); + + let userName = data.name ? participant.profile.firstName : "Anonymous"; + //let role = data.role + + //console.log(userName) + + const matchName = data.message.match(/^My name is (.*)/i); + let img; + + if (data.image != "") { + img = data.image + console.log("image sent") + } else { + img = "" + } + + //console.log("img: " + img) + + // if (matchName && matchName[1]!="") { + // userName = matchName[1]; + // Messages.insert({ + // name: "Narrator", + // message: "The murderer is at a " + info, + // createdAt: new Date(), + // recipient: "all", + // }); + // } else { + Messages.insert({ + name: userName, + message: data.message, + createdAt: new Date(), + recipient: "all", + imageID: img, + //chapter: "1" + }); + //} + + return { + name: userName + }; + + }, + + sendWhisper: function (role, user, instruction) { + console.log("Welcome! You've been cast in the " + role + " role! " + instruction + ""); + Messages.insert({ + name: "Narrator (whispers to you)", + message: "Welcome! You've been cast in the " + role + " role! " + instruction + "", + createdAt: new Date(), + recipient: user, + }); + }, + + sendPrompt: function (info) { + console.log(info); + Messages.insert({ + name: "Narrator", + message: info, + createdAt: new Date(), + recipient: "all", + }); + } + + +}); \ No newline at end of file diff --git a/imports/api/messages/server/publications.js b/imports/api/messages/server/publications.js new file mode 100644 index 00000000..02222016 --- /dev/null +++ b/imports/api/messages/server/publications.js @@ -0,0 +1,6 @@ +// import { Meteor } from 'meteor/meteor'; +import { Messages } from '../messages.js'; + +Meteor.publish("messages", function() { + return Messages.find({}, { fields: { name: 1, message: 1, createdAt: 1, announcement: 1, recipient: 1, imageID: 1, }, limit: 100, sort: { createdAt: -1 } }); //we want the 100 most recent messages +}); \ No newline at end of file diff --git a/imports/one.app-test.js b/imports/one.app-test.js index b65298b7..b684d549 100644 --- a/imports/one.app-test.js +++ b/imports/one.app-test.js @@ -1,4 +1,4 @@ -import {Meteor} from 'meteor/meteor'; +// import {Meteor} from 'meteor/meteor'; //meteor test --full-app --driver-package practicalmeteor:mocha diff --git a/imports/startup/accounts_config.js b/imports/startup/accounts_config.js index fbadd801..dffe11df 100644 --- a/imports/startup/accounts_config.js +++ b/imports/startup/accounts_config.js @@ -41,7 +41,6 @@ AccountsTemplates.configure({ info.profile.lastParticipated = null; info.profile.lastNotified = null; info.profile.pastIncidents = []; - info.profile.activeIncidents = []; info.profile.staticAffordances = {}; }, }); diff --git a/imports/startup/client/load.js b/imports/startup/client/load.js index fda9ed58..a50e6e71 100644 --- a/imports/startup/client/load.js +++ b/imports/startup/client/load.js @@ -1,5 +1,5 @@ import { GoogleMaps } from 'meteor/dburles:google-maps'; -import {Meteor} from "meteor/meteor"; +// import {Meteor} from "meteor/meteor"; import {serverLog} from "../../api/logs"; diff --git a/imports/startup/client/notifications.js b/imports/startup/client/notifications.js index b1d3ed48..b78570fe 100644 --- a/imports/startup/client/notifications.js +++ b/imports/startup/client/notifications.js @@ -1,7 +1,7 @@ import { Push } from 'meteor/raix:push'; import { Router } from 'meteor/iron:router'; import { log, serverLog } from '../../api/logs.js'; -import {Meteor} from "meteor/meteor"; +// import {Meteor} from "meteor/meteor"; Push.Configure({ android: { diff --git a/imports/startup/client/router.js b/imports/startup/client/router.js index 09c3fd72..d94efc0f 100644 --- a/imports/startup/client/router.js +++ b/imports/startup/client/router.js @@ -23,12 +23,15 @@ import '../../ui/pages/api_custom_results.js'; import '../../ui/pages/affordances.js'; import '../../ui/pages/participate_backdoor.html'; import '../../ui/pages/participate_backdoor.js'; +import '../../ui/pages/dynamic_participate.html'; +import '../../ui/pages/dynamic_participate.js'; import { Experiences, Incidents } from "../../api/OCEManager/OCEs/experiences"; import { Locations } from "../../api/UserMonitor/locations/locations"; import {Avatars, Images} from "../../api/ImageUpload/images"; +import { Messages } from "../../api/messages/messages.js"; import { Submissions } from "../../api/OCEManager/currentNeeds"; -import { Meteor } from "meteor/meteor"; +// import { Meteor } from "meteor/meteor"; import {Assignments, Availability} from "../../api/OpportunisticCoordinator/databaseHelpers"; import {Notification_log} from "../../api/Logging/notification_log"; import {Page_log} from "../../api/Logging/page_log/page_log"; @@ -69,29 +72,39 @@ Router.route('affordances', { } }); +Router.route('api.custom.dynamic', { + path: '/apicustomdynamic/:iid/:detectorId', + template: 'dynamicParticipate', + before: function() { + this.next(); + }, +}); + Router.route('api.custom', { path: '/apicustom/:iid/:eid/:needName', template: 'api_custom', before: function () { - if (Meteor.userId()) { - let dic = { - uid: Meteor.userId(), - timestamp: Date.now(), - route: "customparticipate", - params: { - iid: this.params.iid, - eid: this.params.eid, - needName: this.params.needName - } - }; - Meteor.call('insertLog', dic); + if (!Meteor.userId()) { + Router.go('home'); } + let dic = { + uid: Meteor.userId(), + timestamp: Date.now(), + route: "customparticipate", + params: { + iid: this.params.iid, + eid: this.params.eid, + needName: this.params.needName + } + }; + Meteor.call('insertLog', dic); this.subscribe('experiences.single', this.params.eid).wait(); this.subscribe('incidents.single', this.params.iid).wait(); this.subscribe('locations.activeUser').wait(); this.subscribe('images.activeIncident', this.params.iid).wait(); this.subscribe('notification_log.activeIncident', this.params.iid).wait(); + this.subscribe('participating.now.activeIncident', this.params.iid).wait(); // TODO(rlouie): create subscribers which only get certain fields like, username which would be useful for templates this.subscribe('users.all').wait(); this.subscribe('avatars.all').wait(); diff --git a/imports/startup/location_tracking.js b/imports/startup/location_tracking.js index a75d910e..86f4c3d2 100644 --- a/imports/startup/location_tracking.js +++ b/imports/startup/location_tracking.js @@ -1,4 +1,4 @@ -import { Meteor } from 'meteor/meteor'; +// import { Meteor } from 'meteor/meteor'; import { serverLog } from '../api/logs.js'; import { Tracker } from 'meteor/tracker'; diff --git a/imports/startup/server/fixtures.js b/imports/startup/server/fixtures.js index b9dee2b9..cf98071b 100644 --- a/imports/startup/server/fixtures.js +++ b/imports/startup/server/fixtures.js @@ -1,4 +1,4 @@ -import { Meteor } from 'meteor/meteor'; +// import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; import { Random } from 'meteor/random' @@ -6,6 +6,7 @@ import { CONFIG } from '../../api/config.js'; import { Experiences, Incidents } from '../../api/OCEManager/OCEs/experiences.js'; import { Locations } from '../../api/UserMonitor/locations/locations.js'; import { Submissions } from "../../api/OCEManager/currentNeeds"; +import { Messages } from "../../api/messages/messages.js"; import { Assignments, Availability } from "../../api/OpportunisticCoordinator/databaseHelpers"; import { Images, Avatars } from '../../api/ImageUpload/images.js'; import { log } from '../../api/logs.js'; @@ -39,6 +40,7 @@ Meteor.methods({ Object.values(CONSTANTS.DETECTORS).forEach(function (value) { Detectors.insert(value); }); + log.info(`${CONSTANTS.DETECTORS}`); log.info(`Populated ${ Detectors.find().count() } detectors`); }, startStorytime(){ @@ -55,6 +57,13 @@ Meteor.methods({ let incident = createIncidentFromExperience(value); startRunningIncident(incident); }, + startBumpedThree(){ + console.log("starting bumped three"); + let value = CONSTANTS.EXPERIENCES.bumpedThree; + Experiences.insert(value); + let incident = createIncidentFromExperience(value); + startRunningIncident(incident); + }, startScavengerHunt(){ console.log("starting scavenger"); @@ -122,14 +131,10 @@ function createTestData(){ createTestExperiences(); log.info(`Created ${ Experiences.find().count() } experiences`); - let uid1 = findUserByUsername('garrett')._id; - let uid2 = findUserByUsername('garretts_brother')._id; - let uid3 = findUserByUsername('meg')._id; - let uid4 = findUserByUsername('megs_sister')._id; - let uid5 = findUserByUsername('josh')._id; + let uid1 = findUserByUsername('meg')._id; + let uid2 = findUserByUsername('josh')._id; + let uid3 = findUserByUsername('andrew')._id; - let olinuid1 = findUserByUsername('nagy')._id; - let olinuid2 = findUserByUsername('bonnie')._id; Meteor.users.update({ // everyone @@ -140,13 +145,12 @@ function createTestData(){ "profile.lastParticipated": null, "profile.lastNotified": null, "profile.pastIncidents": [], - "profile.activeIncidents": [], "profile.staticAffordances": {} } }, { multi: true }); - + /* Meteor.users.update({ _id: {$in: [uid1, uid2]} }, { @@ -166,7 +170,7 @@ function createTestData(){ Meteor.users.update({ _id: {$in: [uid1, uid3, uid5]} }, { - $set: { 'profile.staticAffordances.lovesDTR': true} + $set: { 'profile.staticAffordances.lovesDTR': true } }, { multi: true }); @@ -179,5 +183,22 @@ function createTestData(){ multi: true }); - log.debug('FOR LOCATION TESTING RUN >>>> python simulatelocations.py '+ uid1 + " " + uid2 + " " + uid3+" " + uid4 + " " + uid5 ); + Meteor.users.update({ + _id: {$in: [uid1, uid2, uid3, uid4, uid5]} + }, { + $set: { 'profile.staticAffordances': { "triadOne": true} } + }, { + multi: true + }); +*/ +Meteor.users.update({ + _id: {$in: [uid1, uid2, uid3]} + }, { + $set: { 'profile.staticAffordances': { "cn": true} } + }, { + multi: true + }); + + log.debug('FOR LOCATION TESTING RUN >>>> python simulatelocations.py '+ uid1 + " " + uid2 + " " + uid3); + //log.debug('uid1 ' + Meteor.users.find({_id: uid1})).username; } diff --git a/imports/startup/server/register-api.js b/imports/startup/server/register-api.js index a8b9398b..4baf5519 100644 --- a/imports/startup/server/register-api.js +++ b/imports/startup/server/register-api.js @@ -2,6 +2,8 @@ import '../../api/OCEManager/OCEs/server/publications.js'; import '../../api/OCEManager/OCEs/methods.js'; import '../../api/ImageUpload/server/publications.js'; import '../../api/ImageUpload/methods.js'; +import '../../api/messages/server/methods.js'; +import '../../api/messages/server/publications.js'; import '../../api/UserMonitor/users/methods.js'; import '../../api/UserMonitor/users/server/publications.js'; import '../../api/UserMonitor/locations/server/publications.js'; @@ -13,6 +15,6 @@ import '../../api/OpportunisticCoordinator/server/publications.js'; import '../../api/OCEManager/progressorHelper.js'; import '../../api/OCEManager/server/publications.js'; import '../../api/OpportunisticCoordinator/server/noticationMethods.js'; -import '../../api/Testing/createNewExperiences'; +import '../../api/Testing/createNewExperiences.js'; import '../../api/Logging/page_log/methods.js'; import '../../api/Logging/groundtruth_log/methods.js'; diff --git a/imports/ui/components/active_experience.html b/imports/ui/components/active_experience.html index 7959ad7b..a6014d14 100644 --- a/imports/ui/components/active_experience.html +++ b/imports/ui/components/active_experience.html @@ -1,9 +1,11 @@ +

{{role}}

+ +--> + @@ -40,7 +46,93 @@ + + + + + + + + + \ No newline at end of file diff --git a/imports/ui/components/contributions.js b/imports/ui/components/contributions.js new file mode 100644 index 00000000..905914c9 --- /dev/null +++ b/imports/ui/components/contributions.js @@ -0,0 +1,226 @@ + import { Template } from 'meteor/templating'; + import { Cookies } from 'meteor/mrt:cookies'; + // import { Meteor } from 'meteor/meteor'; + import { Messages } from '../../api/messages/messages.js'; + import { Images } from '../../api/ImageUpload/images.js'; + + //import '../../api/messages/server/methods.js'; + import moment from 'moment'; + import './contributions.html'; + + + Template.message.helpers({ + + timestamp() { + const sentTime = moment(this.createdAt); + //if today, just show time, else if some other day, show date and time + if (sentTime.isSame(new Date(), "day")) { + return sentTime.format("h:mm a"); + } + return sentTime.format("M/D/YY h:mm a"); + }, + + getImage() { + const theImage = (Images.findOne(this.imageID)); + console.log("theImage" + theImage._id); + return theImage; + } + +}); + + + Template.chat.onCreated(function bodyOnCreated() { + + this.messagesSub = this.subscribe("messages"); //get messages + +}); + + Template.chat.onRendered(function bodyOnRendered() { + + const $messagesScroll = this.$('.messages-scroll'); + + //this is used to auto-scroll to new messages whenever they come in + + let initialized = false; + + this.autorun(() => { + if (this.messagesSub.ready()) { + Messages.find({recipient: "all"}, { fields: { _id: 1 } }).fetch(); + Tracker.afterFlush(() => { + //only auto-scroll if near the bottom already + if (!initialized || Math.abs($messagesScroll[0].scrollHeight - $messagesScroll.scrollTop() - $messagesScroll.outerHeight()) < 200) { + initialized = true; + $messagesScroll.stop().animate({ + scrollTop: $messagesScroll[0].scrollHeight + }); + } + }); + } + }); + +}); + + Template.chat.helpers({ + //get messages from database that have a recipient of either all or uid + messages() { + const uid = Meteor.userId(); + console.log("uid: " + uid) + // let m = async function() {return Messages.find({name: "Josh"}, { sort: { createdAt: 1 } })}; + // m.then((doc) => {}); + // return m + return Messages.find({ recipient: { $in: ["all", uid] } }, { sort: { createdAt: 1 } }); //most recent at the bottom + //return Messages.find({}, { sort: { createdAt: 1 } }); + }, + + hideHint() { + return (Cookie.get("hideHint")=="true"); //convert from string to boolean + } + +}); + + Template.chat.events({ + + //send message + + 'submit #message'(event, instance) { + + event.preventDefault(); + + const $el = $(event.currentTarget); + const $input = $el.find('.message-input'); + + //find the fileinput element? + //const $images = $el.find('.fileinput'); + //find the first and only picture that's been added + let picture; + const uid = Meteor.userId(); + const data = { message: $input.val() }; + + if (event.target.photo.files.length != 0) { + + picture = event.target.photo.files[0] + + const location = this.location ? this.location : {lat: null, lng: null}; + const iid = Router.current().params.iid; + const needName = Router.current().params.needName; + + const user = Meteor.user().username; + const timestamp = Date.now() + + const imageFile = Images.insert(picture, (err, imageFile) => { + //this is a callback for after the image is inserted + if (err) { + alert(err); + } else { + //success branch of callback + //add more info about the photo + Images.update({ _id: imageFile._id }, { + $set: { + iid: iid, + uid: uid, + lat: location.lat, + lng: location.lng, + needName: needName, + } + }, (err, docs) => { + if (err) { + console.log('upload error,', err); + } else { + } + }); + // TODO: setTimeout for automatically moving on if upload takes too long + + //watch to see when the image db has been updated, then go to results + const cursor = Images.find(imageFile._id).observe({ + changed(newImage) { + if (newImage.isUploaded()) { + cursor.stop(); + Router.go(resultsUrl); + } + } + }); + } + }); + + data.image = imageFile._id; + } + else { + data.image = ""; + } + + // const $setChar = $el.find('.character'); + // const $chapter = $el.find('.chapter'); + + + + //const userName = $setChar.text(); + + let participant = Meteor.users.findOne({ + "_id": uid, + }) + //const chapterID = $chapter.text(); + console.log("data: " + data); + console.log("first name: " + participant.profile.firstName); + //console.log("chapter: " + chapterID); + + data.name = uid; + + //data.role = ; + //data.chapterID = chapterID; + + Meteor.call("sendMessage", data, (error, response) => { + if (error) { + alert(error.reason); + } else { + //Cookie.set("name", response.name); + $input.val(""); + } + }); + + document.forms[0].elements[0].value = ''; + + }, + + /*playing with idea of having time passed be an event + 'time passed'(event, instance) { + event.preventDefault(); + + + + data.order = order + Meteor.call("sendPrompt", data, (error, response) => { + if (error) { + alert(error.reason); + } else { + //Cookie.set("name", response.name); + $input.val(""); + } + }); + + }, + */ + + //send prompt function + /* + let uid = //however we get the user's id + data = //info we get from uid + + */ + + //hide hint in the top right corner + + 'click .ready-button'(event, instance) { + event.preventDefault(); + + const $el = $(event.currentTarget); + }, + + 'click .hide-hint-button'(event, instance) { + + //cookies only understand strings + Cookie.set("hideHint", (Cookie.get("hideHint")=="true") ? "false" : "true"); + + } + + +}); \ No newline at end of file diff --git a/imports/ui/components/result_link.js b/imports/ui/components/result_link.js index b9c30979..d808ed63 100644 --- a/imports/ui/components/result_link.js +++ b/imports/ui/components/result_link.js @@ -1,6 +1,6 @@ import './result_link.html'; -import { Meteor } from 'meteor/meteor'; +// import { Meteor } from 'meteor/meteor'; import { Template } from 'meteor/templating'; import { SimpleSchema } from 'meteor/aldeed:simple-schema'; diff --git a/imports/ui/layout/header.html b/imports/ui/layout/header.html index b1e9e0e8..d1fefabb 100644 --- a/imports/ui/layout/header.html +++ b/imports/ui/layout/header.html @@ -2,7 +2,7 @@