From 73ab9c866ae17b8220bab62c755cc3290a8c6faa Mon Sep 17 00:00:00 2001 From: Andrew Mao Date: Sun, 3 Nov 2019 23:36:49 -0500 Subject: [PATCH 1/7] pre-decaffeinate staging --- lib/{common.js => shared.js} | 0 package.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename lib/{common.js => shared.js} (100%) diff --git a/lib/common.js b/lib/shared.js similarity index 100% rename from lib/common.js rename to lib/shared.js diff --git a/package.js b/package.js index 951e784..91c6a5c 100644 --- a/package.js +++ b/package.js @@ -64,7 +64,7 @@ Package.onUse(function (api) { // Shared files api.addFiles([ - 'lib/common.js', + 'lib/shared.js', 'lib/common.coffee', 'lib/util.coffee' ]); From aeec178fb93d1abd1a6d4a705283677015f687ee Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Mon, 4 Nov 2019 00:28:19 -0500 Subject: [PATCH 2/7] decaffeinate: Rename admin.coffee and 31 other files from .coffee to .js --- admin/{admin.coffee => admin.js} | 0 admin/{clientAdmin.coffee => clientAdmin.js} | 0 admin/{experimentAdmin.coffee => experimentAdmin.js} | 0 admin/{lobbyAdmin.coffee => lobbyAdmin.js} | 0 admin/{mturkAdmin.coffee => mturkAdmin.js} | 0 admin/{util.coffee => util.js} | 0 client/{dialogs.coffee => dialogs.js} | 0 client/{helpers.coffee => helpers.js} | 0 client/{lobby_client.coffee => lobby_client.js} | 0 client/{logging_client.coffee => logging_client.js} | 0 client/{login.coffee => login.js} | 0 client/{timers_client.coffee => timers_client.js} | 0 client/{ts_client.coffee => ts_client.js} | 0 lib/{common.coffee => common.js} | 0 lib/{util.coffee => util.js} | 0 server/{accounts_mturk.coffee => accounts_mturk.js} | 0 server/{batches.coffee => batches.js} | 0 server/{connections.coffee => connections.js} | 0 server/{lobby_server.coffee => lobby_server.js} | 0 server/{logging.coffee => logging.js} | 0 server/{turkserver.coffee => turkserver.js} | 0 tests/{admin_tests.coffee => admin_tests.js} | 0 tests/{assigner_tests.coffee => assigner_tests.js} | 0 tests/{auth_tests.coffee => auth_tests.js} | 0 tests/{connection_tests.coffee => connection_tests.js} | 0 ...{experiment_client_tests.coffee => experiment_client_tests.js} | 0 tests/{experiment_tests.coffee => experiment_tests.js} | 0 tests/{helper_tests.coffee => helper_tests.js} | 0 tests/{lobby_tests.coffee => lobby_tests.js} | 0 tests/{logging_tests.coffee => logging_tests.js} | 0 tests/{timer_tests.coffee => timer_tests.js} | 0 tests/{utils.coffee => utils.js} | 0 32 files changed, 0 insertions(+), 0 deletions(-) rename admin/{admin.coffee => admin.js} (100%) rename admin/{clientAdmin.coffee => clientAdmin.js} (100%) rename admin/{experimentAdmin.coffee => experimentAdmin.js} (100%) rename admin/{lobbyAdmin.coffee => lobbyAdmin.js} (100%) rename admin/{mturkAdmin.coffee => mturkAdmin.js} (100%) rename admin/{util.coffee => util.js} (100%) rename client/{dialogs.coffee => dialogs.js} (100%) rename client/{helpers.coffee => helpers.js} (100%) rename client/{lobby_client.coffee => lobby_client.js} (100%) rename client/{logging_client.coffee => logging_client.js} (100%) rename client/{login.coffee => login.js} (100%) rename client/{timers_client.coffee => timers_client.js} (100%) rename client/{ts_client.coffee => ts_client.js} (100%) rename lib/{common.coffee => common.js} (100%) rename lib/{util.coffee => util.js} (100%) rename server/{accounts_mturk.coffee => accounts_mturk.js} (100%) rename server/{batches.coffee => batches.js} (100%) rename server/{connections.coffee => connections.js} (100%) rename server/{lobby_server.coffee => lobby_server.js} (100%) rename server/{logging.coffee => logging.js} (100%) rename server/{turkserver.coffee => turkserver.js} (100%) rename tests/{admin_tests.coffee => admin_tests.js} (100%) rename tests/{assigner_tests.coffee => assigner_tests.js} (100%) rename tests/{auth_tests.coffee => auth_tests.js} (100%) rename tests/{connection_tests.coffee => connection_tests.js} (100%) rename tests/{experiment_client_tests.coffee => experiment_client_tests.js} (100%) rename tests/{experiment_tests.coffee => experiment_tests.js} (100%) rename tests/{helper_tests.coffee => helper_tests.js} (100%) rename tests/{lobby_tests.coffee => lobby_tests.js} (100%) rename tests/{logging_tests.coffee => logging_tests.js} (100%) rename tests/{timer_tests.coffee => timer_tests.js} (100%) rename tests/{utils.coffee => utils.js} (100%) diff --git a/admin/admin.coffee b/admin/admin.js similarity index 100% rename from admin/admin.coffee rename to admin/admin.js diff --git a/admin/clientAdmin.coffee b/admin/clientAdmin.js similarity index 100% rename from admin/clientAdmin.coffee rename to admin/clientAdmin.js diff --git a/admin/experimentAdmin.coffee b/admin/experimentAdmin.js similarity index 100% rename from admin/experimentAdmin.coffee rename to admin/experimentAdmin.js diff --git a/admin/lobbyAdmin.coffee b/admin/lobbyAdmin.js similarity index 100% rename from admin/lobbyAdmin.coffee rename to admin/lobbyAdmin.js diff --git a/admin/mturkAdmin.coffee b/admin/mturkAdmin.js similarity index 100% rename from admin/mturkAdmin.coffee rename to admin/mturkAdmin.js diff --git a/admin/util.coffee b/admin/util.js similarity index 100% rename from admin/util.coffee rename to admin/util.js diff --git a/client/dialogs.coffee b/client/dialogs.js similarity index 100% rename from client/dialogs.coffee rename to client/dialogs.js diff --git a/client/helpers.coffee b/client/helpers.js similarity index 100% rename from client/helpers.coffee rename to client/helpers.js diff --git a/client/lobby_client.coffee b/client/lobby_client.js similarity index 100% rename from client/lobby_client.coffee rename to client/lobby_client.js diff --git a/client/logging_client.coffee b/client/logging_client.js similarity index 100% rename from client/logging_client.coffee rename to client/logging_client.js diff --git a/client/login.coffee b/client/login.js similarity index 100% rename from client/login.coffee rename to client/login.js diff --git a/client/timers_client.coffee b/client/timers_client.js similarity index 100% rename from client/timers_client.coffee rename to client/timers_client.js diff --git a/client/ts_client.coffee b/client/ts_client.js similarity index 100% rename from client/ts_client.coffee rename to client/ts_client.js diff --git a/lib/common.coffee b/lib/common.js similarity index 100% rename from lib/common.coffee rename to lib/common.js diff --git a/lib/util.coffee b/lib/util.js similarity index 100% rename from lib/util.coffee rename to lib/util.js diff --git a/server/accounts_mturk.coffee b/server/accounts_mturk.js similarity index 100% rename from server/accounts_mturk.coffee rename to server/accounts_mturk.js diff --git a/server/batches.coffee b/server/batches.js similarity index 100% rename from server/batches.coffee rename to server/batches.js diff --git a/server/connections.coffee b/server/connections.js similarity index 100% rename from server/connections.coffee rename to server/connections.js diff --git a/server/lobby_server.coffee b/server/lobby_server.js similarity index 100% rename from server/lobby_server.coffee rename to server/lobby_server.js diff --git a/server/logging.coffee b/server/logging.js similarity index 100% rename from server/logging.coffee rename to server/logging.js diff --git a/server/turkserver.coffee b/server/turkserver.js similarity index 100% rename from server/turkserver.coffee rename to server/turkserver.js diff --git a/tests/admin_tests.coffee b/tests/admin_tests.js similarity index 100% rename from tests/admin_tests.coffee rename to tests/admin_tests.js diff --git a/tests/assigner_tests.coffee b/tests/assigner_tests.js similarity index 100% rename from tests/assigner_tests.coffee rename to tests/assigner_tests.js diff --git a/tests/auth_tests.coffee b/tests/auth_tests.js similarity index 100% rename from tests/auth_tests.coffee rename to tests/auth_tests.js diff --git a/tests/connection_tests.coffee b/tests/connection_tests.js similarity index 100% rename from tests/connection_tests.coffee rename to tests/connection_tests.js diff --git a/tests/experiment_client_tests.coffee b/tests/experiment_client_tests.js similarity index 100% rename from tests/experiment_client_tests.coffee rename to tests/experiment_client_tests.js diff --git a/tests/experiment_tests.coffee b/tests/experiment_tests.js similarity index 100% rename from tests/experiment_tests.coffee rename to tests/experiment_tests.js diff --git a/tests/helper_tests.coffee b/tests/helper_tests.js similarity index 100% rename from tests/helper_tests.coffee rename to tests/helper_tests.js diff --git a/tests/lobby_tests.coffee b/tests/lobby_tests.js similarity index 100% rename from tests/lobby_tests.coffee rename to tests/lobby_tests.js diff --git a/tests/logging_tests.coffee b/tests/logging_tests.js similarity index 100% rename from tests/logging_tests.coffee rename to tests/logging_tests.js diff --git a/tests/timer_tests.coffee b/tests/timer_tests.js similarity index 100% rename from tests/timer_tests.coffee rename to tests/timer_tests.js diff --git a/tests/utils.coffee b/tests/utils.js similarity index 100% rename from tests/utils.coffee rename to tests/utils.js From 6238bb1d3b6db8dba41f59764a4f5ff38a4717e1 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Mon, 4 Nov 2019 00:28:29 -0500 Subject: [PATCH 3/7] decaffeinate: Convert admin.coffee and 31 other files to JS --- admin/admin.js | 1003 ++++++++++++++------------ admin/clientAdmin.js | 466 +++++++------ admin/experimentAdmin.js | 522 ++++++++------ admin/lobbyAdmin.js | 31 +- admin/mturkAdmin.js | 934 ++++++++++++++----------- admin/util.js | 321 +++++---- client/dialogs.js | 167 +++-- client/helpers.js | 78 ++- client/lobby_client.js | 125 ++-- client/logging_client.js | 8 +- client/login.js | 285 ++++---- client/timers_client.js | 247 ++++--- client/ts_client.js | 86 ++- lib/common.js | 136 ++-- lib/util.js | 43 +- server/accounts_mturk.js | 242 ++++--- server/batches.js | 143 ++-- server/connections.js | 409 ++++++----- server/lobby_server.js | 245 ++++--- server/logging.js | 60 +- server/turkserver.js | 268 +++---- tests/admin_tests.js | 334 +++++---- tests/assigner_tests.js | 1124 ++++++++++++++++-------------- tests/auth_tests.js | 746 +++++++++++--------- tests/connection_tests.js | 433 +++++++----- tests/experiment_client_tests.js | 537 +++++++------- tests/experiment_tests.js | 902 +++++++++++++----------- tests/helper_tests.js | 49 +- tests/lobby_tests.js | 240 ++++--- tests/logging_tests.js | 125 ++-- tests/timer_tests.js | 254 ++++--- tests/utils.js | 113 +-- 32 files changed, 5958 insertions(+), 4718 deletions(-) diff --git a/admin/admin.js b/admin/admin.js index 3153e4f..c911e9b 100644 --- a/admin/admin.js +++ b/admin/admin.js @@ -1,16 +1,24 @@ -# Server admin code -isAdmin = (userId) -> userId? and Meteor.users.findOne(userId)?.admin - -# Only admin gets server facts -Facts.setUserIdFilter(isAdmin) - -### +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +// Server admin code +const isAdmin = userId => (userId != null) && __guard__(Meteor.users.findOne(userId), x => x.admin); + +// Only admin gets server facts +Facts.setUserIdFilter(isAdmin); + +/* TODO eliminate unnecessary fields sent over below -### +*/ -# Publish all admin data for /turkserver -Meteor.publish "tsAdmin", -> - return [] unless isAdmin(@userId) +// Publish all admin data for /turkserver +Meteor.publish("tsAdmin", function() { + if (!isAdmin(this.userId)) { return []; } return [ Batches.find(), @@ -18,573 +26,638 @@ Meteor.publish "tsAdmin", -> Qualifications.find(), HITTypes.find(), HITs.find(), - ] - -userFindOptions = - fields: - status: 1 - turkserver: 1 - username: 1 + ]; +}); + +const userFindOptions = { + fields: { + status: 1, + turkserver: 1, + username: 1, workerId: 1 - -Meteor.publish "tsAdminUsers", (groupId) -> - return [] unless isAdmin(@userId) - - # When in a group, override whatever user publication the group sends with our fields - # TODO Don't publish all users for /turkserver - return Meteor.users.find({}, userFindOptions) - -# Don't return status here as the user is not connected to this experiment -offlineFindOptions = - fields: - turkserver: 1 - username: 1 + } +}; + +Meteor.publish("tsAdminUsers", function(groupId) { + if (!isAdmin(this.userId)) { return []; } + + // When in a group, override whatever user publication the group sends with our fields + // TODO Don't publish all users for /turkserver + return Meteor.users.find({}, userFindOptions); +}); + +// Don't return status here as the user is not connected to this experiment +const offlineFindOptions = { + fields: { + turkserver: 1, + username: 1, workerId: 1 - -# Helper publish function to get users for experiments that have ended. -# Necessary to watch completed experiments. -Meteor.publish "tsGroupUsers", (groupId) -> - return [] unless isAdmin(@userId) - - exp = Experiments.findOne(groupId) - return [] unless exp - expUsers = exp.users || [] - - # This won't update if users changes, but it shouldn't after an experiment is completed - # TODO Just return everything here; we don't know what the app subscription was using - return Meteor.users.find({ _id: $in: expUsers}, offlineFindOptions) - -# Get a date that is `days` away from `date`, locked to day boundaries -# See https://kadira.io/academy/improve-cpu-and-network-usage/ -getDateFloor = (date, days) -> - timestamp = date.valueOf() - closestDay = timestamp - (timestamp % (24 * 3600 * 1000)) - return new Date(closestDay + days * 24 * 3600 * 1000) - -# Data for a single worker -Meteor.publish "tsAdminWorkerData", (workerId) -> - return [] unless isAdmin(@userId) - check(workerId, String) - - # TODO also return users here if they are not all published + } +}; + +// Helper publish function to get users for experiments that have ended. +// Necessary to watch completed experiments. +Meteor.publish("tsGroupUsers", function(groupId) { + if (!isAdmin(this.userId)) { return []; } + + const exp = Experiments.findOne(groupId); + if (!exp) { return []; } + const expUsers = exp.users || []; + + // This won't update if users changes, but it shouldn't after an experiment is completed + // TODO Just return everything here; we don't know what the app subscription was using + return Meteor.users.find({ _id: {$in: expUsers}}, offlineFindOptions); +}); + +// Get a date that is `days` away from `date`, locked to day boundaries +// See https://kadira.io/academy/improve-cpu-and-network-usage/ +const getDateFloor = function(date, days) { + const timestamp = date.valueOf(); + const closestDay = timestamp - (timestamp % (24 * 3600 * 1000)); + return new Date(closestDay + (days * 24 * 3600 * 1000)); +}; + +// Data for a single worker +Meteor.publish("tsAdminWorkerData", function(workerId) { + if (!isAdmin(this.userId)) { return []; } + check(workerId, String); + + // TODO also return users here if they are not all published return [ Workers.find(workerId), Assignments.find({workerId}) - ] + ]; +}); -Meteor.publish "tsAdminWorkers", -> - return [] unless isAdmin(@userId) +Meteor.publish("tsAdminWorkers", function() { + if (!isAdmin(this.userId)) { return []; } return [ Workers.find(), WorkerEmails.find() - ] + ]; +}); -Meteor.publish "tsAdminActiveAssignments", (batchId) -> - return [] unless isAdmin(@userId) - check(batchId, String) +Meteor.publish("tsAdminActiveAssignments", function(batchId) { + if (!isAdmin(this.userId)) { return []; } + check(batchId, String); - # TODO this isn't fully indexed + // TODO this isn't fully indexed return Assignments.find({ batchId, submitTime: null, status: "assigned" - }) + }); +}); -Meteor.publish "tsAdminCompletedAssignments", (batchId, days, limit) -> - return [] unless isAdmin(@userId) - check(batchId, String) - check(days, Number) - check(limit, Number) +Meteor.publish("tsAdminCompletedAssignments", function(batchId, days, limit) { + if (!isAdmin(this.userId)) { return []; } + check(batchId, String); + check(days, Number); + check(limit, Number); - threshold = getDateFloor(new Date, -days) + const threshold = getDateFloor(new Date, -days); - # effectively { status: "completed" } but there is an index on submitTime + // effectively { status: "completed" } but there is an index on submitTime return Assignments.find({ batchId, submitTime: { $gte: threshold } }, { - sort: { submitTime: -1 } - limit: limit - }) - -# Publish a single instance to the admin. -Meteor.publish "tsAdminInstance", (instance) -> - return [] unless isAdmin(@userId) - check(instance, String) - return Experiments.find(instance) - -# Two separate publications for running and completed experiments, because -# it's hard to do both endTime: null and endTime > some date while sorting by -# endTime desc, because null sorts below any value. -Meteor.publish "tsAdminBatchRunningExperiments", (batchId) -> - return [] unless isAdmin(@userId) - check(batchId, String) - - return Experiments.find({batchId, endTime: null}) - -Meteor.publish "tsAdminBatchCompletedExperiments", (batchId, days, limit) -> - return [] unless isAdmin(@userId) - check(batchId, String) - check(days, Number) - check(limit, Number) - - threshold = getDateFloor(new Date, -days) + sort: { submitTime: -1 }, + limit + }); +}); + +// Publish a single instance to the admin. +Meteor.publish("tsAdminInstance", function(instance) { + if (!isAdmin(this.userId)) { return []; } + check(instance, String); + return Experiments.find(instance); +}); + +// Two separate publications for running and completed experiments, because +// it's hard to do both endTime: null and endTime > some date while sorting by +// endTime desc, because null sorts below any value. +Meteor.publish("tsAdminBatchRunningExperiments", function(batchId) { + if (!isAdmin(this.userId)) { return []; } + check(batchId, String); + + return Experiments.find({batchId, endTime: null}); +}); + +Meteor.publish("tsAdminBatchCompletedExperiments", function(batchId, days, limit) { + if (!isAdmin(this.userId)) { return []; } + check(batchId, String); + check(days, Number); + check(limit, Number); + + const threshold = getDateFloor(new Date, -days); return Experiments.find({ batchId, endTime: { $gte: threshold } }, { - sort: { endTime: -1 } - limit: limit - }) + sort: { endTime: -1 }, + limit + }); +}); -Meteor.publish "tsGroupLogs", (groupId, limit) -> - return [] unless isAdmin(@userId) +Meteor.publish("tsGroupLogs", function(groupId, limit) { + if (!isAdmin(this.userId)) { return []; } return [ Experiments.find(groupId), Logs.find({_groupId: groupId}, { sort: {_timestamp: -1}, - limit: limit + limit }) - ] - -# Get a HIT Type and make sure it is ready for use -getAndCheckHitType = (hitTypeId) -> - hitType = HITTypes.findOne(HITTypeId: hitTypeId) - throw new Meteor.Error(403, "HITType not registered") unless hitType.HITTypeId - batch = Batches.findOne(hitType.batchId) - throw new Meteor.Error(403, "Batch not active; activate it first") unless batch.active - return hitType - -Meteor.methods - "ts-admin-account-balance": -> - TurkServer.checkAdmin() - try - return TurkServer.mturk "GetAccountBalance", {} - catch e - throw new Meteor.Error(403, e.toString()) - - # This is the only method that uses the _id field of HITType instead of HITTypeId. - "ts-admin-register-hittype": (hitType_id) -> - TurkServer.checkAdmin() - # Build up the params to register the HIT Type - params = HITTypes.findOne(hitType_id) - delete params._id - delete params.batchId - - params.Reward = - Amount: params.Reward + ]; +}); + +// Get a HIT Type and make sure it is ready for use +const getAndCheckHitType = function(hitTypeId) { + const hitType = HITTypes.findOne({HITTypeId: hitTypeId}); + if (!hitType.HITTypeId) { throw new Meteor.Error(403, "HITType not registered"); } + const batch = Batches.findOne(hitType.batchId); + if (!batch.active) { throw new Meteor.Error(403, "Batch not active; activate it first"); } + return hitType; +}; + +Meteor.methods({ + "ts-admin-account-balance"() { + TurkServer.checkAdmin(); + try { + return TurkServer.mturk("GetAccountBalance", {}); + } catch (e) { + throw new Meteor.Error(403, e.toString()); + } + }, + + // This is the only method that uses the _id field of HITType instead of HITTypeId. + "ts-admin-register-hittype"(hitType_id) { + TurkServer.checkAdmin(); + // Build up the params to register the HIT Type + const params = HITTypes.findOne(hitType_id); + delete params._id; + delete params.batchId; + + params.Reward = { + Amount: params.Reward, CurrencyCode: "USD" + }; - quals = [] - for i, qualId of params.QualificationRequirement - qual = Qualifications.findOne(qualId) - delete qual._id - delete qual.name + const quals = []; + for (let i in params.QualificationRequirement) { + const qualId = params.QualificationRequirement[i]; + const qual = Qualifications.findOne(qualId); + delete qual._id; + delete qual.name; - # Integer value is fine as array or not, but - # Get the locale into its weird structure - if Array.isArray(qual.LocaleValue) - qual.LocaleValue = ({ Country: locale } for locale in qual.LocaleValue) - else if qual.LocaleValue - qual.LocaleValue = { Country: qual.LocaleValue } + // Integer value is fine as array or not, but + // Get the locale into its weird structure + if (Array.isArray(qual.LocaleValue)) { + qual.LocaleValue = (Array.from(qual.LocaleValue).map((locale) => ({ Country: locale }))); + } else if (qual.LocaleValue) { + qual.LocaleValue = { Country: qual.LocaleValue }; + } - quals.push qual + quals.push(qual); + } - params.QualificationRequirement = quals + params.QualificationRequirement = quals; - hitTypeId = null - try - hitTypeId = TurkServer.mturk "RegisterHITType", params - catch e - throw new Meteor.Error(500, e.toString()) + let hitTypeId = null; + try { + hitTypeId = TurkServer.mturk("RegisterHITType", params); + } catch (e) { + throw new Meteor.Error(500, e.toString()); + } - HITTypes.update hitType_id, - $set: {HITTypeId: hitTypeId} - return + HITTypes.update(hitType_id, + {$set: {HITTypeId: hitTypeId}}); + }, - "ts-admin-create-hit": (hitTypeId, params) -> - TurkServer.checkAdmin() + "ts-admin-create-hit"(hitTypeId, params) { + TurkServer.checkAdmin(); - hitType = getAndCheckHitType(hitTypeId) + const hitType = getAndCheckHitType(hitTypeId); - params.HITTypeId = hitType.HITTypeId + params.HITTypeId = hitType.HITTypeId; params.Question = - """ - #{TurkServer.config.mturk.externalUrl}?batchId=#{hitType.batchId} - #{TurkServer.config.mturk.frameHeight} - - """ - - hitId = null - try - hitId = TurkServer.mturk "CreateHIT", params - catch e - throw new Meteor.Error(500, e.toString()) - - HITs.insert - HITId: hitId + ` + ${TurkServer.config.mturk.externalUrl}?batchId=${hitType.batchId} + ${TurkServer.config.mturk.frameHeight} +\ +`; + + let hitId = null; + try { + hitId = TurkServer.mturk("CreateHIT", params); + } catch (e) { + throw new Meteor.Error(500, e.toString()); + } + + HITs.insert({ + HITId: hitId, HITTypeId: hitType.HITTypeId + }); - this.unblock() - # Immediately refresh HIT data after creation - Meteor.call("ts-admin-refresh-hit", hitId) - - return + this.unblock(); + // Immediately refresh HIT data after creation + Meteor.call("ts-admin-refresh-hit", hitId); - "ts-admin-refresh-hit": (HITId) -> - TurkServer.checkAdmin() - throw new Meteor.Error(400, "HIT ID not specified") unless HITId - try - hitData = TurkServer.mturk "GetHIT", HITId: HITId - HITs.update {HITId: HITId}, {$set: hitData} - catch e - throw new Meteor.Error(500, e.toString()) + }, + + "ts-admin-refresh-hit"(HITId) { + TurkServer.checkAdmin(); + if (!HITId) { throw new Meteor.Error(400, "HIT ID not specified"); } + try { + const hitData = TurkServer.mturk("GetHIT", {HITId}); + HITs.update({HITId}, {$set: hitData}); + } catch (e) { + throw new Meteor.Error(500, e.toString()); + } - return + }, - "ts-admin-expire-hit": (HITId) -> - TurkServer.checkAdmin() - throw new Meteor.Error(400, "HIT ID not specified") unless HITId - try - hitData = TurkServer.mturk "ForceExpireHIT", HITId: HITId + "ts-admin-expire-hit"(HITId) { + TurkServer.checkAdmin(); + if (!HITId) { throw new Meteor.Error(400, "HIT ID not specified"); } + try { + const hitData = TurkServer.mturk("ForceExpireHIT", {HITId}); - @unblock() # If successful, refresh the HIT - Meteor.call "ts-admin-refresh-hit", HITId - catch e - throw new Meteor.Error(500, e.toString()) + this.unblock(); // If successful, refresh the HIT + Meteor.call("ts-admin-refresh-hit", HITId); + } catch (e) { + throw new Meteor.Error(500, e.toString()); + } - return + }, - "ts-admin-change-hittype": (params) -> - TurkServer.checkAdmin() - check(params.HITId, String) - check(params.HITTypeId, String) + "ts-admin-change-hittype"(params) { + TurkServer.checkAdmin(); + check(params.HITId, String); + check(params.HITTypeId, String); - # TODO: don't allow change if the old HIT Type has a different batchId from the new one - try - TurkServer.mturk "ChangeHITTypeOfHIT", params - @unblock() # If successful, refresh the HIT - Meteor.call "ts-admin-refresh-hit", params.HITId - catch e - throw new Meteor.Error(500, e.toString()) + // TODO: don't allow change if the old HIT Type has a different batchId from the new one + try { + TurkServer.mturk("ChangeHITTypeOfHIT", params); + this.unblock(); // If successful, refresh the HIT + Meteor.call("ts-admin-refresh-hit", params.HITId); + } catch (e) { + throw new Meteor.Error(500, e.toString()); + } - return + }, - "ts-admin-extend-hit": (params) -> - TurkServer.checkAdmin() - check(params.HITId, String) + "ts-admin-extend-hit"(params) { + TurkServer.checkAdmin(); + check(params.HITId, String); - hit = HITs.findOne(HITId: params.HITId) + const hit = HITs.findOne({HITId: params.HITId}); - getAndCheckHitType(hit.HITTypeId) + getAndCheckHitType(hit.HITTypeId); - try - TurkServer.mturk "ExtendHIT", params + try { + TurkServer.mturk("ExtendHIT", params); - @unblock() # If successful, refresh the HIT - Meteor.call "ts-admin-refresh-hit", params.HITId - catch e - throw new Meteor.Error(500, e.toString()) + this.unblock(); // If successful, refresh the HIT + Meteor.call("ts-admin-refresh-hit", params.HITId); + } catch (e) { + throw new Meteor.Error(500, e.toString()); + } - return + }, - "ts-admin-lobby-event": (batchId, event) -> - TurkServer.checkAdmin() - check(batchId, String) + "ts-admin-lobby-event"(batchId, event) { + TurkServer.checkAdmin(); + check(batchId, String); - batch = TurkServer.Batch.getBatch(batchId) - throw new Meteor.Error(500, "Batch #{batchId} does not exist") unless batch? - emitter = batch.lobby.events - emitter.emit.apply(emitter, Array::slice.call(arguments, 1)) # Event and any other arguments - return + const batch = TurkServer.Batch.getBatch(batchId); + if (batch == null) { throw new Meteor.Error(500, `Batch ${batchId} does not exist`); } + const emitter = batch.lobby.events; + emitter.emit.apply(emitter, Array.prototype.slice.call(arguments, 1)); // Event and any other arguments + }, - "ts-admin-create-message": (subject, message, copyFromId) -> - TurkServer.checkAdmin() - check(subject, String) - check(message, String) + "ts-admin-create-message"(subject, message, copyFromId) { + let recipients; + TurkServer.checkAdmin(); + check(subject, String); + check(message, String); - if copyFromId? - recipients = WorkerEmails.findOne(copyFromId)?.recipients + if (copyFromId != null) { + recipients = __guard__(WorkerEmails.findOne(copyFromId), x => x.recipients); + } - recipients ?= [] + if (recipients == null) { recipients = []; } - return WorkerEmails.insert({ subject, message, recipients }) + return WorkerEmails.insert({ subject, message, recipients }); + }, - "ts-admin-send-message": (emailId) -> - TurkServer.checkAdmin() - check(emailId, String) + "ts-admin-send-message"(emailId) { + TurkServer.checkAdmin(); + check(emailId, String); - email = WorkerEmails.findOne(emailId) - throw new Error(403, "Message already sent") if email.sentTime? + const email = WorkerEmails.findOne(emailId); + if (email.sentTime != null) { throw new Error(403, "Message already sent"); } - recipients = email.recipients + const { + recipients + } = email; - check(email.subject, String) - check(email.message, String) - check(recipients, Array) + check(email.subject, String); + check(email.message, String); + check(recipients, Array); + + if (recipients.length === 0) { throw new Error(403, "No recipients on e-mail"); } - throw new Error(403, "No recipients on e-mail") if recipients.length is 0 + let count = 0; - count = 0 + while (recipients.length > 0) { + // Notify workers 50 at a time + const chunk = recipients.splice(0, 50); - while recipients.length > 0 - # Notify workers 50 at a time - chunk = recipients.splice(0, 50) - - params = - Subject: email.subject - MessageText: email.message + const params = { + Subject: email.subject, + MessageText: email.message, WorkerId: chunk + }; - try - TurkServer.mturk "NotifyWorkers", params - catch e - throw new Meteor.Error(500, e.toString()) + try { + TurkServer.mturk("NotifyWorkers", params); + } catch (e) { + throw new Meteor.Error(500, e.toString()); + } - count += chunk.length - Meteor._debug(count + " workers notified") + count += chunk.length; + Meteor._debug(count + " workers notified"); - # Record which workers got the e-mail in case something breaks - Workers.update({_id: $in: chunk}, { + // Record which workers got the e-mail in case something breaks + Workers.update({_id: {$in: chunk}}, { $push: {emailsReceived: emailId} - }, {multi: true}) - - # Record date that this was sent - WorkerEmails.update emailId, - $set: sentTime: new Date - - return "#{count} workers notified." - - # TODO implement this - "ts-admin-resend-message": (emailId) -> - TurkServer.checkAdmin() - check(emailId, String) - - throw new Meteor.Error(500, "Not implemented") - - "ts-admin-copy-message": (emailId) -> - TurkServer.checkAdmin() - check(emailId, String) - - email = WorkerEmails.findOne(emailId) - return WorkerEmails.insert - subject: email.subject - message: email.message - recipients: [] - - "ts-admin-delete-message": (emailId) -> - TurkServer.checkAdmin() - check(emailId, String) - - email = WorkerEmails.findOne(emailId) - throw new Meteor.Error(403, "Email has already been sent") if email.sentTime - - WorkerEmails.remove(emailId) - return - - "ts-admin-cleanup-user-state": -> - TurkServer.checkAdmin() - # Find all users that are state: experiment but don't have an active assignment - # This shouldn't have to be used in most cases - Meteor.users.find({"turkserver.state": "experiment"}).map (user) -> - return if TurkServer.Assignment.getCurrentUserAssignment(user._id)? - Meteor.users.update user._id, - $unset: "turkserver.state": null - - return - - "ts-admin-cancel-assignments": (batchId) -> - TurkServer.checkAdmin() - check(batchId, String) - - count = 0 - Assignments.find({batchId, status: "assigned"}).map (asst) -> - user = Meteor.users.findOne({workerId: asst.workerId}) - return if user.status?.online - tsAsst = TurkServer.Assignment.getAssignment(asst._id) - tsAsst.setReturned() - - # if they were disconnected in the middle of an experiment, - # and the experiment was either never torndown, - # or torndown with returnToLobby = false - if ( userGroup = Partitioner.getUserGroup(user._id) )? + }, {multi: true}); + } + + // Record date that this was sent + WorkerEmails.update(emailId, + {$set: {sentTime: new Date}}); + + return `${count} workers notified.`; + }, + + // TODO implement this + "ts-admin-resend-message"(emailId) { + TurkServer.checkAdmin(); + check(emailId, String); + + throw new Meteor.Error(500, "Not implemented"); + }, + + "ts-admin-copy-message"(emailId) { + TurkServer.checkAdmin(); + check(emailId, String); + + const email = WorkerEmails.findOne(emailId); + return WorkerEmails.insert({ + subject: email.subject, + message: email.message, + recipients: []}); + }, + + "ts-admin-delete-message"(emailId) { + TurkServer.checkAdmin(); + check(emailId, String); + + const email = WorkerEmails.findOne(emailId); + if (email.sentTime) { throw new Meteor.Error(403, "Email has already been sent"); } + + WorkerEmails.remove(emailId); + }, + + "ts-admin-cleanup-user-state"() { + TurkServer.checkAdmin(); + // Find all users that are state: experiment but don't have an active assignment + // This shouldn't have to be used in most cases + Meteor.users.find({"turkserver.state": "experiment"}).map(function(user) { + if (TurkServer.Assignment.getCurrentUserAssignment(user._id) != null) { return; } + return Meteor.users.update(user._id, + {$unset: {"turkserver.state": null}}); + }); + + }, + + "ts-admin-cancel-assignments"(batchId) { + TurkServer.checkAdmin(); + check(batchId, String); + + let count = 0; + Assignments.find({batchId, status: "assigned"}).map(function(asst) { + let userGroup; + const user = Meteor.users.findOne({workerId: asst.workerId}); + if (user.status != null ? user.status.online : undefined) { return; } + const tsAsst = TurkServer.Assignment.getAssignment(asst._id); + tsAsst.setReturned(); + + // if they were disconnected in the middle of an experiment, + // and the experiment was either never torndown, + // or torndown with returnToLobby = false + if (( userGroup = Partitioner.getUserGroup(user._id) ) != null) { tsAsst._leaveInstance(userGroup); Partitioner.clearUserGroup(user._id); + } - count++ + return count++; + }); - return "#{count} assignments canceled." + return `${count} assignments canceled.`; + }, - # Refresh all assignments in a batch that are either unknown or submitted - "ts-admin-refresh-assignments": (batchId) -> - TurkServer.checkAdmin() - check(batchId, String) + // Refresh all assignments in a batch that are either unknown or submitted + "ts-admin-refresh-assignments"(batchId) { + TurkServer.checkAdmin(); + check(batchId, String); - # We may encounter more than one error when refreshing a bunch of - # assignments. This allows things to continue as much as possible, but - # will throw the first error encountered. - err = null + // We may encounter more than one error when refreshing a bunch of + // assignments. This allows things to continue as much as possible, but + // will throw the first error encountered. + let err = null; Assignments.find({ - batchId: batchId - status: "completed" + batchId, + status: "completed", mturkStatus: { $in: [null, "Submitted"] } - }).forEach (a) -> - asst = TurkServer.Assignment.getAssignment(a._id) - # Refresh submitted assignments as they may have been auto-approved - try - asst.refreshStatus() - catch e - err ?= e + }).forEach(function(a) { + const asst = TurkServer.Assignment.getAssignment(a._id); + // Refresh submitted assignments as they may have been auto-approved + try { + return asst.refreshStatus(); + } catch (e) { + return err != null ? err : (err = e); + } + }); - throw err if err? - return + if (err != null) { throw err; } + }, - "ts-admin-refresh-assignment": (asstId) -> - TurkServer.checkAdmin() - check(asstId, String) + "ts-admin-refresh-assignment"(asstId) { + TurkServer.checkAdmin(); + check(asstId, String); - TurkServer.Assignment.getAssignment(asstId).refreshStatus() - return + TurkServer.Assignment.getAssignment(asstId).refreshStatus(); + }, - "ts-admin-approve-assignment": (asstId, msg) -> - TurkServer.checkAdmin() - check(asstId, String) + "ts-admin-approve-assignment"(asstId, msg) { + TurkServer.checkAdmin(); + check(asstId, String); - TurkServer.Assignment.getAssignment(asstId).approve(msg) - return + TurkServer.Assignment.getAssignment(asstId).approve(msg); + }, - "ts-admin-reject-assignment": (asstId, msg) -> - TurkServer.checkAdmin() - check(asstId, String) + "ts-admin-reject-assignment"(asstId, msg) { + TurkServer.checkAdmin(); + check(asstId, String); - TurkServer.Assignment.getAssignment(asstId).reject(msg) - return + TurkServer.Assignment.getAssignment(asstId).reject(msg); + }, - # Count number of submitted assignments in a batch - "ts-admin-count-submitted": (batchId) -> - TurkServer.checkAdmin() - check(batchId, String) + // Count number of submitted assignments in a batch + "ts-admin-count-submitted"(batchId) { + TurkServer.checkAdmin(); + check(batchId, String); - # First refresh everything - Meteor.call "ts-admin-refresh-assignments", batchId + // First refresh everything + Meteor.call("ts-admin-refresh-assignments", batchId); return Assignments.find({ - batchId: batchId + batchId, mturkStatus: "Submitted" - }).count() + }).count(); + }, - # Approve all submitted assignments in a batch - "ts-admin-approve-all": (batchId, msg) -> - TurkServer.checkAdmin() - check(batchId, String) + // Approve all submitted assignments in a batch + "ts-admin-approve-all"(batchId, msg) { + TurkServer.checkAdmin(); + check(batchId, String); - Assignments.find({ - batchId: batchId + return Assignments.find({ + batchId, mturkStatus: "Submitted" - }).forEach (asst) -> - TurkServer.Assignment.getAssignment(asst._id).approve(msg); + }).forEach(asst => TurkServer.Assignment.getAssignment(asst._id).approve(msg)); + }, - # Count number of unpaid bonuses in a batch - "ts-admin-count-unpaid-bonuses": (batchId) -> - TurkServer.checkAdmin() - check(batchId, String) + // Count number of unpaid bonuses in a batch + "ts-admin-count-unpaid-bonuses"(batchId) { + TurkServer.checkAdmin(); + check(batchId, String); - # First refresh everything - Meteor.call "ts-admin-refresh-assignments", batchId + // First refresh everything + Meteor.call("ts-admin-refresh-assignments", batchId); - result = - numPaid: 0 + const result = { + numPaid: 0, amt: 0 + }; Assignments.find({ - batchId: batchId - mturkStatus: "Approved" - bonusPayment: {$gt: 0} + batchId, + mturkStatus: "Approved", + bonusPayment: {$gt: 0}, bonusPaid: {$exists: false} - }).forEach (asst) -> - result.numPaid += 1 - result.amt += asst.bonusPayment + }).forEach(function(asst) { + result.numPaid += 1; + return result.amt += asst.bonusPayment; + }); - return result + return result; + }, - # Pay all unpaid bonuses in a batch - "ts-admin-pay-bonuses": (batchId, msg) -> - TurkServer.checkAdmin() - check(batchId, String) + // Pay all unpaid bonuses in a batch + "ts-admin-pay-bonuses"(batchId, msg) { + TurkServer.checkAdmin(); + check(batchId, String); Assignments.find({ - batchId: batchId - mturkStatus: "Approved" - bonusPayment: {$gt: 0} + batchId, + mturkStatus: "Approved", + bonusPayment: {$gt: 0}, bonusPaid: {$exists: false} - }).forEach (asst) -> - TurkServer.Assignment.getAssignment(asst._id).payBonus(msg) - - return - - "ts-admin-unset-bonus": (asstId) -> - TurkServer.checkAdmin() - check(asstId, String) - - TurkServer.Assignment.getAssignment(asstId).setPayment(null) - - "ts-admin-pay-bonus": (asstId, amount, reason) -> - TurkServer.checkAdmin() - check(asstId, String) - check(amount, Number) - check(reason, String) - - # Protect against possible typos in payment amount. - throw new Meteor.Error(403, "You probably didn't mean to pay #{amount}") if amount > 10.00 - - asst = TurkServer.Assignment.getAssignment(asstId) - try - asst.setPayment(amount) - asst.payBonus(reason) - catch e - throw new Meteor.Error(403, e.toString()) - return - - "ts-admin-stop-experiment": (groupId) -> - TurkServer.checkAdmin() - check(groupId, String) - - TurkServer.Instance.getInstance(groupId).teardown() - return - - "ts-admin-stop-all-experiments": (batchId) -> - TurkServer.checkAdmin() - check(batchId, String) - - count = 0 - Experiments.find({batchId, endTime: {$exists: false} }).map (instance) -> - TurkServer.Instance.getInstance(instance._id).teardown() - count++ - - return "#{count} instances stopped." - -# Create and set up admin user (and password) if not existent -Meteor.startup -> - adminPw = TurkServer.config?.adminPassword - unless adminPw? - Meteor._debug "No admin password found for Turkserver. Please configure it in your settings." - return - - adminUser = Meteor.users.findOne(username: "admin") - unless adminUser - Accounts.createUser - username: "admin" + }).forEach(asst => TurkServer.Assignment.getAssignment(asst._id).payBonus(msg)); + + }, + + "ts-admin-unset-bonus"(asstId) { + TurkServer.checkAdmin(); + check(asstId, String); + + return TurkServer.Assignment.getAssignment(asstId).setPayment(null); + }, + + "ts-admin-pay-bonus"(asstId, amount, reason) { + TurkServer.checkAdmin(); + check(asstId, String); + check(amount, Number); + check(reason, String); + + // Protect against possible typos in payment amount. + if (amount > 10.00) { throw new Meteor.Error(403, `You probably didn't mean to pay ${amount}`); } + + const asst = TurkServer.Assignment.getAssignment(asstId); + try { + asst.setPayment(amount); + asst.payBonus(reason); + } catch (e) { + throw new Meteor.Error(403, e.toString()); + } + }, + + "ts-admin-stop-experiment"(groupId) { + TurkServer.checkAdmin(); + check(groupId, String); + + TurkServer.Instance.getInstance(groupId).teardown(); + }, + + "ts-admin-stop-all-experiments"(batchId) { + TurkServer.checkAdmin(); + check(batchId, String); + + let count = 0; + Experiments.find({batchId, endTime: {$exists: false} }).map(function(instance) { + TurkServer.Instance.getInstance(instance._id).teardown(); + return count++; + }); + + return `${count} instances stopped.`; + } +}); + +// Create and set up admin user (and password) if not existent +Meteor.startup(function() { + const adminPw = TurkServer.config != null ? TurkServer.config.adminPassword : undefined; + if (adminPw == null) { + Meteor._debug("No admin password found for Turkserver. Please configure it in your settings."); + return; + } + + const adminUser = Meteor.users.findOne({username: "admin"}); + if (!adminUser) { + Accounts.createUser({ + username: "admin", password: adminPw - Meteor._debug "Created Turkserver admin user from Meteor.settings." - - Meteor.users.update {username: "admin"}, - $set: {admin: true} - - else - # Make sure password matches that of settings file - # Don't change password unless necessary, which pitches login tokens - if Accounts._checkPassword(adminUser, adminPw).error - Accounts.setPassword(adminUser._id, adminPw) + }); + Meteor._debug("Created Turkserver admin user from Meteor.settings."); + + return Meteor.users.update({username: "admin"}, + {$set: {admin: true}}); + + } else { + // Make sure password matches that of settings file + // Don't change password unless necessary, which pitches login tokens + if (Accounts._checkPassword(adminUser, adminPw).error) { + return Accounts.setPassword(adminUser._id, adminPw); + } + } +}); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/admin/clientAdmin.js b/admin/clientAdmin.js index 9f3bb7f..fe16a8f 100644 --- a/admin/clientAdmin.js +++ b/admin/clientAdmin.js @@ -1,227 +1,305 @@ -TurkServer.adminSettings = - # Thresholds for ghetto pagination - defaultDaysThreshold: 7 +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +TurkServer.adminSettings = { + // Thresholds for ghetto pagination + defaultDaysThreshold: 7, defaultLimit: 200 - -# This controller handles the behavior of all admin templates -class TSAdminController extends RouteController - onBeforeAction: -> - # If not logged in, render login - unless Meteor.user() - @layout("tsContainer") - @render("tsAdminLogin") - # If not admin, render access denied - else unless Meteor.user().admin - @layout("tsContainer") - @render("tsAdminDenied") - else - @next() - - # Using subscriptions here is safe as long as everything else below uses waitOn - subscriptions: -> - return [] unless TurkServer.isAdmin() - - # Subscribe to admin data if we are an admin user, and in the admin interface - # Re-subscribes should be a no-op; no arguments - subs = [ Meteor.subscribe("tsAdmin") ] - - # Subscribe to user data and resubscribe when group changes - # Only subscribe if in admin interface, or assigned to a group - # TODO this should grab the group in watch mode as well - or maybe not, it can be handled by implementer publications - group = Partitioner.group() - - # must pass in different args for group to actually effect changes - subs.push Meteor.subscribe("tsAdminUsers", group) - - return subs - - layoutTemplate: "tsAdminLayout" - -logSubErrors = - onError: (e) -> console.log(e) - -Router.map -> - @route "tsOverview", +}; + +// This controller handles the behavior of all admin templates +class TSAdminController extends RouteController { + static initClass() { + + this.prototype.layoutTemplate = "tsAdminLayout"; + } + onBeforeAction() { + // If not logged in, render login + if (!Meteor.user()) { + this.layout("tsContainer"); + return this.render("tsAdminLogin"); + // If not admin, render access denied + } else if (!Meteor.user().admin) { + this.layout("tsContainer"); + return this.render("tsAdminDenied"); + } else { + return this.next(); + } + } + + // Using subscriptions here is safe as long as everything else below uses waitOn + subscriptions() { + if (!TurkServer.isAdmin()) { return []; } + + // Subscribe to admin data if we are an admin user, and in the admin interface + // Re-subscribes should be a no-op; no arguments + const subs = [ Meteor.subscribe("tsAdmin") ]; + + // Subscribe to user data and resubscribe when group changes + // Only subscribe if in admin interface, or assigned to a group + // TODO this should grab the group in watch mode as well - or maybe not, it can be handled by implementer publications + const group = Partitioner.group(); + + // must pass in different args for group to actually effect changes + subs.push(Meteor.subscribe("tsAdminUsers", group)); + + return subs; + } +} +TSAdminController.initClass(); + +const logSubErrors = + {onError(e) { return console.log(e); }}; + +Router.map(function() { + this.route("tsOverview", { path: "/turkserver", - controller: TSAdminController + controller: TSAdminController, template: "tsAdminOverview" - @route "tsMturk", + } + ); + this.route("tsMturk", { path: "turkserver/mturk", - controller: TSAdminController + controller: TSAdminController, template: "tsAdminMTurk" - @route "tsHits", + } + ); + this.route("tsHits", { path: "turkserver/hits", - controller: TSAdminController + controller: TSAdminController, template: "tsAdminHits" - # No sub needed - done with autocomplete - @route "tsWorkers", + } + ); + // No sub needed - done with autocomplete + this.route("tsWorkers", { path: "turkserver/workers/:workerId?", - controller: TSAdminController - template: "tsAdminWorkers" - waitOn: -> - return unless (workerId = this.params.workerId)? - Meteor.subscribe("tsAdminWorkerData", workerId) - data: -> - workerId: this.params.workerId - - @route "tsPanel", + controller: TSAdminController, + template: "tsAdminWorkers", + waitOn() { + let workerId; + if ((workerId = this.params.workerId) == null) { return; } + return Meteor.subscribe("tsAdminWorkerData", workerId); + }, + data() { + return {workerId: this.params.workerId}; + } + } + ); + + this.route("tsPanel", { path: "turkserver/panel", - controller: TSAdminController - template: "tsAdminPanel" - waitOn: -> Meteor.subscribe("tsAdminWorkers") + controller: TSAdminController, + template: "tsAdminPanel", + waitOn() { return Meteor.subscribe("tsAdminWorkers"); } + } + ); - @route "tsActiveAssignments", + this.route("tsActiveAssignments", { path: "turkserver/assignments/active", - controller: TSAdminController - template: "tsAdminActiveAssignments" - waitOn: -> - return unless (batchId = Session.get("_tsViewingBatchId"))? - return Meteor.subscribe("tsAdminActiveAssignments", batchId) - - @route "tsCompletedAssignments", + controller: TSAdminController, + template: "tsAdminActiveAssignments", + waitOn() { + let batchId; + if ((batchId = Session.get("_tsViewingBatchId")) == null) { return; } + return Meteor.subscribe("tsAdminActiveAssignments", batchId); + } + } + ); + + this.route("tsCompletedAssignments", { path: "turkserver/assignments/completed/:days?/:limit?", - controller: TSAdminController - template: "tsAdminCompletedAssignments" - waitOn: -> - return unless (batchId = Session.get("_tsViewingBatchId"))? - days = parseInt(@params.days) || TurkServer.adminSettings.defaultDaysThreshold - limit = parseInt(@params.limit) || TurkServer.adminSettings.defaultLimit - return Meteor.subscribe("tsAdminCompletedAssignments", batchId, days, limit) - data: -> - days: @params.days || TurkServer.adminSettings.defaultDaysThreshold - limit: @params.limit || TurkServer.adminSettings.defaultLimit - - @route "tsConnections", + controller: TSAdminController, + template: "tsAdminCompletedAssignments", + waitOn() { + let batchId; + if ((batchId = Session.get("_tsViewingBatchId")) == null) { return; } + const days = parseInt(this.params.days) || TurkServer.adminSettings.defaultDaysThreshold; + const limit = parseInt(this.params.limit) || TurkServer.adminSettings.defaultLimit; + return Meteor.subscribe("tsAdminCompletedAssignments", batchId, days, limit); + }, + data() { + return { + days: this.params.days || TurkServer.adminSettings.defaultDaysThreshold, + limit: this.params.limit || TurkServer.adminSettings.defaultLimit + }; + } + } + ); + + this.route("tsConnections", { path: "turkserver/connections", - controller: TSAdminController + controller: TSAdminController, template: "tsAdminConnections" - @route "tsLobby", + } + ); + this.route("tsLobby", { path: "turkserver/lobby", - controller: TSAdminController - template: "tsAdminLobby" - waitOn: -> - return unless (batchId = Session.get("_tsViewingBatchId"))? - # Same sub as normal lobby clients - return Meteor.subscribe("lobby", batchId) - - @route "tsExperiments", + controller: TSAdminController, + template: "tsAdminLobby", + waitOn() { + let batchId; + if ((batchId = Session.get("_tsViewingBatchId")) == null) { return; } + // Same sub as normal lobby clients + return Meteor.subscribe("lobby", batchId); + } + } + ); + + this.route("tsExperiments", { path: "turkserver/experiments/:days?/:limit?", - controller: TSAdminController + controller: TSAdminController, template: "tsAdminExperiments", - waitOn: -> - return unless (batchId = Session.get("_tsViewingBatchId"))? - days = parseInt(@params.days) || TurkServer.adminSettings.defaultDaysThreshold - limit = parseInt(@params.limit) || TurkServer.adminSettings.defaultLimit + waitOn() { + let batchId; + if ((batchId = Session.get("_tsViewingBatchId")) == null) { return; } + const days = parseInt(this.params.days) || TurkServer.adminSettings.defaultDaysThreshold; + const limit = parseInt(this.params.limit) || TurkServer.adminSettings.defaultLimit; return [ - Meteor.subscribe("tsAdminBatchRunningExperiments", batchId, logSubErrors) - Meteor.subscribe "tsAdminBatchCompletedExperiments", batchId, days, limit, logSubErrors - ] - data: -> - days: @params.days || TurkServer.adminSettings.defaultDaysThreshold - limit: @params.limit || TurkServer.adminSettings.defaultLimit - - @route "tsLogs", + Meteor.subscribe("tsAdminBatchRunningExperiments", batchId, logSubErrors), + Meteor.subscribe("tsAdminBatchCompletedExperiments", batchId, days, limit, logSubErrors) + ]; + }, + data() { + return { + days: this.params.days || TurkServer.adminSettings.defaultDaysThreshold, + limit: this.params.limit || TurkServer.adminSettings.defaultLimit + }; + } + } + ); + + this.route("tsLogs", { path: "turkserver/logs/:groupId/:count", - controller: TSAdminController - template: "tsAdminLogs" - waitOn: -> Meteor.subscribe("tsGroupLogs", @params.groupId, parseInt(@params.count)) - data: -> - instance: @params.groupId - count: @params.count - - @route "tsManage", + controller: TSAdminController, + template: "tsAdminLogs", + waitOn() { return Meteor.subscribe("tsGroupLogs", this.params.groupId, parseInt(this.params.count)); }, + data() { + return { + instance: this.params.groupId, + count: this.params.count + }; + } + } + ); + + return this.route("tsManage", { path: "turkserver/manage", - controller: TSAdminController + controller: TSAdminController, template: "tsAdminManage" + } + ); +}); -# Extra admin user subscription for after experiment ended -Deps.autorun -> - return unless TurkServer.isAdmin() - return unless (group = Partitioner.group())? - Meteor.subscribe "tsGroupUsers", group +// Extra admin user subscription for after experiment ended +Deps.autorun(function() { + let group; + if (!TurkServer.isAdmin()) { return; } + if ((group = Partitioner.group()) == null) { return; } + return Meteor.subscribe("tsGroupUsers", group); +}); -TurkServer.showInstanceModal = (id) -> - TurkServer._displayModal Template.tsAdminInstance, id +TurkServer.showInstanceModal = id => TurkServer._displayModal(Template.tsAdminInstance, id); -pillPopoverEvents = - # Show assignment instance info - "mouseenter .ts-instance-pill-container": (e) -> - container = $(e.target) +const pillPopoverEvents = { + // Show assignment instance info + "mouseenter .ts-instance-pill-container"(e) { + const container = $(e.target); container.popover({ - html: true - placement: "auto right" - trigger: "manual" - container: container - # TODO: Dynamic popover content would be very helpful here. - # https://github.com/meteor/meteor/issues/2010#issuecomment-40532280 - content: Blaze.toHTMLWithData Template.tsAdminAssignmentInstanceInfo, Blaze.getData(e.target) - }).popover("show") - - container.one("mouseleave", -> container.popover("destroy") ) - - # Show instance info in modal - "click .ts-instance-pill-container": (e) -> - TurkServer.showInstanceModal Blaze.getData(e.target).id - - "mouseenter .ts-user-pill-container": (e) -> - container = $(e.target) + html: true, + placement: "auto right", + trigger: "manual", + container, + // TODO: Dynamic popover content would be very helpful here. + // https://github.com/meteor/meteor/issues/2010#issuecomment-40532280 + content: Blaze.toHTMLWithData(Template.tsAdminAssignmentInstanceInfo, Blaze.getData(e.target)) + }).popover("show"); + + return container.one("mouseleave", () => container.popover("destroy")); + }, + + // Show instance info in modal + "click .ts-instance-pill-container"(e) { + return TurkServer.showInstanceModal(Blaze.getData(e.target).id); + }, + + "mouseenter .ts-user-pill-container"(e) { + const container = $(e.target); container.popover({ - html: true - placement: "auto right" - trigger: "manual" - container: container - # TODO: ditto - content: Blaze.toHTMLWithData Template.tsUserPillPopover, Blaze.getData(e.target) - }).popover("show") - - container.one("mouseleave", -> container.popover("destroy") ) - -Template.turkserverPulldown.events - "click .ts-adminToggle": (e) -> - e.preventDefault() - $("#ts-content").slideToggle() - -# Add the pill events as well -Template.turkserverPulldown.events(pillPopoverEvents) - -Template.turkserverPulldown.helpers - admin: TurkServer.isAdmin - currentExperiment: -> Experiments.findOne() - -Template.tsAdminLogin.events = - "submit form": (e, tp) -> - e.preventDefault() - password = $(tp.find("input")).val() - Meteor.loginWithPassword "admin", password, (err) -> - bootbox.alert("Unable to login: " + err.reason) if err? - -Template.tsAdminLayout.events(pillPopoverEvents) - -onlineUsers = -> Meteor.users.find({ + html: true, + placement: "auto right", + trigger: "manual", + container, + // TODO: ditto + content: Blaze.toHTMLWithData(Template.tsUserPillPopover, Blaze.getData(e.target)) + }).popover("show"); + + return container.one("mouseleave", () => container.popover("destroy")); + } +}; + +Template.turkserverPulldown.events({ + "click .ts-adminToggle"(e) { + e.preventDefault(); + return $("#ts-content").slideToggle(); + } +}); + +// Add the pill events as well +Template.turkserverPulldown.events(pillPopoverEvents); + +Template.turkserverPulldown.helpers({ + admin: TurkServer.isAdmin, + currentExperiment() { return Experiments.findOne(); } +}); + +Template.tsAdminLogin.events = { + "submit form"(e, tp) { + e.preventDefault(); + const password = $(tp.find("input")).val(); + return Meteor.loginWithPassword("admin", password, function(err) { + if (err != null) { return bootbox.alert("Unable to login: " + err.reason); } + }); + } +}; + +Template.tsAdminLayout.events(pillPopoverEvents); + +const onlineUsers = () => Meteor.users.find({ admin: {$exists: false}, "status.online": true -}) - -Template.tsAdminOverview.events = - "click .-ts-account-balance": -> - Meteor.call "ts-admin-account-balance", (err, res) -> - if err then bootbox.alert(err.reason) else bootbox.alert("

$#{res}

") - -Template.tsAdminOverview.helpers - onlineUserCount: -> onlineUsers().count() - -# All non-admin users who are online, sorted by most recent login -Template.tsAdminConnections.helpers - users: -> - Meteor.users.find({ - admin: {$exists: false} +}); + +Template.tsAdminOverview.events = { + "click .-ts-account-balance"() { + return Meteor.call("ts-admin-account-balance", function(err, res) { + if (err) { return bootbox.alert(err.reason); } else { return bootbox.alert(`

$${res}

`); } + }); + } +}; + +Template.tsAdminOverview.helpers({ + onlineUserCount() { return onlineUsers().count(); }}); + +// All non-admin users who are online, sorted by most recent login +Template.tsAdminConnections.helpers({ + users() { + return Meteor.users.find({ + admin: {$exists: false}, "turkserver.state": {$exists: true} }, { sort: { "status.lastLogin.date" : -1 } - }) - -Template.tsAdminConnectionMaintenance.events - "click .-ts-cleanup-user-state": -> - TurkServer.callWithModal("ts-admin-cleanup-user-state") + }); + } +}); + +Template.tsAdminConnectionMaintenance.events({ + "click .-ts-cleanup-user-state"() { + return TurkServer.callWithModal("ts-admin-cleanup-user-state"); + } +}); diff --git a/admin/experimentAdmin.js b/admin/experimentAdmin.js index e4c39cc..e3dd019 100644 --- a/admin/experimentAdmin.js +++ b/admin/experimentAdmin.js @@ -1,277 +1,351 @@ -treatments = -> Treatments.find() - -Template.tsAdminExperiments.events - "submit form.-ts-admin-experiment-filter": (e, t) -> - e.preventDefault() - - Router.go "tsExperiments", +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const treatments = () => Treatments.find(); + +Template.tsAdminExperiments.events({ + "submit form.-ts-admin-experiment-filter"(e, t) { + e.preventDefault(); + + return Router.go("tsExperiments", { days: parseInt(t.find("input[name=filter_days]").value) || - TurkServer.adminSettings.defaultDaysThreshold + TurkServer.adminSettings.defaultDaysThreshold, limit: parseInt(t.find("input[name=filter_limit]").value) || TurkServer.adminSettings.defaultLimit - - "click .-ts-stop-experiment": -> - expId = @_id - bootbox.confirm "This will end the experiment immediately. Are you sure?", (res) -> - Meteor.call "ts-admin-stop-experiment", expId if res - -Template.tsAdminExperiments.helpers - numExperiments: -> Experiments.find().count() - -numUsers = -> @users?.length - -Template.tsAdminExperimentMaintenance.events - "click .-ts-stop-all-experiments": (e) -> - bootbox.confirm "This will end all experiments in progress. Are you sure?", (res) -> - return unless res - TurkServer.callWithModal "ts-admin-stop-all-experiments", Session.get("_tsViewingBatchId") + } + ); + }, + + "click .-ts-stop-experiment"() { + const expId = this._id; + return bootbox.confirm("This will end the experiment immediately. Are you sure?", function(res) { + if (res) { return Meteor.call("ts-admin-stop-experiment", expId); } + }); + } +}); + +Template.tsAdminExperiments.helpers({ + numExperiments() { return Experiments.find().count(); }}); + +const numUsers = function() { return (this.users != null ? this.users.length : undefined); }; + +Template.tsAdminExperimentMaintenance.events({ + "click .-ts-stop-all-experiments"(e) { + return bootbox.confirm("This will end all experiments in progress. Are you sure?", function(res) { + if (!res) { return; } + return TurkServer.callWithModal("ts-admin-stop-all-experiments", Session.get("_tsViewingBatchId")); + }); + } +}); Template.tsAdminExperimentTimeline.helpers({ - experiments: -> - Experiments.find({startTime: $exists: true}, { + experiments() { + return Experiments.find({startTime: {$exists: true}}, { sort: {startTime: 1}, fields: {startTime: 1, endTime: 1} - }) -}) + }); + } +}); -Template.tsAdminExperimentTimeline.rendered = -> - @lastUpdate = new ReactiveVar(new Date) +Template.tsAdminExperimentTimeline.rendered = function() { + this.lastUpdate = new ReactiveVar(new Date); - svg = d3.select(this.find("svg")) - $svg = this.$("svg") + const svg = d3.select(this.find("svg")); + const $svg = this.$("svg"); - margin = - bottom: 30 + const margin = + {bottom: 30}; - chartHeight = $svg.height() - margin.bottom + const chartHeight = $svg.height() - margin.bottom; - x = d3.scale.linear() - .range([0, $svg.width()]) + const x = d3.scale.linear() + .range([0, $svg.width()]); - y = d3.scale.ordinal() - .rangeBands([0, $svg.height() - margin.bottom], 0.2) + const y = d3.scale.ordinal() + .rangeBands([0, $svg.height() - margin.bottom], 0.2); - xAxis = d3.svg.axis() + const xAxis = d3.svg.axis() .scale(x) .orient("bottom") - .ticks(5) # Dates are long - .tickFormat( (date) -> new Date(date).toLocaleString() ) + .ticks(5) // Dates are long + .tickFormat( date => new Date(date).toLocaleString()); - svgX = svg.select("g.x.axis") - .attr("transform", "translate(0,#{chartHeight})") + const svgX = svg.select("g.x.axis") + .attr("transform", `translate(0,${chartHeight})`); - svgXgrid = svg.select("g.x.grid") + const svgXgrid = svg.select("g.x.grid"); - chart = svg.select("g.chart") + const chart = svg.select("g.chart"); - redraw = -> - # Update x axis - svgX.call(xAxis) + const redraw = function() { + // Update x axis + svgX.call(xAxis); - # Update x grid - grid = svgXgrid.selectAll("line.grid") - .data(x.ticks(10)) # More gridlines than above + // Update x grid + const grid = svgXgrid.selectAll("line.grid") + .data(x.ticks(10)); // More gridlines than above grid.enter() .append("line") - .attr("class", "grid") + .attr("class", "grid"); - grid.exit().remove() + grid.exit().remove(); - grid.attr - x1: x - x2: x - y1: 0 + grid.attr({ + x1: x, + x2: x, + y1: 0, y2: chartHeight + }); - now = Tracker.nonreactive -> new Date(TimeSync.serverTime()) + const now = Tracker.nonreactive(() => new Date(TimeSync.serverTime())); - # Update bar positions; need to guard against missing values upon load - chart.selectAll(".bar").attr - x: (e) -> e && x(e.startTime) || 0 - width: (e) -> - e && Math.max( x(e.endTime || now) - x(e.startTime), 0 ) || 0 - y: (e) -> e && y(e._id) || 0 + // Update bar positions; need to guard against missing values upon load + return chart.selectAll(".bar").attr({ + x(e) { return (e && x(e.startTime)) || 0; }, + width(e) { + return (e && Math.max( x(e.endTime || now) - x(e.startTime), 0 )) || 0; + }, + y(e) { return (e && y(e._id)) || 0; }, height: y.rangeBand() + }); + }; - zoom = d3.behavior.zoom() + const zoom = d3.behavior.zoom() .scaleExtent([1, 100]) - .on("zoom", redraw) + .on("zoom", redraw); - svg.call(zoom) + svg.call(zoom); - this.autorun => - @lastUpdate.get() + return this.autorun(() => { + this.lastUpdate.get(); - # Grab bound data - exps = chart.selectAll(".bar").data() + // Grab bound data + const exps = chart.selectAll(".bar").data(); - # Note that this will redraw until experiments are done. - # But, once all experiments are done, timesync won't be used + // Note that this will redraw until experiments are done. + // But, once all experiments are done, timesync won't be used - # guards below since some bars may not have data bound - # compute new domains - minStart = d3.min(exps, (e) -> e?.startTime) || TimeSync.serverTime(null, 2000) - # a running experiment hasn't ended yet :) - maxEnd = d3.max(exps, (e) -> e?.endTime || TimeSync.serverTime(null, 2000)) + // guards below since some bars may not have data bound + // compute new domains + const minStart = d3.min(exps, e => e != null ? e.startTime : undefined) || TimeSync.serverTime(null, 2000); + // a running experiment hasn't ended yet :) + const maxEnd = d3.max(exps, e => (e != null ? e.endTime : undefined) || TimeSync.serverTime(null, 2000)); - # However, we cannot use Deps.currentComputation.firstRun here as data may not - # be ready on first run. - x.domain( [minStart, maxEnd] ) - y.domain( exps.map( (e) -> e && e._id ) ) + // However, we cannot use Deps.currentComputation.firstRun here as data may not + // be ready on first run. + x.domain( [minStart, maxEnd] ); + y.domain( exps.map( e => e && e._id) ); - # Set zoom **after** x axis has been initialized - zoom.x(x) + // Set zoom **after** x axis has been initialized + zoom.x(x); - redraw() + return redraw(); + }); +}; -Template.tsAdminExperimentTimeline.events - "click .bar": (e, t) -> - TurkServer.showInstanceModal this._id +Template.tsAdminExperimentTimeline.events({ + "click .bar"(e, t) { + return TurkServer.showInstanceModal(this._id); + } +}); -Template.tsAdminExperimentTimelineBar.onRendered -> - d3.select(this.firstNode).datum(this.data) - # Trigger re-draw on parent, guard against first render - this.parent().lastUpdate?.set(new Date) +Template.tsAdminExperimentTimelineBar.onRendered(function() { + d3.select(this.firstNode).datum(this.data); + // Trigger re-draw on parent, guard against first render + return __guard__(this.parent().lastUpdate, x => x.set(new Date)); +}); -Template.tsAdminActiveExperiments.helpers - experiments: -> - Experiments.find - endTime: {$exists: false} +Template.tsAdminActiveExperiments.helpers({ + experiments() { + return Experiments.find( + {endTime: {$exists: false}} , - sort: { startTime: -1 } + {sort: { startTime: -1 }}); + }, - numUsers: numUsers + numUsers +}); -Template.tsAdminCompletedExperiments.helpers - experiments: -> - Experiments.find - endTime: {$exists: true} +Template.tsAdminCompletedExperiments.helpers({ + experiments() { + return Experiments.find( + {endTime: {$exists: true}} , - sort: { startTime: -1 } - duration: -> - TurkServer.Util.duration(@endTime - @startTime) - numUsers: numUsers - -Template.tsAdminExpButtons.helpers - dataRoute: Meteor.settings?.public?.turkserver?.dataRoute - -Template.tsAdminLogs.helpers - experiment: -> Experiments.findOne(@instance) - logEntries: -> Logs.find({}, {sort: _timestamp: -1}) - entryData: -> _.omit(@, "_id", "_userId", "_groupId", "_timestamp") - -Template.tsAdminLogs.events - "submit form.ts-admin-log-filter": (e, t) -> - e.preventDefault() - count = parseInt(t.find("input[name=count]").value) - return unless count - - Router.go "tsLogs", - groupId: @instance, - count: count - -Template.tsAdminTreatments.helpers - treatments: treatments - zeroTreatments: -> Treatments.find().count() is 0 - -Template.tsAdminTreatments.events = - "click tbody > tr": (e) -> - Session.set("_tsSelectedTreatmentId", @_id) - - "click .-ts-delete-treatment": -> - Meteor.call "ts-delete-treatment", @_id, (err, res) -> - bootbox.alert(err.message) if err - -Template.tsAdminNewTreatment.events = - "submit form": (e, tmpl) -> - e.preventDefault() - el = tmpl.find("input[name=name]") - name = el.value - el.value = "" - - unless name - bootbox.alert "Enter a non-empty string." - return - - Treatments.insert - name: name - , (e) -> bootbox.alert(e.message) if e - -Template.tsAdminTreatmentConfig.helpers - selectedTreatment: -> - Treatments.findOne Session.get("_tsSelectedTreatmentId") - -Template.tsAdminConfigureBatch.events = - "click .-ts-activate-batch": -> - Batches.update @_id, $set: + {sort: { startTime: -1 }}); + }, + duration() { + return TurkServer.Util.duration(this.endTime - this.startTime); + }, + numUsers +}); + +Template.tsAdminExpButtons.helpers({ + dataRoute: __guard__(__guard__(Meteor.settings != null ? Meteor.settings.public : undefined, x1 => x1.turkserver), x => x.dataRoute)}); + +Template.tsAdminLogs.helpers({ + experiment() { return Experiments.findOne(this.instance); }, + logEntries() { return Logs.find({}, {sort: {_timestamp: -1}}); }, + entryData() { return _.omit(this, "_id", "_userId", "_groupId", "_timestamp"); } +}); + +Template.tsAdminLogs.events({ + "submit form.ts-admin-log-filter"(e, t) { + e.preventDefault(); + const count = parseInt(t.find("input[name=count]").value); + if (!count) { return; } + + return Router.go("tsLogs", { + groupId: this.instance, + count + } + ); + } +}); + +Template.tsAdminTreatments.helpers({ + treatments, + zeroTreatments() { return Treatments.find().count() === 0; } +}); + +Template.tsAdminTreatments.events = { + "click tbody > tr"(e) { + return Session.set("_tsSelectedTreatmentId", this._id); + }, + + "click .-ts-delete-treatment"() { + return Meteor.call("ts-delete-treatment", this._id, function(err, res) { + if (err) { return bootbox.alert(err.message); } + }); + } +}; + +Template.tsAdminNewTreatment.events = { + "submit form"(e, tmpl) { + e.preventDefault(); + const el = tmpl.find("input[name=name]"); + const name = el.value; + el.value = ""; + + if (!name) { + bootbox.alert("Enter a non-empty string."); + return; + } + + return Treatments.insert( + {name} + , function(e) { if (e) { return bootbox.alert(e.message); } }); + } +}; + +Template.tsAdminTreatmentConfig.helpers({ + selectedTreatment() { + return Treatments.findOne(Session.get("_tsSelectedTreatmentId")); + } +}); + +Template.tsAdminConfigureBatch.events = { + "click .-ts-activate-batch"() { + return Batches.update(this._id, { $set: { active: true + } + } + ); + }, - "click .-ts-deactivate-batch": -> - Batches.update @_id, $set: + "click .-ts-deactivate-batch"() { + return Batches.update(this._id, { $set: { active: false + } + } + ); + }, - "change input[name=allowReturns]": (e) -> - Batches.update @_id, $set: + "change input[name=allowReturns]"(e) { + return Batches.update(this._id, { $set: { allowReturns: e.target.checked - -Template.tsAdminConfigureBatch.helpers - selectedBatch: -> Batches.findOne(Session.get("_tsSelectedBatchId")) - -Template.tsAdminBatchEditDesc.rendered = -> - container = @$('div.editable') - grabValue = -> $.trim container.text() # Always get reactively updated value - container.editable - value: grabValue - display: -> # Never set text; have Meteor update to preserve reactivity - success: (response, newValue) => - Batches.update @data._id, - $set: { desc: newValue } - # Thinks it knows the value, but it actually doesn't - grab a fresh value each time - Meteor.defer -> container.data('editableContainer').formOptions.value = grabValue - return # The value of this function matters - return - -Template.tsAdminBatchEditTreatments.events = - "click .-ts-remove-batch-treatment": (e, tmpl) -> - treatmentName = "" + (@name || @) # In case the treatment is gone - Batches.update Session.get("_tsSelectedBatchId"), - $pull: { treatments: treatmentName } - - "click .-ts-add-batch-treatment": (e, tmpl) -> - e.preventDefault() - treatment = Blaze.getData(tmpl.find(":selected")) - return unless treatment? - Batches.update @_id, - $addToSet: { treatments: treatment.name } - -Template.tsAdminBatchEditTreatments.helpers - allTreatments: treatments - -Template.tsAdminBatchList.events = - "click tbody > tr": (e) -> - Session.set("_tsSelectedBatchId", @_id) - -Template.tsAdminBatchList.helpers - batches: -> Batches.find() - zeroBatches: -> Batches.find().count() is 0 - selectedClass: -> - if Session.equals("_tsSelectedBatchId", @_id) then "info" else "" - -Template.tsAdminAddBatch.events = - "submit form": (e, tmpl) -> - e.preventDefault() - - el = tmpl.find("input") - name = el.value - return if name is "" - - el.value = "" - - # Default batch settings - Batches.insert - name: name - grouping: "groupSize" - groupVal: 1 + } + } + ); + } +}; + +Template.tsAdminConfigureBatch.helpers({ + selectedBatch() { return Batches.findOne(Session.get("_tsSelectedBatchId")); }}); + +Template.tsAdminBatchEditDesc.rendered = function() { + const container = this.$('div.editable'); + const grabValue = () => $.trim(container.text()); // Always get reactively updated value + container.editable({ + value: grabValue, + display() {}, // Never set text; have Meteor update to preserve reactivity + success: (response, newValue) => { + Batches.update(this.data._id, + {$set: { desc: newValue }}); + // Thinks it knows the value, but it actually doesn't - grab a fresh value each time + Meteor.defer(() => container.data('editableContainer').formOptions.value = grabValue); + } + }); // The value of this function matters +}; + +Template.tsAdminBatchEditTreatments.events = { + "click .-ts-remove-batch-treatment"(e, tmpl) { + const treatmentName = "" + (this.name || this); // In case the treatment is gone + return Batches.update(Session.get("_tsSelectedBatchId"), + {$pull: { treatments: treatmentName }}); + }, + + "click .-ts-add-batch-treatment"(e, tmpl) { + e.preventDefault(); + const treatment = Blaze.getData(tmpl.find(":selected")); + if (treatment == null) { return; } + return Batches.update(this._id, + {$addToSet: { treatments: treatment.name }}); + } +}; + +Template.tsAdminBatchEditTreatments.helpers({ + allTreatments: treatments}); + +Template.tsAdminBatchList.events = { + "click tbody > tr"(e) { + return Session.set("_tsSelectedBatchId", this._id); + } +}; + +Template.tsAdminBatchList.helpers({ + batches() { return Batches.find(); }, + zeroBatches() { return Batches.find().count() === 0; }, + selectedClass() { + if (Session.equals("_tsSelectedBatchId", this._id)) { return "info"; } else { return ""; } + } +}); + +Template.tsAdminAddBatch.events = { + "submit form"(e, tmpl) { + e.preventDefault(); + + const el = tmpl.find("input"); + const name = el.value; + if (name === "") { return; } + + el.value = ""; + + // Default batch settings + return Batches.insert({ + name, + grouping: "groupSize", + groupVal: 1, lobby: true - , (e) -> bootbox.alert(e.message) if e + } + , function(e) { if (e) { return bootbox.alert(e.message); } }); + } +}; + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/admin/lobbyAdmin.js b/admin/lobbyAdmin.js index 2c2bf4d..3255390 100644 --- a/admin/lobbyAdmin.js +++ b/admin/lobbyAdmin.js @@ -1,13 +1,22 @@ -Template.tsAdminLobby.helpers - lobbyUsers: -> LobbyStatus.find() +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +Template.tsAdminLobby.helpers({ + lobbyUsers() { return LobbyStatus.find(); }}); -Template.tsAdminLobbyHeader.events = - "submit form": (e, t) -> - e.preventDefault() - event = t.$("input[name=lobby-event]").val() - Meteor.call "ts-admin-lobby-event", Session.get("_tsViewingBatchId"), event, (err, res) -> - bootbox.alert(err) if err +Template.tsAdminLobbyHeader.events = { + "submit form"(e, t) { + e.preventDefault(); + const event = t.$("input[name=lobby-event]").val(); + return Meteor.call("ts-admin-lobby-event", Session.get("_tsViewingBatchId"), event, function(err, res) { + if (err) { return bootbox.alert(err); } + }); + } +}; -Template.tsAdminLobbyHeader.helpers - count: -> LobbyStatus.find().count() - readyCount: -> LobbyStatus.find({status: true}).count() +Template.tsAdminLobbyHeader.helpers({ + count() { return LobbyStatus.find().count(); }, + readyCount() { return LobbyStatus.find({status: true}).count(); } +}); diff --git a/admin/mturkAdmin.js b/admin/mturkAdmin.js index 40218f4..bccb85c 100644 --- a/admin/mturkAdmin.js +++ b/admin/mturkAdmin.js @@ -1,190 +1,248 @@ -quals = -> Qualifications.find() -hitTypes = -> HITTypes.find() - -Template.tsAdminMTurk.helpers - selectedHITType: -> HITTypes.findOne Session.get("_tsSelectedHITType") +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const quals = () => Qualifications.find(); +const hitTypes = () => HITTypes.find(); + +Template.tsAdminMTurk.helpers({ + selectedHITType() { return HITTypes.findOne(Session.get("_tsSelectedHITType")); }}); Template.tsAdminMTurk.events = - "click .-ts-new-hittype": -> Session.set("_tsSelectedHITType", undefined) + {"click .-ts-new-hittype"() { return Session.set("_tsSelectedHITType", undefined); }}; Template.tsAdminHitTypes.events = - "click tr": -> Session.set("_tsSelectedHITType", @_id) - -Template.tsAdminHitTypes.helpers - hitTypes: hitTypes - selectedClass: -> - if Session.equals("_tsSelectedHITType", @_id) then "info" else "" - -Template.tsAdminViewHitType.events = - "click .-ts-register-hittype": -> - Meteor.call "ts-admin-register-hittype", @_id, (err, res) -> - bootbox.alert(err.reason) if err - "click .-ts-delete-hittype": -> - HITTypes.remove(@_id) - -Template.tsAdminViewHitType.helpers - batchName: -> Batches.findOne(@batchId)?.name || "(none)" - renderReward: -> @Reward.toFixed(2) - qualName: -> Qualifications.findOne(""+@)?.name - -Template.tsAdminNewHitType.events = - "submit form": (e, tmpl) -> - e.preventDefault() - - id = HITTypes.insert - batchId: tmpl.find("select[name=batch]").value - Title: tmpl.find("input[name=title]").value - Description: tmpl.find("textarea[name=desc]").value - Keywords: tmpl.find("input[name=keywords]").value - Reward: parseFloat(tmpl.find("input[name=reward]").value) - QualificationRequirement: $(tmpl.find("select[name=quals]")).val() - AssignmentDurationInSeconds: parseInt(tmpl.find("input[name=duration]").value) + {"click tr"() { return Session.set("_tsSelectedHITType", this._id); }}; + +Template.tsAdminHitTypes.helpers({ + hitTypes, + selectedClass() { + if (Session.equals("_tsSelectedHITType", this._id)) { return "info"; } else { return ""; } + } +}); + +Template.tsAdminViewHitType.events = { + "click .-ts-register-hittype"() { + return Meteor.call("ts-admin-register-hittype", this._id, function(err, res) { + if (err) { return bootbox.alert(err.reason); } + }); + }, + "click .-ts-delete-hittype"() { + return HITTypes.remove(this._id); + } +}; + +Template.tsAdminViewHitType.helpers({ + batchName() { return __guard__(Batches.findOne(this.batchId), x => x.name) || "(none)"; }, + renderReward() { return this.Reward.toFixed(2); }, + qualName() { return __guard__(Qualifications.findOne(""+this), x => x.name); } +}); + +Template.tsAdminNewHitType.events = { + "submit form"(e, tmpl) { + e.preventDefault(); + + const id = HITTypes.insert({ + batchId: tmpl.find("select[name=batch]").value, + Title: tmpl.find("input[name=title]").value, + Description: tmpl.find("textarea[name=desc]").value, + Keywords: tmpl.find("input[name=keywords]").value, + Reward: parseFloat(tmpl.find("input[name=reward]").value), + QualificationRequirement: $(tmpl.find("select[name=quals]")).val(), + AssignmentDurationInSeconds: parseInt(tmpl.find("input[name=duration]").value), AutoApprovalDelayInSeconds: parseInt(tmpl.find("input[name=delay]").value) + }); + + return Session.set("_tsSelectedHITType", id); + } +}; - Session.set("_tsSelectedHITType", id) - -Template.tsAdminNewHitType.helpers - quals: quals - batches: -> Batches.find() - -Template.tsAdminQuals.events = - "click .-ts-delete-qual": -> - Qualifications.remove(@_id) - -Template.tsAdminQuals.helpers - quals: quals - value: -> - if @IntegerValue - return @IntegerValue + " (Integer)" - else if @LocaleValue - return @LocaleValue + " (Locale)" - else - return - -Template.tsAdminNewQual.events = - "click .-ts-create-qual": (e, tmpl) -> - name = tmpl.find("input[name=name]").value - type = tmpl.find("input[name=type]").value - comp = tmpl.find("select[name=comp]").value - value = tmpl.find("input[name=value]").value - preview = tmpl.find("input[name=preview]").checked - - return if !name or !type or !comp - - qual = - name: name - QualificationTypeId: type - Comparator: comp +Template.tsAdminNewHitType.helpers({ + quals, + batches() { return Batches.find(); } +}); + +Template.tsAdminQuals.events = { + "click .-ts-delete-qual"() { + return Qualifications.remove(this._id); + } +}; + +Template.tsAdminQuals.helpers({ + quals, + value() { + if (this.IntegerValue) { + return this.IntegerValue + " (Integer)"; + } else if (this.LocaleValue) { + return this.LocaleValue + " (Locale)"; + } else { + return; + } + } +}); + +Template.tsAdminNewQual.events = { + "click .-ts-create-qual"(e, tmpl) { + const name = tmpl.find("input[name=name]").value; + let type = tmpl.find("input[name=type]").value; + const comp = tmpl.find("select[name=comp]").value; + const { + value + } = tmpl.find("input[name=value]"); + const preview = tmpl.find("input[name=preview]").checked; + + if (!name || !type || !comp) { return; } + + const qual = { + name, + QualificationTypeId: type, + Comparator: comp, RequiredToPreview: preview + }; + + try { + switch (comp) { + case "Exists": case "DoesNotExist": + if (!!value) { throw new Error("No value should be specified for Exists or DoesNotExist"); } + break; + + case "In": case "NotIn": + // Parse value as a comma-separated array + var vals = []; + type = null; + + // Check that they are all the same type + // TODO we don't check for the validity of the type here + for (let v of Array.from(value.split(/[\s,]+/))) { + var newType, numV; + if (!v) { continue; } + + if (numV = parseInt(v)) { + vals.push(numV); + newType = "Integer"; + } else { + vals.push(v); + newType = "String"; + } + + if ((type != null) && (newType !== type)) { throw new Error("Must be all Integers or Locales"); } + type = newType; + } + + if (type == null) { throw new Error("Must specify at least one value for In or NotIn"); } + + if (type === "Integer") { + qual.IntegerValue = vals; + } else { + qual.LocaleValue = vals; + } + break; + + default: // Things with values + if (!!value) { + if (parseInt(value)) { + qual.IntegerValue = value; + } else { + qual.LocaleValue = value; + } + } + } - try - switch comp - when "Exists", "DoesNotExist" - throw new Error("No value should be specified for Exists or DoesNotExist") if !!value - - when "In", "NotIn" - # Parse value as a comma-separated array - vals = [] - type = null - - # Check that they are all the same type - # TODO we don't check for the validity of the type here - for v in value.split(/[\s,]+/) - continue if !v - - if numV = parseInt(v) - vals.push(numV) - newType = "Integer" - else - vals.push(v) - newType = "String" - - throw new Error("Must be all Integers or Locales") if type? and newType isnt type - type = newType - - throw new Error("Must specify at least one value for In or NotIn") unless type? - - if type is "Integer" - qual.IntegerValue = vals - else - qual.LocaleValue = vals - - else # Things with values - if !!value - if parseInt(value) - qual.IntegerValue = value - else - qual.LocaleValue = value - - Qualifications.insert(qual) - catch e - bootbox.alert(e.toString()) - -Template.tsAdminHits.events = - "click tr": -> Session.set("_tsSelectedHIT", @_id) - "click .-ts-new-hit": -> Session.set("_tsSelectedHIT", undefined) - -Template.tsAdminHits.helpers - hits: -> HITs.find({}, {sort: {CreationTime: -1}}) - selectedHIT: -> HITs.findOne Session.get("_tsSelectedHIT") - -Template.tsAdminViewHit.events = - "click .-ts-refresh-hit": -> - TurkServer.callWithModal "ts-admin-refresh-hit", @HITId - - "click .-ts-expire-hit": -> - TurkServer.callWithModal "ts-admin-expire-hit", @HITId - - "submit .-ts-change-hittype": (e, tmpl) -> - e.preventDefault() - htId = tmpl.find("select[name=hittype]").value - HITTypeId = HITTypes.findOne(htId).HITTypeId - unless HITTypeId - bootbox.alert("Register that HIT Type first") - return - - params = - HITId: @HITId - HITTypeId: HITTypeId - TurkServer.callWithModal "ts-admin-change-hittype", params - - "submit .-ts-extend-assignments": (e, tmpl) -> - e.preventDefault() - params = - HITId: @HITId + return Qualifications.insert(qual); + } catch (error) { + e = error; + return bootbox.alert(e.toString()); + } + } +}; + +Template.tsAdminHits.events = { + "click tr"() { return Session.set("_tsSelectedHIT", this._id); }, + "click .-ts-new-hit"() { return Session.set("_tsSelectedHIT", undefined); } +}; + +Template.tsAdminHits.helpers({ + hits() { return HITs.find({}, {sort: {CreationTime: -1}}); }, + selectedHIT() { return HITs.findOne(Session.get("_tsSelectedHIT")); } +}); + +Template.tsAdminViewHit.events = { + "click .-ts-refresh-hit"() { + return TurkServer.callWithModal("ts-admin-refresh-hit", this.HITId); + }, + + "click .-ts-expire-hit"() { + return TurkServer.callWithModal("ts-admin-expire-hit", this.HITId); + }, + + "submit .-ts-change-hittype"(e, tmpl) { + e.preventDefault(); + const htId = tmpl.find("select[name=hittype]").value; + const { + HITTypeId + } = HITTypes.findOne(htId); + if (!HITTypeId) { + bootbox.alert("Register that HIT Type first"); + return; + } + + const params = { + HITId: this.HITId, + HITTypeId + }; + return TurkServer.callWithModal("ts-admin-change-hittype", params); + }, + + "submit .-ts-extend-assignments"(e, tmpl) { + e.preventDefault(); + const params = { + HITId: this.HITId, MaxAssignmentsIncrement: parseInt(tmpl.find("input[name=assts]").value) - TurkServer.callWithModal "ts-admin-extend-hit", params - - "submit .-ts-extend-expiration": (e, tmpl) -> - e.preventDefault() - params = - HITId: @HITId + }; + return TurkServer.callWithModal("ts-admin-extend-hit", params); + }, + + "submit .-ts-extend-expiration"(e, tmpl) { + e.preventDefault(); + const params = { + HITId: this.HITId, ExpirationIncrementInSeconds: parseInt(tmpl.find("input[name=secs]").value) - TurkServer.callWithModal "ts-admin-extend-hit", params + }; + return TurkServer.callWithModal("ts-admin-extend-hit", params); + } +}; -Template.tsAdminViewHit.helpers - hitTypes: hitTypes +Template.tsAdminViewHit.helpers({ + hitTypes}); -Template.tsAdminNewHit.events = - "submit form": (e, tmpl) -> - e.preventDefault() +Template.tsAdminNewHit.events = { + "submit form"(e, tmpl) { + e.preventDefault(); - hitTypeId = tmpl.find("select[name=hittype]").value + const hitTypeId = tmpl.find("select[name=hittype]").value; - unless hitTypeId - bootbox.alert("HIT Type isn't registered") - return + if (!hitTypeId) { + bootbox.alert("HIT Type isn't registered"); + return; + } - params = - MaxAssignments:parseInt(tmpl.find("input[name=maxAssts]").value) + const params = { + MaxAssignments:parseInt(tmpl.find("input[name=maxAssts]").value), LifetimeInSeconds:parseInt(tmpl.find("input[name=lifetime]").value) + }; - TurkServer.callWithModal "ts-admin-create-hit", hitTypeId, params + return TurkServer.callWithModal("ts-admin-create-hit", hitTypeId, params); + } +}; -Template.tsAdminNewHit.helpers - hitTypes: hitTypes +Template.tsAdminNewHit.helpers({ + hitTypes}); -Template.tsAdminWorkers.helpers +Template.tsAdminWorkers.helpers({ settings: { position: "bottom", limit: 5, @@ -192,78 +250,90 @@ Template.tsAdminWorkers.helpers { collection: Meteor.users, field: "workerId", - template: Template.tsAdminWorkerItem - # Match on workerId or username - selector: (match) -> - $or: [ - { workerId: { $regex: "^" + match.toUpperCase() } }, - { username: { $regex: match, $options: "i" } } - ] + template: Template.tsAdminWorkerItem, + // Match on workerId or username + selector(match) { + return { + $or: [ + { workerId: { $regex: "^" + match.toUpperCase() } }, + { username: { $regex: match, $options: "i" } } + ] + }; + } } ] - } + }, - workerData: -> Workers.findOne(@workerId) + workerData() { return Workers.findOne(this.workerId); }, - workerActiveAssts: -> - Assignments.find({ - workerId: @workerId, + workerActiveAssts() { + return Assignments.find({ + workerId: this.workerId, status: { $ne: "completed" } }, { - sort: acceptTime: -1 - }) - - workerCompletedAssts: -> - Assignments.find({ - workerId: @workerId, + sort: { acceptTime: -1 + } + }); + }, + + workerCompletedAssts() { + return Assignments.find({ + workerId: this.workerId, status: "completed" }, { - sort: submitTime: -1 - }) - - numCompletedAssts: -> - Assignments.find({ - workerId: @workerId, + sort: { submitTime: -1 + } + }); + }, + + numCompletedAssts() { + return Assignments.find({ + workerId: this.workerId, status: "completed" - }).count() + }).count(); + } +}); -Template.tsAdminWorkers.events - "autocompleteselect input": (e, t, user) -> - Router.go("tsWorkers", {workerId: user.workerId}) if user.workerId? +Template.tsAdminWorkers.events({ + "autocompleteselect input"(e, t, user) { + if (user.workerId != null) { return Router.go("tsWorkers", {workerId: user.workerId}); } + } +}); -Template.tsAdminPanel.rendered = -> - svg = d3.select(@find("svg")) - $svg = @$("svg") +Template.tsAdminPanel.rendered = function() { + const svg = d3.select(this.find("svg")); + const $svg = this.$("svg"); - margin = - left: 90 + const margin = { + left: 90, bottom: 30 + }; - x = d3.scale.linear() - .range([0, $svg.width() - margin.left]) + const x = d3.scale.linear() + .range([0, $svg.width() - margin.left]); - y = d3.scale.ordinal() - # Data was originally stored in GMT -5 so just display that - .domain(m.zone(300).format("HH ZZ") for m in TurkServer.Util._defaultTimeSlots()) - .rangeBands([0, $svg.height() - margin.bottom], 0.2) + const y = d3.scale.ordinal() + // Data was originally stored in GMT -5 so just display that + .domain(Array.from(TurkServer.Util._defaultTimeSlots()).map((m) => m.zone(300).format("HH ZZ"))) + .rangeBands([0, $svg.height() - margin.bottom], 0.2); - xAxis = d3.svg.axis() + const xAxis = d3.svg.axis() .scale(x) - .orient("bottom") + .orient("bottom"); - yAxis = d3.svg.axis() + const yAxis = d3.svg.axis() .scale(y) - .orient("left") + .orient("left"); - # Draw axes - chart = svg.append("g") - .attr("transform", "translate(" + margin.left + ",0)") + // Draw axes + const chart = svg.append("g") + .attr("transform", "translate(" + margin.left + ",0)"); chart.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + ($svg.height() - margin.bottom) + ")") - .call(xAxis) + .call(xAxis); chart.append("g") .attr("class", "y axis") @@ -273,209 +343,267 @@ Template.tsAdminPanel.rendered = -> .attr("y", -80) .attr("dy", ".71em") .style("text-anchor", "end") - .text("Timezone") + .text("Timezone"); - data = {} + const data = {}; - newData = false - redraw = -> - return unless newData - newData = false + let newData = false; + const redraw = function() { + if (!newData) { return; } + newData = false; - entries = d3.entries(data) + const entries = d3.entries(data); - # Update domain with max value - x.domain([0, d3.max(entries, (d) -> d.value)]) - chart.select("g.x.axis").call(xAxis) + // Update domain with max value + x.domain([0, d3.max(entries, d => d.value)]); + chart.select("g.x.axis").call(xAxis); - bars = chart.selectAll(".bar") - .data(entries, (d) -> d.key) + const bars = chart.selectAll(".bar") + .data(entries, d => d.key); - # Add any new bars in the enter selection + // Add any new bars in the enter selection bars.enter() .append("rect") .attr("class", "bar") - .attr("y", (d) -> y(d.key) ) + .attr("y", d => y(d.key)) .attr("height", y.rangeBand()); - # Update widths in the update selection, including entered nodes - bars.attr("data-value", (d) -> d.value ) + // Update widths in the update selection, including entered nodes + return bars.attr("data-value", d => d.value) .transition() - .attr("width", (d) -> x(d.value) ) - - # Aggregate the worker times into the current timezone - @handle = Workers.find().observeChanges - added: (id, fields) -> - # Only use data from workers who agreed to be contacted - return unless fields.contact and fields.available? - for time in fields.available.times - # normalize into buckets - continue unless time # Ignore invalid (empty) entries - data[time] ?= 0 - data[time] += 1 - - newData = true - Meteor.defer(redraw) - -Template.tsAdminPanel.destroyed = -> - @handle.stop() - -Template.tsAdminPanel.helpers - workerContact: -> Workers.find(contact: true).count() - workerTotal: -> Workers.find().count() - -recipientsHelper = (recipients) -> - if recipients.length == 1 - return recipients - else - return recipients.length - -Template.tsAdminEmail.helpers - messages: -> WorkerEmails.find({}, {sort: {sentTime: -1}}) - recipientsHelper: recipientsHelper - -Template.tsAdminEmail.events - "click tr": -> Session.set("_tsSelectedEmailId", @_id) - -Template.tsAdminEmailMessage.helpers - selectedMessage: -> - emailId = Session.get("_tsSelectedEmailId") - return WorkerEmails.findOne(emailId) if emailId? - recipientsHelper: recipientsHelper - -Template.tsAdminEmailMessage.events - "click .ts-admin-send-message": -> - TurkServer.callWithModal "ts-admin-send-message", @_id - - "click .ts-admin-resend-message": -> - TurkServer.callWithModal "ts-admin-resend-message", @_id - - "click .ts-admin-copy-message": -> - TurkServer.callWithModal "ts-admin-copy-message", @_id - - "click .ts-admin-delete-message": -> - TurkServer.callWithModal "ts-admin-delete-message", @_id - -Template.tsAdminNewEmail.helpers - messages: -> - WorkerEmails.find({}, { + .attr("width", d => x(d.value)); + }; + + // Aggregate the worker times into the current timezone + return this.handle = Workers.find().observeChanges({ + added(id, fields) { + // Only use data from workers who agreed to be contacted + if (!fields.contact || (fields.available == null)) { return; } + for (let time of Array.from(fields.available.times)) { + // normalize into buckets + if (!time) { continue; } // Ignore invalid (empty) entries + if (data[time] == null) { data[time] = 0; } + data[time] += 1; + } + + newData = true; + return Meteor.defer(redraw); + } + }); +}; + +Template.tsAdminPanel.destroyed = function() { + return this.handle.stop(); +}; + +Template.tsAdminPanel.helpers({ + workerContact() { return Workers.find({contact: true}).count(); }, + workerTotal() { return Workers.find().count(); } +}); + +const recipientsHelper = function(recipients) { + if (recipients.length === 1) { + return recipients; + } else { + return recipients.length; + } + }; + +Template.tsAdminEmail.helpers({ + messages() { return WorkerEmails.find({}, {sort: {sentTime: -1}}); }, + recipientsHelper +}); + +Template.tsAdminEmail.events({ + "click tr"() { return Session.set("_tsSelectedEmailId", this._id); }}); + +Template.tsAdminEmailMessage.helpers({ + selectedMessage() { + const emailId = Session.get("_tsSelectedEmailId"); + if (emailId != null) { return WorkerEmails.findOne(emailId); } + }, + recipientsHelper +}); + +Template.tsAdminEmailMessage.events({ + "click .ts-admin-send-message"() { + return TurkServer.callWithModal("ts-admin-send-message", this._id); + }, + + "click .ts-admin-resend-message"() { + return TurkServer.callWithModal("ts-admin-resend-message", this._id); + }, + + "click .ts-admin-copy-message"() { + return TurkServer.callWithModal("ts-admin-copy-message", this._id); + }, + + "click .ts-admin-delete-message"() { + return TurkServer.callWithModal("ts-admin-delete-message", this._id); + } +}); + +Template.tsAdminNewEmail.helpers({ + messages() { + return WorkerEmails.find({}, { fields: {subject: 1}, sort: {sentTime: -1} - }) - -Template.tsAdminNewEmail.events - "submit form": (e, t) -> - e.preventDefault() - $sub = t.$("input[name=subject]") - $msg = t.$("textarea[name=message]") - - subject = $sub.val() - message = $msg.val() - - if t.$("input[name=recipients]:checked").val() is "copy" - copyFromId = t.$("select[name=copyFrom]").val() - unless copyFromId? - bootbox.alert("Select an e-mail to copy recipients from") - return - - TurkServer.callWithModal "ts-admin-create-message", subject, message, copyFromId, (res) -> - # Display the new message - Session.set("_tsSelectedEmailId", res) - -Template.tsAdminAssignmentMaintenance.events - "click .-ts-cancel-assignments": -> - message = "This will cancel all assignments of users are disconnected. You should only do this if these users will definitely not return to their work. Continue? " - bootbox.confirm message, (res) -> - return unless res - TurkServer.callWithModal "ts-admin-cancel-assignments", Session.get("_tsViewingBatchId") - -numAssignments = -> Assignments.find().count() - -Template.tsAdminActiveAssignments.helpers - numAssignments: numAssignments - activeAssts: -> - Assignments.find {}, { sort: acceptTime: -1 } - -checkBatch = (batchId) -> - unless batchId? - bootbox.alert("Select a batch first!") - return false - return true - -Template.tsAdminCompletedMaintenance.events - "click .-ts-refresh-assignments": -> - batchId = Session.get("_tsViewingBatchId") - return unless checkBatch(batchId) - TurkServer.callWithModal "ts-admin-refresh-assignments", batchId - - "click .-ts-approve-all": -> - batchId = Session.get("_tsViewingBatchId") - return unless checkBatch(batchId) - - TurkServer.callWithModal "ts-admin-count-submitted", batchId, (count) -> - if count is 0 - bootbox.alert "No assignments to approve!" - return - - bootbox.prompt "#{count} assignments will be approved. Enter a (possibly blank) message to send to each worker.", (res) -> - return unless res? - TurkServer.callWithModal "ts-admin-approve-all", batchId, res - - "click .-ts-pay-bonuses": -> - batchId = Session.get("_tsViewingBatchId") - return unless checkBatch(batchId) - - TurkServer.callWithModal "ts-admin-count-unpaid-bonuses", batchId, (data) -> - if data.numPaid is 0 - bootbox.alert "No bonuses to pay!" - return - - bootbox.prompt "#{data.numPaid} workers will be paid, for a total of $#{data.amt}. Enter a message to send to each worker.", (res) -> - return unless res - TurkServer.callWithModal "ts-admin-pay-bonuses", batchId, res - -Template.tsAdminCompletedAssignments.events - "submit form.ts-admin-assignment-filter": (e, t) -> - e.preventDefault() - - Router.go "tsCompletedAssignments", + }); + } +}); + +Template.tsAdminNewEmail.events({ + "submit form"(e, t) { + let copyFromId; + e.preventDefault(); + const $sub = t.$("input[name=subject]"); + const $msg = t.$("textarea[name=message]"); + + const subject = $sub.val(); + const message = $msg.val(); + + if (t.$("input[name=recipients]:checked").val() === "copy") { + copyFromId = t.$("select[name=copyFrom]").val(); + if (copyFromId == null) { + bootbox.alert("Select an e-mail to copy recipients from"); + return; + } + } + + return TurkServer.callWithModal("ts-admin-create-message", subject, message, copyFromId, res => // Display the new message + Session.set("_tsSelectedEmailId", res)); + } +}); + +Template.tsAdminAssignmentMaintenance.events({ + "click .-ts-cancel-assignments"() { + const message = "This will cancel all assignments of users are disconnected. You should only do this if these users will definitely not return to their work. Continue? "; + return bootbox.confirm(message, function(res) { + if (!res) { return; } + return TurkServer.callWithModal("ts-admin-cancel-assignments", Session.get("_tsViewingBatchId")); + }); + } +}); + +const numAssignments = () => Assignments.find().count(); + +Template.tsAdminActiveAssignments.helpers({ + numAssignments, + activeAssts() { + return Assignments.find({}, { sort: {acceptTime: -1} }); + }}); + +const checkBatch = function(batchId) { + if (batchId == null) { + bootbox.alert("Select a batch first!"); + return false; + } + return true; +}; + +Template.tsAdminCompletedMaintenance.events({ + "click .-ts-refresh-assignments"() { + const batchId = Session.get("_tsViewingBatchId"); + if (!checkBatch(batchId)) { return; } + return TurkServer.callWithModal("ts-admin-refresh-assignments", batchId); + }, + + "click .-ts-approve-all"() { + const batchId = Session.get("_tsViewingBatchId"); + if (!checkBatch(batchId)) { return; } + + return TurkServer.callWithModal("ts-admin-count-submitted", batchId, function(count) { + if (count === 0) { + bootbox.alert("No assignments to approve!"); + return; + } + + return bootbox.prompt(`${count} assignments will be approved. Enter a (possibly blank) message to send to each worker.`, function(res) { + if (res == null) { return; } + return TurkServer.callWithModal("ts-admin-approve-all", batchId, res); + }); + }); + }, + + "click .-ts-pay-bonuses"() { + const batchId = Session.get("_tsViewingBatchId"); + if (!checkBatch(batchId)) { return; } + + return TurkServer.callWithModal("ts-admin-count-unpaid-bonuses", batchId, function(data) { + if (data.numPaid === 0) { + bootbox.alert("No bonuses to pay!"); + return; + } + + return bootbox.prompt(`${data.numPaid} workers will be paid, for a total of $${data.amt}. Enter a message to send to each worker.`, function(res) { + if (!res) { return; } + return TurkServer.callWithModal("ts-admin-pay-bonuses", batchId, res); + }); + }); + } +}); + +Template.tsAdminCompletedAssignments.events({ + "submit form.ts-admin-assignment-filter"(e, t) { + e.preventDefault(); + + return Router.go("tsCompletedAssignments", { days: parseInt(t.find("input[name=filter_days]").value) || - TurkServer.adminSettings.defaultDaysThreshold + TurkServer.adminSettings.defaultDaysThreshold, limit: parseInt(t.find("input[name=filter_limit]").value) || TurkServer.adminSettings.defaultLimit + } + ); + } +}); + +Template.tsAdminCompletedAssignments.helpers({ + numAssignments, + completedAssts() { + return Assignments.find({}, { sort: {submitTime: -1} }); + }}); + +Template.tsAdminCompletedAssignmentsTable.events({ + "click .ts-admin-refresh-assignment"() { + return TurkServer.callWithModal("ts-admin-refresh-assignment", this._id); + }, + + "click .ts-admin-approve-assignment"() { + const _asstId = this._id; + return bootbox.prompt("Approve assignment: enter an optional message to send to the worker.", res => TurkServer.callWithModal("ts-admin-approve-assignment", _asstId, res)); + }, + + "click .ts-admin-reject-assignment"() { + const _asstId = this._id; + return bootbox.prompt("1 worker's assignment will be rejected. Enter a message to send to the worker.", function(res) { + if (!res) { return; } + return TurkServer.callWithModal("ts-admin-reject-assignment", _asstId, res); + }); + }, + + "click .ts-admin-unset-bonus"() { + return Meteor.call("ts-admin-unset-bonus", this._id); + }, + + "click .ts-admin-pay-bonus"() { + return TurkServer._displayModal(Template.tsAdminPayBonus, this); + } +}); + +Template.tsAdminCompletedAssignmentRow.helpers({ + labelStatus() { + switch (this.mturkStatus) { + case "Submitted": return "label-warning"; + case "Approved": return "label-primary"; + case "Rejected": return "label-danger"; + default: return "label-default"; + } + }, + submitted() { + return this.mturkStatus === "Submitted"; + } +}); -Template.tsAdminCompletedAssignments.helpers - numAssignments: numAssignments - completedAssts: -> - Assignments.find {}, { sort: submitTime: -1 } - -Template.tsAdminCompletedAssignmentsTable.events - "click .ts-admin-refresh-assignment": -> - TurkServer.callWithModal "ts-admin-refresh-assignment", this._id - - "click .ts-admin-approve-assignment": -> - _asstId = this._id - bootbox.prompt "Approve assignment: enter an optional message to send to the worker.", (res) -> - TurkServer.callWithModal "ts-admin-approve-assignment", _asstId, res - - "click .ts-admin-reject-assignment": -> - _asstId = this._id - bootbox.prompt "1 worker's assignment will be rejected. Enter a message to send to the worker.", (res) -> - return unless res - TurkServer.callWithModal "ts-admin-reject-assignment", _asstId, res - - "click .ts-admin-unset-bonus": -> - Meteor.call("ts-admin-unset-bonus", this._id) - - "click .ts-admin-pay-bonus": -> - TurkServer._displayModal Template.tsAdminPayBonus, this - -Template.tsAdminCompletedAssignmentRow.helpers - labelStatus: -> - switch @mturkStatus - when "Submitted" then "label-warning" - when "Approved" then "label-primary" - when "Rejected" then "label-danger" - else "label-default" - submitted: -> - return @mturkStatus == "Submitted" +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/admin/util.js b/admin/util.js index 2cbd410..46354dc 100644 --- a/admin/util.js +++ b/admin/util.js @@ -1,144 +1,183 @@ -TurkServer.Util ?= {} - -TurkServer.Util.duration = (millis) -> - diff = moment.utc(millis) - time = diff.format("H:mm:ss") - days = +diff.format("DDD") - 1 - return (if days isnt 0 then days + "d " else "") + time - -TurkServer.Util.timeSince = (timestamp) -> - TurkServer.Util.duration(TimeSync.serverTime() - timestamp) -TurkServer.Util.timeUntil = (timestamp) -> - TurkServer.Util.duration(timestamp - TimeSync.serverTime()) - -TurkServer.callWithModal = (args..., callback) -> - dialog = bootbox.dialog - closeButton: false +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS201: Simplify complex destructure assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +if (TurkServer.Util == null) { TurkServer.Util = {}; } + +TurkServer.Util.duration = function(millis) { + const diff = moment.utc(millis); + const time = diff.format("H:mm:ss"); + const days = +diff.format("DDD") - 1; + return (days !== 0 ? days + "d " : "") + time; +}; + +TurkServer.Util.timeSince = timestamp => TurkServer.Util.duration(TimeSync.serverTime() - timestamp); +TurkServer.Util.timeUntil = timestamp => TurkServer.Util.duration(timestamp - TimeSync.serverTime()); + +TurkServer.callWithModal = function(...args1) { + let adjustedLength = Math.max(args1.length, 1), args = args1.slice(0, adjustedLength - 1), callback = args1[adjustedLength - 1]; + const dialog = bootbox.dialog({ + closeButton: false, message: "

Working...

" - - # If callback is not specified, assume it is just an argument. - unless _.isFunction(callback) - args.push(callback) - callback = null - - # Add our own callback that alerts for errors - args.push (err, res) -> - dialog.modal("hide") - if err? - bootbox.alert(err) - return - - # If callback is given, calls it with data, otherwise just alert - if res? && callback? - callback(res) - else if res? - bootbox.alert(res) - - return Meteor.call.apply(null, args) - -UI.registerHelper "_tsViewingBatch", -> Batches.findOne(Session.get("_tsViewingBatchId")) - -UI.registerHelper "_tsLookupTreatment", -> Treatments.findOne(name: ""+@) - -UI.registerHelper "_tsRenderTime", (timestamp) -> new Date(timestamp).toLocaleString() -UI.registerHelper "_tsRenderTimeMillis", (timestamp) -> - m = moment(timestamp) - m.format("L h:mm:ss.SSS A") - -UI.registerHelper "_tsRenderTimeSince", TurkServer.Util.timeSince -UI.registerHelper "_tsRenderTimeUntil", TurkServer.Util.timeUntil - -UI.registerHelper "_tsRenderISOTime", (isoString) -> - m = moment(isoString) - return m.format("L LT") + " (" + m.fromNow() + ")" - -# https://github.com/kvz/phpjs/blob/master/functions/strings/nl2br.js -nl2br = (str) -> (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1
$2') - -UI.registerHelper "_tsnl2br", nl2br - -Template.tsBatchSelector.events = - "change select": (e) -> - unless Session.equals("_tsViewingBatchId", e.target.value) - Session.set("_tsViewingBatchId", e.target.value) - -Template.tsBatchSelector.helpers - batches: -> Batches.find({}, {sort: {name: 1}}) - noBatchSelection: -> not Session.get("_tsViewingBatchId") - selected: -> Session.equals("_tsViewingBatchId", @_id) - viewingBatchId: -> Session.get("_tsViewingBatchId") - -Template.tsAdminInstance.rendered = -> - # Subscribe to instance with whatever we rendered with - this.autorun -> - Meteor.subscribe "tsAdminInstance", Blaze.getData() - -Template.tsAdminInstance.helpers - instance: -> Experiments.findOne(@+"") - -Template.tsAdminPayBonus.events - "submit form": (e, t) -> - e.preventDefault() - amount = parseFloat(t.find("input[name=amount]").value) - reason = t.find("textarea[name=reason]").value - - $(t.firstNode).closest(".bootbox.modal").modal('hide') - - TurkServer.callWithModal("ts-admin-pay-bonus", @_id, amount, reason) - -Template.tsAdminEmailWorker.events - "submit form": (e, t) -> - e.preventDefault() - subject = t.find("input[name=subject]").value - message = t.find("textarea[name=message]").value - recipients = [@workerId] - - emailId = WorkerEmails.insert({ subject, message, recipients }) - - $(t.firstNode).closest(".bootbox.modal").modal('hide') - - TurkServer.callWithModal("ts-admin-send-message", emailId) - -userLabelClass = -> - switch - when @status?.idle then "label-warning" - when @status?.online then "label-success" - else "label-default" - -userIdentifier = -> - if @username - @username - else if @workerId - "(" + @workerId + ")" - else - "(" + @_id + ")" - -Template.tsAdminWorkerItem.helpers - labelClass: userLabelClass + }); + + // If callback is not specified, assume it is just an argument. + if (!_.isFunction(callback)) { + args.push(callback); + callback = null; + } + + // Add our own callback that alerts for errors + args.push(function(err, res) { + dialog.modal("hide"); + if (err != null) { + bootbox.alert(err); + return; + } + + // If callback is given, calls it with data, otherwise just alert + if ((res != null) && (callback != null)) { + return callback(res); + } else if (res != null) { + return bootbox.alert(res); + } + }); + + return Meteor.call.apply(null, args); +}; + +UI.registerHelper("_tsViewingBatch", () => Batches.findOne(Session.get("_tsViewingBatchId"))); + +UI.registerHelper("_tsLookupTreatment", function() { return Treatments.findOne({name: ""+this}); }); + +UI.registerHelper("_tsRenderTime", timestamp => new Date(timestamp).toLocaleString()); +UI.registerHelper("_tsRenderTimeMillis", function(timestamp) { + const m = moment(timestamp); + return m.format("L h:mm:ss.SSS A"); +}); + +UI.registerHelper("_tsRenderTimeSince", TurkServer.Util.timeSince); +UI.registerHelper("_tsRenderTimeUntil", TurkServer.Util.timeUntil); + +UI.registerHelper("_tsRenderISOTime", function(isoString) { + const m = moment(isoString); + return m.format("L LT") + " (" + m.fromNow() + ")"; +}); + +// https://github.com/kvz/phpjs/blob/master/functions/strings/nl2br.js +const nl2br = str => (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1
$2'); + +UI.registerHelper("_tsnl2br", nl2br); + +Template.tsBatchSelector.events = { + "change select"(e) { + if (!Session.equals("_tsViewingBatchId", e.target.value)) { + return Session.set("_tsViewingBatchId", e.target.value); + } + } +}; + +Template.tsBatchSelector.helpers({ + batches() { return Batches.find({}, {sort: {name: 1}}); }, + noBatchSelection() { return !Session.get("_tsViewingBatchId"); }, + selected() { return Session.equals("_tsViewingBatchId", this._id); }, + viewingBatchId() { return Session.get("_tsViewingBatchId"); } +}); + +Template.tsAdminInstance.rendered = function() { + // Subscribe to instance with whatever we rendered with + return this.autorun(() => Meteor.subscribe("tsAdminInstance", Blaze.getData())); +}; + +Template.tsAdminInstance.helpers({ + instance() { return Experiments.findOne(this+""); }}); + +Template.tsAdminPayBonus.events({ + "submit form"(e, t) { + e.preventDefault(); + const amount = parseFloat(t.find("input[name=amount]").value); + const reason = t.find("textarea[name=reason]").value; + + $(t.firstNode).closest(".bootbox.modal").modal('hide'); + + return TurkServer.callWithModal("ts-admin-pay-bonus", this._id, amount, reason); + } +}); + +Template.tsAdminEmailWorker.events({ + "submit form"(e, t) { + e.preventDefault(); + const subject = t.find("input[name=subject]").value; + const message = t.find("textarea[name=message]").value; + const recipients = [this.workerId]; + + const emailId = WorkerEmails.insert({ subject, message, recipients }); + + $(t.firstNode).closest(".bootbox.modal").modal('hide'); + + return TurkServer.callWithModal("ts-admin-send-message", emailId); + } +}); + +const userLabelClass = function() { + switch (false) { + case !(this.status != null ? this.status.idle : undefined): return "label-warning"; + case !(this.status != null ? this.status.online : undefined): return "label-success"; + default: return "label-default"; + } +}; + +const userIdentifier = function() { + if (this.username) { + return this.username; + } else if (this.workerId) { + return "(" + this.workerId + ")"; + } else { + return "(" + this._id + ")"; + } +}; + +Template.tsAdminWorkerItem.helpers({ + labelClass: userLabelClass, identifier: userIdentifier - -Template.tsUserPill.helpers - user: -> - switch - when @userId then Meteor.users.findOne(@userId) - when @workerId then Meteor.users.findOne(workerId: @workerId) - else @ # Object was already passed in - labelClass: userLabelClass +}); + +Template.tsUserPill.helpers({ + user() { + switch (false) { + case !this.userId: return Meteor.users.findOne(this.userId); + case !this.workerId: return Meteor.users.findOne({workerId: this.workerId}); + default: return this; + } + }, // Object was already passed in + labelClass: userLabelClass, identifier: userIdentifier - -Template.tsUserPill.events - "click .ts-admin-email-worker": -> - TurkServer._displayModal Template.tsAdminEmailWorker, this - -Template.tsDescList.helpers - properties: -> - result = [] - for key, value of this - result.push key: key, value: value - return result - # Special rules for rendering description lists - value: -> - switch - when @value is false then "false" - when _.isObject(@value) then JSON.stringify(@value) - else nl2br(@value) +}); + +Template.tsUserPill.events({ + "click .ts-admin-email-worker"() { + return TurkServer._displayModal(Template.tsAdminEmailWorker, this); + } +}); + +Template.tsDescList.helpers({ + properties() { + const result = []; + for (let key in this) { + const value = this[key]; + result.push({key, value}); + } + return result; + }, + // Special rules for rendering description lists + value() { + switch (false) { + case this.value !== false: return "false"; + case !_.isObject(this.value): return JSON.stringify(this.value); + default: return nl2br(this.value); + } + } +}); diff --git a/client/dialogs.js b/client/dialogs.js index 7b500e7..1d40384 100644 --- a/client/dialogs.js +++ b/client/dialogs.js @@ -1,82 +1,109 @@ -### +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +/* Dialogs to possibly show after page loaded -### +*/ -### +/* Disconnect warning Don't display this warning until some time after app has started; otherwise it's confusing to users -### -disconnectWarningDelay = 5000 +*/ +const disconnectWarningDelay = 5000; -TurkServer._delayedStartup -> - disconnectDialog = null +TurkServer._delayedStartup(function() { + let disconnectDialog = null; - # Warn when disconnected instead of just sitting there. - Deps.autorun -> - status = Meteor.status() + // Warn when disconnected instead of just sitting there. + return Deps.autorun(function() { + const status = Meteor.status(); - if status.connected and disconnectDialog? - disconnectDialog.modal("hide") - disconnectDialog = null - return + if (status.connected && (disconnectDialog != null)) { + disconnectDialog.modal("hide"); + disconnectDialog = null; + return; + } - if !status.connected and disconnectDialog is null - disconnectDialog = bootbox.dialog - closeButton: false + if (!status.connected && (disconnectDialog === null)) { + disconnectDialog = bootbox.dialog({ + closeButton: false, message: - """

You have been disconnected from the server. - Please check your Internet connection.

""" - return -, disconnectWarningDelay - -TurkServer._displayModal = (template, data, options) -> - # minimum options to get message to show - options ?= { message: " " } - dialog = bootbox.dialog(options) - # Take out the thing that bootbox rendered - dialog.find(".bootbox-body").remove() - - # Since bootbox/bootstrap uses jQuery, this should clean up itself - Blaze.renderWithData(template, data, dialog.find(".modal-body")[0]) - return dialog - -TurkServer.ensureUsername = -> - ### + `

You have been disconnected from the server. +Please check your Internet connection.

` + }); + return; + } + }); +} +, disconnectWarningDelay); + +TurkServer._displayModal = function(template, data, options) { + // minimum options to get message to show + if (options == null) { options = { message: " " }; } + const dialog = bootbox.dialog(options); + // Take out the thing that bootbox rendered + dialog.find(".bootbox-body").remove(); + + // Since bootbox/bootstrap uses jQuery, this should clean up itself + Blaze.renderWithData(template, data, dialog.find(".modal-body")[0]); + return dialog; +}; + +TurkServer.ensureUsername = function() { + /* Capture username after logging in - ### - usernameDialog = null - - Deps.autorun -> - userId = Meteor.userId() - unless userId - usernameDialog?.modal("hide") - usernameDialog = null - return - - # TODO: stop the username dialog popping up during the subscription process - username = Meteor.users.findOne(userId, fields: {username: 1})?.username - - if username and usernameDialog - usernameDialog.modal("hide") - usernameDialog = null - return - - if !username and usernameDialog is null - usernameDialog = bootbox.dialog(message: " ").html('') - Blaze.render(Template.tsRequestUsername, usernameDialog[0]) - return - -Template.tsRequestUsername.events = - "focus input": -> Session.set("_tsUsernameError", undefined) - "submit form": (e, tmpl) -> - e.preventDefault() - input = tmpl.find("input[name=username]") - input.blur() - username = input.value - Meteor.call "ts-set-username", username, (err, res) -> - Session.set("_tsUsernameError", err.reason) if err - -Template.tsRequestUsername.helpers - usernameError: -> Session.get("_tsUsernameError") + */ + let usernameDialog = null; + + return Deps.autorun(function() { + const userId = Meteor.userId(); + if (!userId) { + if (usernameDialog != null) { + usernameDialog.modal("hide"); + } + usernameDialog = null; + return; + } + + // TODO: stop the username dialog popping up during the subscription process + const username = __guard__(Meteor.users.findOne(userId, {fields: {username: 1}}), x => x.username); + + if (username && usernameDialog) { + usernameDialog.modal("hide"); + usernameDialog = null; + return; + } + + if (!username && (usernameDialog === null)) { + usernameDialog = bootbox.dialog({message: " "}).html(''); + Blaze.render(Template.tsRequestUsername, usernameDialog[0]); + return; + } + }); +}; + +Template.tsRequestUsername.events = { + "focus input"() { return Session.set("_tsUsernameError", undefined); }, + "submit form"(e, tmpl) { + e.preventDefault(); + const input = tmpl.find("input[name=username]"); + input.blur(); + const username = input.value; + return Meteor.call("ts-set-username", username, function(err, res) { + if (err) { return Session.set("_tsUsernameError", err.reason); } + }); + } +}; + +Template.tsRequestUsername.helpers({ + usernameError() { return Session.get("_tsUsernameError"); }}); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/client/helpers.js b/client/helpers.js index edc1a1b..09ced01 100644 --- a/client/helpers.js +++ b/client/helpers.js @@ -1,38 +1,48 @@ -UI.registerHelper "_tsDebug", -> - console.log @, arguments - -TurkServer.Util ?= {} - -TurkServer.Util._defaultTimeSlots = -> - # Default time selections: 9AM EST to 11PM EST - m = moment.utc(hours: 9 + 5).local() - return (m.clone().add(x, 'hours') for x in [0..14]) - -# Submit as soon as this template appears on the page. -Template.mturkSubmit.rendered = -> @find("form").submit() - -Template.tsTimePicker.helpers - zone: -> moment().format("Z") - -Template.tsTimeOptions.helpers - momentList: TurkServer.Util._defaultTimeSlots - -Template.tsTimeOptions.helpers - # Store all values in GMT-5 - valueFormatted: -> @zone(300).format('HH ZZ') - # Display values in user's timezone - displayFormatted: -> @local().format('hA [UTC]Z') - -### +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +UI.registerHelper("_tsDebug", function() { + return console.log(this, arguments); +}); + +if (TurkServer.Util == null) { TurkServer.Util = {}; } + +TurkServer.Util._defaultTimeSlots = function() { + // Default time selections: 9AM EST to 11PM EST + const m = moment.utc({hours: 9 + 5}).local(); + return ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14].map((x) => m.clone().add(x, 'hours'))); +}; + +// Submit as soon as this template appears on the page. +Template.mturkSubmit.rendered = function() { return this.find("form").submit(); }; + +Template.tsTimePicker.helpers({ + zone() { return moment().format("Z"); }}); + +Template.tsTimeOptions.helpers({ + momentList: TurkServer.Util._defaultTimeSlots}); + +Template.tsTimeOptions.helpers({ + // Store all values in GMT-5 + valueFormatted() { return this.zone(300).format('HH ZZ'); }, + // Display values in user's timezone + displayFormatted() { return this.local().format('hA [UTC]Z'); } +}); + +/* Submits the exit survey data to the server and submits the HIT if successful -### -TurkServer.submitExitSurvey = (results, panel) -> - Meteor.call "ts-submit-exitdata", results, panel, (err, res) -> - bootbox.alert(err) if err +*/ +TurkServer.submitExitSurvey = (results, panel) => Meteor.call("ts-submit-exitdata", results, panel, function(err, res) { + if (err) { bootbox.alert(err); } - if res - TurkServer.submitHIT() - # TODO: log the user out here? Maybe doesn't matter because resume login will be disabled + if (res) { + return TurkServer.submitHIT(); + } +}); + // TODO: log the user out here? Maybe doesn't matter because resume login will be disabled -TurkServer.submitHIT = -> Blaze.render(Template.mturkSubmit, document.body) +TurkServer.submitHIT = () => Blaze.render(Template.mturkSubmit, document.body); diff --git a/client/lobby_client.js b/client/lobby_client.js index 8a12ee6..ec75c05 100644 --- a/client/lobby_client.js +++ b/client/lobby_client.js @@ -1,56 +1,81 @@ -### +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +/* Set up route and auto-redirection for default lobby, unless disabled As defined below, autoLobby is default true unless explicitly set to false TODO document this setting -### -unless Meteor.settings?.public?.turkserver?.autoLobby is false - Router.map -> - @route "lobby", +*/ +if (__guard__(__guard__(Meteor.settings != null ? Meteor.settings.public : undefined, x1 => x1.turkserver), x => x.autoLobby) !== false) { + Router.map(function() { + return this.route("lobby", { template: "tsBasicLobby", - layoutTemplate: "tsContainer" - onBeforeAction: -> - # Don't show lobby template to unauthenticated users - unless Meteor.user() - @layout("tsContainer") - @render("tsUserAccessDenied") - else - @next() - - # We need to defer this because iron router can throw errors if a route is - # hit before the page is fully loaded - Meteor.startup -> - Meteor.defer -> - # Subscribe to lobby if we are in it (auto unsubscribe if we aren't) - Deps.autorun -> - return if Package?.tinytest # Don't change routes when being tested - if TurkServer.inLobby() - Meteor.subscribe("lobby", TurkServer.batch()?._id) - Router.go("/lobby") - -Meteor.methods - "toggleStatus" : -> - userId = Meteor.userId() - existing = LobbyStatus.findOne(userId) if userId - return unless userId and existing + layoutTemplate: "tsContainer", + onBeforeAction() { + // Don't show lobby template to unauthenticated users + if (!Meteor.user()) { + this.layout("tsContainer"); + return this.render("tsUserAccessDenied"); + } else { + return this.next(); + } + } + } + ); + }); + + // We need to defer this because iron router can throw errors if a route is + // hit before the page is fully loaded + Meteor.startup(() => Meteor.defer(() => // Subscribe to lobby if we are in it (auto unsubscribe if we aren't) + Deps.autorun(function() { + if (typeof Package !== 'undefined' && Package !== null ? Package.tinytest : undefined) { return; } // Don't change routes when being tested + if (TurkServer.inLobby()) { + Meteor.subscribe("lobby", __guard__(TurkServer.batch(), x2 => x2._id)); + return Router.go("/lobby"); + } + }))); +} + +Meteor.methods({ + "toggleStatus"() { + let existing; + const userId = Meteor.userId(); + if (userId) { existing = LobbyStatus.findOne(userId); } + if (!userId || !existing) { return; } - LobbyStatus.update userId, - $set: { status: not existing.status } - -Template.tsBasicLobby.helpers - count: -> LobbyStatus.find().count() - lobbyInfo: -> LobbyStatus.find() - identifier: -> Meteor.users.findOne(@_id)?.username || "unnamed user" - -Template.tsLobby.helpers - lobbyInfo: -> LobbyStatus.find() - identifier: -> Meteor.users.findOne(@_id)?.username || @_id - readyEnabled: -> - return LobbyStatus.find().count() >= TSConfig.findOne("lobbyThreshold").value and @_id is Meteor.userId() - -Template.tsLobby.events = - "click a.changeStatus": (ev) -> - ev.preventDefault() - - Meteor.call "toggleStatus", (err, res) -> - bootbox.alert err.reason if err + return LobbyStatus.update(userId, + {$set: { status: !existing.status }}); + }}); + +Template.tsBasicLobby.helpers({ + count() { return LobbyStatus.find().count(); }, + lobbyInfo() { return LobbyStatus.find(); }, + identifier() { return __guard__(Meteor.users.findOne(this._id), x2 => x2.username) || "unnamed user"; } +}); + +Template.tsLobby.helpers({ + lobbyInfo() { return LobbyStatus.find(); }, + identifier() { return __guard__(Meteor.users.findOne(this._id), x2 => x2.username) || this._id; }, + readyEnabled() { + return (LobbyStatus.find().count() >= TSConfig.findOne("lobbyThreshold").value) && (this._id === Meteor.userId()); + } +}); + +Template.tsLobby.events = { + "click a.changeStatus"(ev) { + ev.preventDefault(); + + return Meteor.call("toggleStatus", function(err, res) { + if (err) { return bootbox.alert(err.reason); } + }); + } +}; + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/client/logging_client.js b/client/logging_client.js index 58895c8..aec3c43 100644 --- a/client/logging_client.js +++ b/client/logging_client.js @@ -1,3 +1,7 @@ -TurkServer.log = (doc, callback) -> - Meteor.call "ts-log", doc, callback +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +TurkServer.log = (doc, callback) => Meteor.call("ts-log", doc, callback); diff --git a/client/login.js b/client/login.js index 5a5a8e6..c49c8a2 100644 --- a/client/login.js +++ b/client/login.js @@ -1,139 +1,166 @@ -unescapeURL = (s) -> - decodeURIComponent s.replace(/\+/g, "%20") - -getURLParams = -> - params = {} - m = window.location.href.match(/[\\?&]([^=]+)=([^&#]*)/g) - if m - i = 0 - while i < m.length - a = m[i].match(/.([^=]+)=(.*)/) - params[unescapeURL(a[1])] = unescapeURL(a[2]) - i++ - return params - -params = getURLParams() - -hitIsViewing = params.assignmentId and params.assignmentId is "ASSIGNMENT_ID_NOT_AVAILABLE" - -# UI helpers for login -UI.registerHelper "hitParams", params -UI.registerHelper "hitIsViewing", hitIsViewing - -# Subscribe to the currently viewed batch if in the preview page -# TODO: allow for reading meta properties later as well -if hitIsViewing and params.batchId? - Meteor.subscribe "tsLoginBatches", params.batchId - -loginCallback = (err) -> - return unless err - console.log err - if err.reason is ErrMsg.alreadyCompleted - # submit the HIT - TurkServer.submitHIT() - else - # Make sure to display this after client fully loads; otherwise error may - # not appear. (However, log out immediately as below.) - Meteor.startup -> - bootbox.dialog({ - closeButton: false - message: "

Unable to login:

" + err.message - }) - - # TODO: make this a bit more robust - # Log us out even if the resume token logged us in; copied from - # https://github.com/meteor/meteor/blob/devel/packages/accounts-base/accounts_client.js#L195 - Accounts.connection.setUserId(null) - Accounts.connection.onReconnect = null - -mturkLogin = (args) -> - Accounts.callLoginMethod - methodArguments: [ args ], - userCallback: loginCallback - -loginDialog = null - -Template.tsTestingLogin.events = - "submit form": (e, tmpl) -> - e.preventDefault() - batchId = tmpl.find("select[name=batch]").value - return unless batchId - console.log "Trying login with testing credentials" - # Save parameters (including generated stuff) and login - loginParams = _.extend @, { - batchId: batchId - test: true +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const unescapeURL = s => decodeURIComponent(s.replace(/\+/g, "%20")); + +const getURLParams = function() { + const params = {}; + const m = window.location.href.match(/[\\?&]([^=]+)=([^&#]*)/g); + if (m) { + let i = 0; + while (i < m.length) { + const a = m[i].match(/.([^=]+)=(.*)/); + params[unescapeURL(a[1])] = unescapeURL(a[2]); + i++; } + } + return params; +}; + +const params = getURLParams(); + +const hitIsViewing = params.assignmentId && (params.assignmentId === "ASSIGNMENT_ID_NOT_AVAILABLE"); + +// UI helpers for login +UI.registerHelper("hitParams", params); +UI.registerHelper("hitIsViewing", hitIsViewing); + +// Subscribe to the currently viewed batch if in the preview page +// TODO: allow for reading meta properties later as well +if (hitIsViewing && (params.batchId != null)) { + Meteor.subscribe("tsLoginBatches", params.batchId); +} + +const loginCallback = function(err) { + if (!err) { return; } + console.log(err); + if (err.reason === ErrMsg.alreadyCompleted) { + // submit the HIT + return TurkServer.submitHIT(); + } else { + // Make sure to display this after client fully loads; otherwise error may + // not appear. (However, log out immediately as below.) + Meteor.startup(() => bootbox.dialog({ + closeButton: false, + message: "

Unable to login:

" + err.message + })); + + // TODO: make this a bit more robust + // Log us out even if the resume token logged us in; copied from + // https://github.com/meteor/meteor/blob/devel/packages/accounts-base/accounts_client.js#L195 + Accounts.connection.setUserId(null); + return Accounts.connection.onReconnect = null; + } +}; + +const mturkLogin = args => Accounts.callLoginMethod({ + methodArguments: [ args ], + userCallback: loginCallback +}); + +let loginDialog = null; + +Template.tsTestingLogin.events = { + "submit form"(e, tmpl) { + e.preventDefault(); + const batchId = tmpl.find("select[name=batch]").value; + if (!batchId) { return; } + console.log("Trying login with testing credentials"); + // Save parameters (including generated stuff) and login + const loginParams = _.extend(this, { + batchId, + test: true + }); - Session.set("_loginParams", loginParams) - mturkLogin(loginParams) - - loginDialog?.modal('hide') - loginDialog = null - -# Subscribe to the list of batches only when this dialog is open -Template.tsTestingLogin.rendered = -> - @subHandle = Meteor.subscribe("tsLoginBatches") - -Template.tsTestingLogin.destroyed = -> - @subHandle.stop() - -Template.tsTestingLogin.helpers - batches: -> Batches.find() - -testLogin = -> - # FIXME hack: never run this if we are live - return if hitIsViewing - return if window.location.protocol is "https:" or window isnt window.parent - # Don't try logging in if we are logged in or already have parameters - return if Meteor.userId() or Session.get("_loginParams") - # Don't show this if we are trying to get at the admin interface - return if Router.current()?.url?.indexOf("/turkserver") is 0 + Session.set("_loginParams", loginParams); + mturkLogin(loginParams); - str = Random.id() - data = - hitId: str + "_HIT" - assignmentId: str + "_Asst" + if (loginDialog != null) { + loginDialog.modal('hide'); + } + return loginDialog = null; + } +}; + +// Subscribe to the list of batches only when this dialog is open +Template.tsTestingLogin.rendered = function() { + return this.subHandle = Meteor.subscribe("tsLoginBatches"); +}; + +Template.tsTestingLogin.destroyed = function() { + return this.subHandle.stop(); +}; + +Template.tsTestingLogin.helpers({ + batches() { return Batches.find(); }}); + +const testLogin = function() { + // FIXME hack: never run this if we are live + if (hitIsViewing) { return; } + if ((window.location.protocol === "https:") || (window !== window.parent)) { return; } + // Don't try logging in if we are logged in or already have parameters + if (Meteor.userId() || Session.get("_loginParams")) { return; } + // Don't show this if we are trying to get at the admin interface + if (__guard__(__guard__(Router.current(), x1 => x1.url), x => x.indexOf("/turkserver")) === 0) { return; } + + const str = Random.id(); + const data = { + hitId: str + "_HIT", + assignmentId: str + "_Asst", workerId: str + "_Worker" + }; loginDialog = TurkServer._displayModal(Template.tsTestingLogin, data, { - title: 'Select batch' + title: 'Select batch', message: " " - }) + }); - return +}; -# Remember our previous hit parameters unless they have been replaced -# TODO make sure this doesn't interfere with actual HITs -if params.hitId and params.assignmentId and params.workerId +// Remember our previous hit parameters unless they have been replaced +// TODO make sure this doesn't interfere with actual HITs +if (params.hitId && params.assignmentId && params.workerId) { Session.set("_loginParams", { - hitId: params.hitId - assignmentId: params.assignmentId - workerId: params.workerId - batchId: params.batchId - # TODO: hack to allow testing logins - test: params.test? || params.workerId.indexOf("_Worker") >= 0 - }) - Meteor._debug "Captured login params" - -# Recover either page params or stored session params as above -loginParams = Session.get("_loginParams") - -if loginParams - - Meteor._debug "Logging in with captured or stored parameters" - mturkLogin(loginParams) -else - # Give enough time to log in some other way before showing login dialog - TurkServer._delayedStartup testLogin, 1000 - -# TODO Testing disconnect and reconnect, remove later -TurkServer.testingLogin = -> - if Meteor.user() - console.log "Already logged in." - return - unless Session.get("_loginParams") - console.log "No parameters saved." - return - mturkLogin(Session.get("_loginParams")) - + hitId: params.hitId, + assignmentId: params.assignmentId, + workerId: params.workerId, + batchId: params.batchId, + // TODO: hack to allow testing logins + test: (params.test != null) || (params.workerId.indexOf("_Worker") >= 0) + }); + Meteor._debug("Captured login params"); +} + +// Recover either page params or stored session params as above +const loginParams = Session.get("_loginParams"); + +if (loginParams) { + + Meteor._debug("Logging in with captured or stored parameters"); + mturkLogin(loginParams); +} else { + // Give enough time to log in some other way before showing login dialog + TurkServer._delayedStartup(testLogin, 1000); +} + +// TODO Testing disconnect and reconnect, remove later +TurkServer.testingLogin = function() { + if (Meteor.user()) { + console.log("Already logged in."); + return; + } + if (!Session.get("_loginParams")) { + console.log("No parameters saved."); + return; + } + return mturkLogin(Session.get("_loginParams")); +}; + + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/client/timers_client.js b/client/timers_client.js index 63ba9cb..5a08b48 100644 --- a/client/timers_client.js +++ b/client/timers_client.js @@ -1,104 +1,151 @@ -### +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS203: Remove `|| {}` from converted for-own loops + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +/* Reactive time functions The instance time functions are also used in the admin interface to compute individual users' stats. -### -class TurkServer.Timers - _currentAssignmentInstance = -> - return if TurkServer.isAdmin() - return unless (group = TurkServer.group())? - _.find(Assignments.findOne()?.instances, (inst) -> inst.id is group) - - _joinedTime = (instance, serverTime) -> Math.max(0, serverTime - instance.joinTime) - - _idleTime = (instance, serverTime) -> - idleMillis = (instance.idleTime || 0) - # If we're idle, add the time since we went idle - # TODO add a test for this part - if instance.lastIdle? - idleMillis += serverTime - instance.lastIdle - return idleMillis - - _disconnectedTime = (instance, serverTime) -> - discMillis = instance.disconnectedTime || 0 - if instance.lastDisconnect? - discMillis += serverTime - instance.lastDisconnect - return discMillis - - # Milliseconds elapsed since experiment start - @elapsedTime: -> - return unless (exp = Experiments.findOne())? - return unless exp.startTime? - return Math.max(0, TimeSync.serverTime() - exp.startTime) - - # TODO: clean up code repetition below - - # Milliseconds elapsed since this user joined the experiment instance - # This is slightly different than the above - @joinedTime: (instance) -> - return unless (instance ?= _currentAssignmentInstance())? - serverTime = instance.leaveTime || TimeSync.serverTime() - return _joinedTime(instance, serverTime) - - @remainingTime: -> - return unless (exp = Experiments.findOne())? - return unless exp.endTime? - return Math.max(0, exp.endTime - TimeSync.serverTime()) - - ### - Emboxed values below because they aren't using per-second reactivity - ### - - # Milliseconds this user has been idle in the experiment - @idleTime: (instance) -> - return unless (instance ?= _currentAssignmentInstance())? - serverTime = instance.leaveTime || TimeSync.serverTime() - return _idleTime(instance, serverTime) - - # Milliseconds this user has been disconnected in the experiment - @disconnectedTime: (instance) -> - return unless (instance ?= _currentAssignmentInstance())? - serverTime = instance.leaveTime || TimeSync.serverTime() - return _disconnectedTime(instance, serverTime) - - @activeTime: (instance) -> - return unless (instance ?= _currentAssignmentInstance())? - # Compute this using helper functions to avoid thrashing - serverTime = instance.leaveTime || TimeSync.serverTime() - return _joinedTime(instance, serverTime) - _idleTime(instance, serverTime) - _disconnectedTime(instance, serverTime) - - # Milliseconds elapsed since round start - @roundElapsedTime: -> - return unless (round = TurkServer.currentRound())? - return unless round.startTime? - return Math.max(0, TimeSync.serverTime() - round.startTime) - - # Milliseconds until end of round - @roundRemainingTime: -> - return unless (round = TurkServer.currentRound())? - return unless round.endTime? - return Math.max(0, round.endTime - TimeSync.serverTime()) - - # Milliseconds until start of next round, if any - @breakRemainingTime: -> - return unless (round = TurkServer.currentRound())? - now = Date.now() - if (round.startTime <= now and round.endTime >= now) - # if we are not at a break, return 0 - return 0 - - # if we are at a break, we already set next round to be active. - return unless (nextRound = RoundTimers.findOne(index: round.index + 1))? - return unless nextRound.startTime? - return Math.max(0, nextRound.startTime - TimeSync.serverTime()) - -# Register all the helpers in the form tsGlobalHelperTime -for own key of TurkServer.Timers - # camelCase the helper name - helperName = "ts" + key.charAt(0).toUpperCase() + key.slice(1) - (-> # Bind the function to the current value inside the closure - func = TurkServer.Timers[key] - UI.registerHelper helperName, -> - TurkServer.Util.formatMillis func.apply(this, arguments) - )() +*/ +(function() { + let _currentAssignmentInstance = undefined; + let _joinedTime = undefined; + let _idleTime = undefined; + let _disconnectedTime = undefined; + const Cls = (TurkServer.Timers = class Timers { + static initClass() { + _currentAssignmentInstance = function() { + let group; + if (TurkServer.isAdmin()) { return; } + if ((group = TurkServer.group()) == null) { return; } + return _.find(__guard__(Assignments.findOne(), x => x.instances), inst => inst.id === group); + }; + + _joinedTime = (instance, serverTime) => Math.max(0, serverTime - instance.joinTime); + + _idleTime = function(instance, serverTime) { + let idleMillis = (instance.idleTime || 0); + // If we're idle, add the time since we went idle + // TODO add a test for this part + if (instance.lastIdle != null) { + idleMillis += serverTime - instance.lastIdle; + } + return idleMillis; + }; + + _disconnectedTime = function(instance, serverTime) { + let discMillis = instance.disconnectedTime || 0; + if (instance.lastDisconnect != null) { + discMillis += serverTime - instance.lastDisconnect; + } + return discMillis; + }; + } + + // Milliseconds elapsed since experiment start + static elapsedTime() { + let exp; + if ((exp = Experiments.findOne()) == null) { return; } + if (exp.startTime == null) { return; } + return Math.max(0, TimeSync.serverTime() - exp.startTime); + } + + // TODO: clean up code repetition below + + // Milliseconds elapsed since this user joined the experiment instance + // This is slightly different than the above + static joinedTime(instance) { + if ((instance != null ? instance : (instance = _currentAssignmentInstance())) == null) { return; } + const serverTime = instance.leaveTime || TimeSync.serverTime(); + return _joinedTime(instance, serverTime); + } + + static remainingTime() { + let exp; + if ((exp = Experiments.findOne()) == null) { return; } + if (exp.endTime == null) { return; } + return Math.max(0, exp.endTime - TimeSync.serverTime()); + } + + /* + Emboxed values below because they aren't using per-second reactivity + */ + + // Milliseconds this user has been idle in the experiment + static idleTime(instance) { + if ((instance != null ? instance : (instance = _currentAssignmentInstance())) == null) { return; } + const serverTime = instance.leaveTime || TimeSync.serverTime(); + return _idleTime(instance, serverTime); + } + + // Milliseconds this user has been disconnected in the experiment + static disconnectedTime(instance) { + if ((instance != null ? instance : (instance = _currentAssignmentInstance())) == null) { return; } + const serverTime = instance.leaveTime || TimeSync.serverTime(); + return _disconnectedTime(instance, serverTime); + } + + static activeTime(instance) { + if ((instance != null ? instance : (instance = _currentAssignmentInstance())) == null) { return; } + // Compute this using helper functions to avoid thrashing + const serverTime = instance.leaveTime || TimeSync.serverTime(); + return _joinedTime(instance, serverTime) - _idleTime(instance, serverTime) - _disconnectedTime(instance, serverTime); + } + + // Milliseconds elapsed since round start + static roundElapsedTime() { + let round; + if ((round = TurkServer.currentRound()) == null) { return; } + if (round.startTime == null) { return; } + return Math.max(0, TimeSync.serverTime() - round.startTime); + } + + // Milliseconds until end of round + static roundRemainingTime() { + let round; + if ((round = TurkServer.currentRound()) == null) { return; } + if (round.endTime == null) { return; } + return Math.max(0, round.endTime - TimeSync.serverTime()); + } + + // Milliseconds until start of next round, if any + static breakRemainingTime() { + let nextRound, round; + if ((round = TurkServer.currentRound()) == null) { return; } + const now = Date.now(); + if ((round.startTime <= now) && (round.endTime >= now)) { + // if we are not at a break, return 0 + return 0; + } + + // if we are at a break, we already set next round to be active. + if ((nextRound = RoundTimers.findOne({index: round.index + 1})) == null) { return; } + if (nextRound.startTime == null) { return; } + return Math.max(0, nextRound.startTime - TimeSync.serverTime()); + } + }); + Cls.initClass(); + return Cls; +})(); + +// Register all the helpers in the form tsGlobalHelperTime +for (var key of Object.keys(TurkServer.Timers || {})) { + // camelCase the helper name + var helperName = "ts" + key.charAt(0).toUpperCase() + key.slice(1); + (function() { // Bind the function to the current value inside the closure + const func = TurkServer.Timers[key]; + return UI.registerHelper(helperName, function() { + return TurkServer.Util.formatMillis(func.apply(this, arguments)); + }); + })(); +} + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/client/ts_client.js b/client/ts_client.js index 05b9799..3a66d84 100644 --- a/client/ts_client.js +++ b/client/ts_client.js @@ -1,41 +1,61 @@ -# Client-only pseudo collection that receives experiment metadata -@TSConfig = new Mongo.Collection("ts.config") - -TurkServer.batch = -> - if (batchId = Session.get('_loginParams')?.batchId)? - return Batches.findOne(batchId) - else - return Batches.findOne() - -### +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * DS208: Avoid top-level this + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +// Client-only pseudo collection that receives experiment metadata +this.TSConfig = new Mongo.Collection("ts.config"); + +TurkServer.batch = function() { + let batchId; + if ((batchId = __guard__(Session.get('_loginParams'), x => x.batchId)) != null) { + return Batches.findOne(batchId); + } else { + return Batches.findOne(); + } +}; + +/* Reactive computations -### - -# TODO perhaps make a better version of this reactivity -Deps.autorun -> - userId = Meteor.userId() - return unless userId - turkserver = Meteor.users.findOne( - _id: userId +*/ + +// TODO perhaps make a better version of this reactivity +Deps.autorun(function() { + const userId = Meteor.userId(); + if (!userId) { return; } + const turkserver = __guard__(Meteor.users.findOne({ + _id: userId, "turkserver.state": { $exists: true } - , fields: + } + , { fields: { "turkserver.state" : 1 - )?.turkserver - return unless turkserver + } +} + ), x => x.turkserver); + if (!turkserver) { return; } + + return Session.set("turkserver.state", turkserver.state); +}); - Session.set("turkserver.state", turkserver.state) +Deps.autorun(() => Meteor.subscribe("tsCurrentExperiment", Partitioner.group())); -Deps.autorun -> - Meteor.subscribe("tsCurrentExperiment", Partitioner.group()) +// Reactive join on treatments for assignments and experiments +Deps.autorun(function() { + const exp = Experiments.findOne({}, {fields: {treatments: 1}}); + if (!exp || (exp.treatments == null)) { return; } + return Meteor.subscribe("tsTreatments", exp.treatments); +}); -# Reactive join on treatments for assignments and experiments -Deps.autorun -> - exp = Experiments.findOne({}, {fields: {treatments: 1}}) - return unless exp && exp.treatments? - Meteor.subscribe("tsTreatments", exp.treatments) +Deps.autorun(function() { + const asst = Assignments.findOne({}, {fields: {treatments: 1}}); + if (!asst || (asst.treatments == null)) { return; } + return Meteor.subscribe("tsTreatments", asst.treatments); +}); -Deps.autorun -> - asst = Assignments.findOne({}, {fields: {treatments: 1}}) - return unless asst && asst.treatments? - Meteor.subscribe("tsTreatments", asst.treatments) +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/lib/common.js b/lib/common.js index 91d59ec..0d42b2f 100644 --- a/lib/common.js +++ b/lib/common.js @@ -1,74 +1,100 @@ -# Clean up old index -if Meteor.isServer - try - RoundTimers._dropIndex("_groupId_1_index_1") - Meteor._debug("Dropped old non-unique index on RoundTimers") +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * DS208: Avoid top-level this + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +// Clean up old index +if (Meteor.isServer) { + try { + RoundTimers._dropIndex("_groupId_1_index_1"); + Meteor._debug("Dropped old non-unique index on RoundTimers"); + } catch (error) {} +} -TurkServer.partitionCollection RoundTimers, { +TurkServer.partitionCollection(RoundTimers, { index: {index: 1}, indexOptions: { unique: 1, dropDups: true, name: "_groupId_1_index_1_unique" } -} +}); -@Workers = new Mongo.Collection("ts.workers") -@Assignments = new Mongo.Collection("ts.assignments") +this.Workers = new Mongo.Collection("ts.workers"); +this.Assignments = new Mongo.Collection("ts.assignments"); -@WorkerEmails = new Mongo.Collection("ts.workeremails") -@Qualifications = new Mongo.Collection("ts.qualifications") -@HITTypes = new Mongo.Collection("ts.hittypes") -@HITs = new Mongo.Collection("ts.hits") +this.WorkerEmails = new Mongo.Collection("ts.workeremails"); +this.Qualifications = new Mongo.Collection("ts.qualifications"); +this.HITTypes = new Mongo.Collection("ts.hittypes"); +this.HITs = new Mongo.Collection("ts.hits"); -ErrMsg = - # authentication - unexpectedBatch: "This HIT is not recognized." - batchInactive: "This task is currently not accepting new assignments." - batchLimit: "You've attempted or completed the maximum number of HITs allowed in this group. Please return this assignment." - simultaneousLimit: "You are already connected through another HIT, or you previously returned a HIT from this group. If you still have the HIT open, please complete that one first." - alreadyCompleted: "You have already completed this HIT." - # operations - authErr: "You are not logged in" - stateErr: "You can't perform that operation right now" - notAdminErr: "Not logged in as admin" - adminErr: "Operation not permitted by admin" - # Stuff - usernameTaken: "Sorry, that username is taken." +const ErrMsg = { + // authentication + unexpectedBatch: "This HIT is not recognized.", + batchInactive: "This task is currently not accepting new assignments.", + batchLimit: "You've attempted or completed the maximum number of HITs allowed in this group. Please return this assignment.", + simultaneousLimit: "You are already connected through another HIT, or you previously returned a HIT from this group. If you still have the HIT open, please complete that one first.", + alreadyCompleted: "You have already completed this HIT.", + // operations + authErr: "You are not logged in", + stateErr: "You can't perform that operation right now", + notAdminErr: "Not logged in as admin", + adminErr: "Operation not permitted by admin", + // Stuff + usernameTaken: "Sorry, that username is taken.", userNotInLobbyErr: "User is not in lobby" +}; -# TODO move this to a more appropriate location -Meteor.methods - "ts-delete-treatment": (id) -> - TurkServer.checkAdmin() - if Batches.findOne({ treatmentIds: { $in: [id] } }) - throw new Meteor.Error(403, "can't delete treatments that are used by existing batches") +// TODO move this to a more appropriate location +Meteor.methods({ + "ts-delete-treatment"(id) { + TurkServer.checkAdmin(); + if (Batches.findOne({ treatmentIds: { $in: [id] } })) { + throw new Meteor.Error(403, "can't delete treatments that are used by existing batches"); + } - Treatments.remove(id) + return Treatments.remove(id); + } +}); -# Helpful functions +// Helpful functions -# Check if a particular user is an admin. -# If no user is specified, attempts to check the current user. -TurkServer.isAdmin = (userId) -> - userId ?= Meteor.userId() - return false unless userId - return Meteor.users.findOne( - _id: userId +// Check if a particular user is an admin. +// If no user is specified, attempts to check the current user. +TurkServer.isAdmin = function(userId) { + if (userId == null) { userId = Meteor.userId(); } + if (!userId) { return false; } + return __guard__(Meteor.users.findOne({ + _id: userId, "admin": { $exists: true } - , fields: + } + , { fields: { "admin" : 1 - )?.admin || false + } +} + ), x => x.admin) || false; +}; + +TurkServer.checkNotAdmin = function() { + if (Meteor.isClient) { + // Don't register reactive dependencies on userId for a client check + if (Deps.nonreactive(() => TurkServer.isAdmin())) { throw new Meteor.Error(403, ErrMsg.adminErr); } + } else { + if (TurkServer.isAdmin()) { throw new Meteor.Error(403, ErrMsg.adminErr); } + } +}; -TurkServer.checkNotAdmin = -> - if Meteor.isClient - # Don't register reactive dependencies on userId for a client check - throw new Meteor.Error(403, ErrMsg.adminErr) if Deps.nonreactive(-> TurkServer.isAdmin()) - else - throw new Meteor.Error(403, ErrMsg.adminErr) if TurkServer.isAdmin() +TurkServer.checkAdmin = function() { + if (Meteor.isClient) { + if (!Deps.nonreactive(() => TurkServer.isAdmin())) { throw new Meteor.Error(403, ErrMsg.notAdminErr); } + } else { + if (!TurkServer.isAdmin()) { throw new Meteor.Error(403, ErrMsg.notAdminErr); } + } +}; -TurkServer.checkAdmin = -> - if Meteor.isClient - throw new Meteor.Error(403, ErrMsg.notAdminErr) unless Deps.nonreactive(-> TurkServer.isAdmin()) - else - throw new Meteor.Error(403, ErrMsg.notAdminErr) unless TurkServer.isAdmin() +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/lib/util.js b/lib/util.js index fa2a5ac..7c5542f 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,21 +1,30 @@ -### +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +/* Server/client util files -### +*/ -TurkServer.Util ?= {} +if (TurkServer.Util == null) { TurkServer.Util = {}; } -TurkServer.Util.formatMillis = (millis) -> - return unless millis? # Can be 0 in which case we should render it - negative = (millis < 0) - diff = moment.utc(Math.abs(millis)) - time = diff.format("H:mm:ss") - days = +diff.format("DDD") - 1 - return (if negative then "-" else "") + (if days then days + "d " else "") + time +TurkServer.Util.formatMillis = function(millis) { + if (millis == null) { return; } // Can be 0 in which case we should render it + const negative = (millis < 0); + const diff = moment.utc(Math.abs(millis)); + const time = diff.format("H:mm:ss"); + const days = +diff.format("DDD") - 1; + return (negative ? "-" : "") + (days ? days + "d " : "") + time; +}; -TurkServer._mergeTreatments = (arr) -> - fields = - treatments: [] - arr.forEach (treatment) -> - fields.treatments.push treatment.name - _.extend(fields, _.omit(treatment, "_id", "name")) - return fields +TurkServer._mergeTreatments = function(arr) { + const fields = + {treatments: []}; + arr.forEach(function(treatment) { + fields.treatments.push(treatment.name); + return _.extend(fields, _.omit(treatment, "_id", "name")); + }); + return fields; +}; diff --git a/server/accounts_mturk.js b/server/accounts_mturk.js index a78be18..cda9925 100644 --- a/server/accounts_mturk.js +++ b/server/accounts_mturk.js @@ -1,134 +1,156 @@ -### +/* + * decaffeinate suggestions: + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +/* Add a hook to Meteor's login system: To account for for MTurk use, except for admin users for users who are not currently assigned to a HIT. -### -Accounts.validateLoginAttempt (info) -> - return true if info.user?.admin # Always allow admin to login - - # If resuming, is the worker currently assigned to a HIT? - # TODO add a test for this - if info.methodArguments[0].resume? - unless info.user?.workerId and Assignments.findOne( - workerId: info.user.workerId +*/ +Accounts.validateLoginAttempt(function(info) { + if (info.user != null ? info.user.admin : undefined) { return true; } // Always allow admin to login + + // If resuming, is the worker currently assigned to a HIT? + // TODO add a test for this + if (info.methodArguments[0].resume != null) { + if (!(info.user != null ? info.user.workerId : undefined) || !Assignments.findOne({ + workerId: info.user.workerId, status: "assigned" - ) - throw new Meteor.Error(403, "Your HIT session has expired.") + })) { + throw new Meteor.Error(403, "Your HIT session has expired."); + } + } - # TODO Does the worker have this open in another window? If so, reject the login. - # This is a bit fail-prone due to leaking sessions across HCR, so take it out. -# if info.user? and UserStatus.connections.findOne(userId: info.user._id) -# throw new Meteor.Error(403, "You already have this open in another window. Complete it there.") + // TODO Does the worker have this open in another window? If so, reject the login. + // This is a bit fail-prone due to leaking sessions across HCR, so take it out. +// if info.user? and UserStatus.connections.findOne(userId: info.user._id) +// throw new Meteor.Error(403, "You already have this open in another window. Complete it there.") - return true + return true; +}); -### +/* Authenticate a worker taking an assignment. Returns an assignment object corresponding to the assignment. -### -authenticateWorker = (loginRequest) -> - { batchId, hitId, assignmentId, workerId } = loginRequest - - # check if batchId is correct except for testing logins - unless loginRequest.test or TurkServer.config.hits.acceptUnknownHits - hit = HITs.findOne - HITId: hitId - hitType = HITTypes.findOne - HITTypeId: hit.HITTypeId - throw new Meteor.Error(403, ErrMsg.unexpectedBatch) unless batchId is hitType.batchId - - # Has this worker already completed the HIT? - if Assignments.findOne({ - hitId - assignmentId - workerId +*/ +const authenticateWorker = function(loginRequest) { + const { batchId, hitId, assignmentId, workerId } = loginRequest; + + // check if batchId is correct except for testing logins + if (!loginRequest.test && !TurkServer.config.hits.acceptUnknownHits) { + const hit = HITs.findOne({ + HITId: hitId}); + const hitType = HITTypes.findOne({ + HITTypeId: hit.HITTypeId}); + if (batchId !== hitType.batchId) { throw new Meteor.Error(403, ErrMsg.unexpectedBatch); } + } + + // Has this worker already completed the HIT? + if (Assignments.findOne({ + hitId, + assignmentId, + workerId, status: "completed" - }) - # makes the client auto-submit with this error - throw new Meteor.Error(403, ErrMsg.alreadyCompleted) - - # Is this already assigned to someone? - existing = Assignments.findOne - hitId: hitId - assignmentId: assignmentId + })) { + // makes the client auto-submit with this error + throw new Meteor.Error(403, ErrMsg.alreadyCompleted); + } + + // Is this already assigned to someone? + const existing = Assignments.findOne({ + hitId, + assignmentId, status: "assigned" + }); + + if (existing) { + // Was a different account in progress? + const existingAsst = TurkServer.Assignment.getAssignment(existing._id); + if (workerId === existing.workerId) { + // Worker has already logged in to this HIT, no need to create record below + return existingAsst; + } else { + // HIT has been taken by someone else. Record a new assignment for this worker. + existingAsst.setReturned(); + } + } - if existing - # Was a different account in progress? - existingAsst = TurkServer.Assignment.getAssignment(existing._id) - if workerId is existing.workerId - # Worker has already logged in to this HIT, no need to create record below - return existingAsst - else - # HIT has been taken by someone else. Record a new assignment for this worker. - existingAsst.setReturned() - - ### + /* Not a reconnection; we may create a new assignment - ### - batch = Batches.findOne(batchId) + */ + const batch = Batches.findOne(batchId); - # Only active batches accept new HITs - if batchId? and not batch?.active - throw new Meteor.Error(403, ErrMsg.batchInactive) + // Only active batches accept new HITs + if ((batchId != null) && !(batch != null ? batch.active : undefined)) { + throw new Meteor.Error(403, ErrMsg.batchInactive); + } - # Limits - simultaneously accepted HITs - if Assignments.find({ - workerId: workerId, + // Limits - simultaneously accepted HITs + if (Assignments.find({ + workerId, status: { $nin: [ "completed", "returned" ] } - }).count() >= TurkServer.config.experiment.limit.simultaneous - throw new Meteor.Error(403, ErrMsg.simultaneousLimit) - - # Limits for the given batch - predicate = - workerId: loginRequest.workerId - batchId: batchId - - predicate.status = { $ne: "returned" } if batch.allowReturns - - if Assignments.find(predicate).count() >= TurkServer.config.experiment.limit.batch - throw new Meteor.Error(403, ErrMsg.batchLimit) - - # Either no one has this assignment before or this worker replaced someone; - # Create a new record for this worker on this assignment - return TurkServer.Assignment.createAssignment - batchId: batchId - hitId: loginRequest.hitId - assignmentId: loginRequest.assignmentId - workerId: loginRequest.workerId - acceptTime: new Date() - status: "assigned" + }).count() >= TurkServer.config.experiment.limit.simultaneous) { + throw new Meteor.Error(403, ErrMsg.simultaneousLimit); + } + + // Limits for the given batch + const predicate = { + workerId: loginRequest.workerId, + batchId + }; + + if (batch.allowReturns) { predicate.status = { $ne: "returned" }; } + + if (Assignments.find(predicate).count() >= TurkServer.config.experiment.limit.batch) { + throw new Meteor.Error(403, ErrMsg.batchLimit); + } -Accounts.registerLoginHandler "mturk", (loginRequest) -> - # Don't handle unless we have an mturk login - return unless loginRequest.hitId and loginRequest.assignmentId and loginRequest.workerId - - # At some point this became processed as part of a method call - # (DDP._CurrentInvocation.get() is defined), so we need the direct or this - # would fail with a partitioner error. - user = Meteor.users.direct.findOne - workerId: loginRequest.workerId - - unless user - # Use the provided method of creating users - userId = Accounts.insertUserDoc {}, - workerId: loginRequest.workerId - else + // Either no one has this assignment before or this worker replaced someone; + // Create a new record for this worker on this assignment + return TurkServer.Assignment.createAssignment({ + batchId, + hitId: loginRequest.hitId, + assignmentId: loginRequest.assignmentId, + workerId: loginRequest.workerId, + acceptTime: new Date(), + status: "assigned" + }); +}; + +Accounts.registerLoginHandler("mturk", function(loginRequest) { + // Don't handle unless we have an mturk login + let userId; + if (!loginRequest.hitId || !loginRequest.assignmentId || !loginRequest.workerId) { return; } + + // At some point this became processed as part of a method call + // (DDP._CurrentInvocation.get() is defined), so we need the direct or this + // would fail with a partitioner error. + const user = Meteor.users.direct.findOne({ + workerId: loginRequest.workerId}); + + if (!user) { + // Use the provided method of creating users + userId = Accounts.insertUserDoc({}, + {workerId: loginRequest.workerId}); + } else { userId = user._id; + } - # should we let this worker in or not? - asst = authenticateWorker(loginRequest) + // should we let this worker in or not? + const asst = authenticateWorker(loginRequest); - # This currently does nothing except print out some messages. - Meteor.defer -> asst._loggedIn() + // This currently does nothing except print out some messages. + Meteor.defer(() => asst._loggedIn()); - # Because the login token `when` field is set by initialization date, not - # expiration date, we can't artificially make this login expire sooner here. - # So we'll need to aggressively prune logins when a HIT is submitted, instead. + // Because the login token `when` field is set by initialization date, not + // expiration date, we can't artificially make this login expire sooner here. + // So we'll need to aggressively prune logins when a HIT is submitted, instead. return { - userId: userId, - } + userId, + }; +}); -# Test exports -TestUtils.authenticateWorker = authenticateWorker +// Test exports +TestUtils.authenticateWorker = authenticateWorker; diff --git a/server/batches.js b/server/batches.js index 5ddafbe..a1369d8 100644 --- a/server/batches.js +++ b/server/batches.js @@ -1,58 +1,85 @@ -class TurkServer.Batch - _batches = {} - - @getBatch: (batchId) -> - check(batchId, String) - if (batch = _batches[batchId])? - return batch - else - throw new Error("Batch does not exist") unless Batches.findOne(batchId)? - # Return this if another Fiber created it while we yielded - return _batches[batchId] ?= new Batch(batchId) - - @getBatchByName: (batchName) -> - check(batchName, String) - batch = Batches.findOne(name: batchName) - throw new Error("Batch does not exist") unless batch - return @getBatch(batch._id) - - @currentBatch: -> - return unless (userId = Meteor.userId())? - return TurkServer.Assignment.getCurrentUserAssignment(userId).getBatch() - - constructor: (@batchId) -> - throw new Error("Batch already exists; use getBatch") if _batches[@batchId]? - @lobby = new TurkServer.Lobby(@batchId) - - # Creating an instance does not set it up, or initialize the start time. - createInstance: (treatmentNames, fields) -> - fields = _.extend fields || {}, - batchId: @batchId - treatments: treatmentNames || [] - - groupId = Experiments.insert(fields) - - # To prevent bugs if the instance is referenced before this returns, we - # need to go through getInstance. - instance = TurkServer.Instance.getInstance(groupId) - - instance.bindOperation -> - TurkServer.log - _meta: "created" - - return instance - - getTreatments: -> Batches.findOne(@batchId).treatments - - setAssigner: (assigner) -> - throw new Error("Assigner already set for this batch") if @assigner? - @assigner = assigner - assigner.initialize(this) - -TurkServer.ensureBatchExists = (props) -> - throw new Error("Batch must have a name") unless props.name? - Batches.upsert {name: props.name}, props - -TurkServer.ensureTreatmentExists = (props) -> - throw new Error("Treatment must have a name") unless props.name? - Treatments.upsert {name: props.name}, props +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +(function() { + let _batches = undefined; + const Cls = (TurkServer.Batch = class Batch { + static initClass() { + _batches = {}; + } + + static getBatch(batchId) { + let batch; + check(batchId, String); + if ((batch = _batches[batchId]) != null) { + return batch; + } else { + if (Batches.findOne(batchId) == null) { throw new Error("Batch does not exist"); } + // Return this if another Fiber created it while we yielded + return _batches[batchId] != null ? _batches[batchId] : (_batches[batchId] = new Batch(batchId)); + } + } + + static getBatchByName(batchName) { + check(batchName, String); + const batch = Batches.findOne({name: batchName}); + if (!batch) { throw new Error("Batch does not exist"); } + return this.getBatch(batch._id); + } + + static currentBatch() { + let userId; + if ((userId = Meteor.userId()) == null) { return; } + return TurkServer.Assignment.getCurrentUserAssignment(userId).getBatch(); + } + + constructor(batchId) { + this.batchId = batchId; + if (_batches[this.batchId] != null) { throw new Error("Batch already exists; use getBatch"); } + this.lobby = new TurkServer.Lobby(this.batchId); + } + + // Creating an instance does not set it up, or initialize the start time. + createInstance(treatmentNames, fields) { + fields = _.extend(fields || {}, { + batchId: this.batchId, + treatments: treatmentNames || [] + }); + + const groupId = Experiments.insert(fields); + + // To prevent bugs if the instance is referenced before this returns, we + // need to go through getInstance. + const instance = TurkServer.Instance.getInstance(groupId); + + instance.bindOperation(() => TurkServer.log({ + _meta: "created"})); + + return instance; + } + + getTreatments() { return Batches.findOne(this.batchId).treatments; } + + setAssigner(assigner) { + if (this.assigner != null) { throw new Error("Assigner already set for this batch"); } + this.assigner = assigner; + return assigner.initialize(this); + } + }); + Cls.initClass(); + return Cls; +})(); + +TurkServer.ensureBatchExists = function(props) { + if (props.name == null) { throw new Error("Batch must have a name"); } + return Batches.upsert({name: props.name}, props); +}; + +TurkServer.ensureTreatmentExists = function(props) { + if (props.name == null) { throw new Error("Treatment must have a name"); } + return Treatments.upsert({name: props.name}, props); +}; diff --git a/server/connections.js b/server/connections.js index 61d97f4..7793268 100644 --- a/server/connections.js +++ b/server/connections.js @@ -1,252 +1,287 @@ -attemptCallbacks = (callbacks, context, errMsg) -> - for cb in callbacks - try - cb.call(context) - catch e - Meteor._debug errMsg, e - -connectCallbacks = [] -disconnectCallbacks = [] -idleCallbacks = [] -activeCallbacks = [] - -TurkServer.onConnect = (func) -> connectCallbacks.push(func) -TurkServer.onDisconnect = (func) -> disconnectCallbacks.push(func) -TurkServer.onIdle = (func) -> idleCallbacks.push(func) -TurkServer.onActive = (func) -> activeCallbacks.push(func) - -# When getting user records in a session callback, we have to check if admin -getUserNonAdmin = (userId) -> - user = Meteor.users.findOne(userId) - return if not user? or user?.admin - return user - -### +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const attemptCallbacks = (callbacks, context, errMsg) => Array.from(callbacks).map((cb) => + (() => { try { + return cb.call(context); + } catch (e) { + return Meteor._debug(errMsg, e); + } })()); + +const connectCallbacks = []; +const disconnectCallbacks = []; +const idleCallbacks = []; +const activeCallbacks = []; + +TurkServer.onConnect = func => connectCallbacks.push(func); +TurkServer.onDisconnect = func => disconnectCallbacks.push(func); +TurkServer.onIdle = func => idleCallbacks.push(func); +TurkServer.onActive = func => activeCallbacks.push(func); + +// When getting user records in a session callback, we have to check if admin +const getUserNonAdmin = function(userId) { + const user = Meteor.users.findOne(userId); + if ((user == null) || (user != null ? user.admin : undefined)) { return; } + return user; +}; + +/* Connect/disconnect callbacks In the methods below, we use Partitioner.getUserGroup(userId) because user.group takes a moment to be propagated. -### -sessionReconnect = (doc) -> - return unless getUserNonAdmin(doc.userId)? +*/ +const sessionReconnect = function(doc) { + if (getUserNonAdmin(doc.userId) == null) { return; } - asst = TurkServer.Assignment.getCurrentUserAssignment(doc.userId) + const asst = TurkServer.Assignment.getCurrentUserAssignment(doc.userId); - # TODO possible debug message, but probably caught below. - return unless asst? + // TODO possible debug message, but probably caught below. + if (asst == null) { return; } - # Save IP address and UA; multiple connections from different IPs/browsers - # are recorded for diagnostic purposes. - asst._update + // Save IP address and UA; multiple connections from different IPs/browsers + // are recorded for diagnostic purposes. + return asst._update({ $addToSet: { - ipAddr: doc.ipAddr + ipAddr: doc.ipAddr, userAgent: doc.userAgent - } - -userReconnect = (user) -> - asst = TurkServer.Assignment.getCurrentUserAssignment(user._id) - - unless asst? - Meteor._debug("#{user._id} reconnected but has no active assignment") - # TODO maybe kick this user out and show an error - return - - # Ensure user is in a valid state; add to lobby if not - state = user.turkserver?.state - if state is "lobby" or not state? - asst._enterLobby() - return - - # We only call the group operations below if the user was in a group at the - # time of connection - return unless (groupId = Partitioner.getUserGroup(user._id))? - asst._reconnected(groupId) - - TurkServer.Instance.getInstance(groupId).bindOperation -> - TurkServer.log - _userId: user._id + }}); +}; + +const userReconnect = function(user) { + let groupId; + const asst = TurkServer.Assignment.getCurrentUserAssignment(user._id); + + if (asst == null) { + Meteor._debug(`${user._id} reconnected but has no active assignment`); + // TODO maybe kick this user out and show an error + return; + } + + // Ensure user is in a valid state; add to lobby if not + const state = user.turkserver != null ? user.turkserver.state : undefined; + if ((state === "lobby") || (state == null)) { + asst._enterLobby(); + return; + } + + // We only call the group operations below if the user was in a group at the + // time of connection + if ((groupId = Partitioner.getUserGroup(user._id)) == null) { return; } + asst._reconnected(groupId); + + return TurkServer.Instance.getInstance(groupId).bindOperation(function() { + TurkServer.log({ + _userId: user._id, _meta: "connected" + }); - attemptCallbacks(connectCallbacks, this, "Exception in user connect callback") - return + attemptCallbacks(connectCallbacks, this, "Exception in user connect callback"); + } , { - userId: user._id + userId: user._id, event: "connected" - } + }); +}; -userDisconnect = (user) -> - asst = TurkServer.Assignment.getCurrentUserAssignment(user._id) +const userDisconnect = function(user) { + let groupId; + const asst = TurkServer.Assignment.getCurrentUserAssignment(user._id); - # If they are disconnecting after completing an assignment, there will be no - # current assignment. - return unless asst? + // If they are disconnecting after completing an assignment, there will be no + // current assignment. + if (asst == null) { return; } - # If user was in lobby, remove them - asst._removeFromLobby() + // If user was in lobby, remove them + asst._removeFromLobby(); - return unless (groupId = Partitioner.getUserGroup(user._id))? - asst._disconnected(groupId) + if ((groupId = Partitioner.getUserGroup(user._id)) == null) { return; } + asst._disconnected(groupId); - TurkServer.Instance.getInstance(groupId).bindOperation -> - TurkServer.log - _userId: user._id + return TurkServer.Instance.getInstance(groupId).bindOperation(function() { + TurkServer.log({ + _userId: user._id, _meta: "disconnected" + }); - attemptCallbacks(disconnectCallbacks, this, "Exception in user disconnect callback") - return + attemptCallbacks(disconnectCallbacks, this, "Exception in user disconnect callback"); + } , { - userId: user._id + userId: user._id, event: "disconnected" - } + }); +}; -### +/* Idle and returning from idle -### +*/ -userIdle = (user) -> - return unless (groupId = Partitioner.getUserGroup(user._id))? +const userIdle = function(user) { + let groupId; + if ((groupId = Partitioner.getUserGroup(user._id)) == null) { return; } - asst = TurkServer.Assignment.getCurrentUserAssignment(user._id) - asst._isIdle(groupId, user.status.lastActivity) + const asst = TurkServer.Assignment.getCurrentUserAssignment(user._id); + asst._isIdle(groupId, user.status.lastActivity); - TurkServer.Instance.getInstance(groupId).bindOperation -> - TurkServer.log - _userId: user._id - _meta: "idle" - _timestamp: user.status.lastActivity # Overridden to a past value + return TurkServer.Instance.getInstance(groupId).bindOperation(function() { + TurkServer.log({ + _userId: user._id, + _meta: "idle", + _timestamp: user.status.lastActivity + }); // Overridden to a past value - attemptCallbacks(idleCallbacks, this, "Exception in user idle callback") - return + attemptCallbacks(idleCallbacks, this, "Exception in user idle callback"); + } , { - userId: user._id + userId: user._id, event: "idle" - } + }); +}; -# Because activity on any session will make a user active, we use this in -# order to properly record the last activity time on the client -sessionActive = (doc) -> - return unless getUserNonAdmin(doc.userId)? +// Because activity on any session will make a user active, we use this in +// order to properly record the last activity time on the client +const sessionActive = function(doc) { + let groupId; + if (getUserNonAdmin(doc.userId) == null) { return; } - return unless (groupId = Partitioner.getUserGroup(doc.userId))? + if ((groupId = Partitioner.getUserGroup(doc.userId)) == null) { return; } - asst = TurkServer.Assignment.getCurrentUserAssignment(doc.userId) - asst._isActive(groupId, doc.lastActivity) + const asst = TurkServer.Assignment.getCurrentUserAssignment(doc.userId); + asst._isActive(groupId, doc.lastActivity); - TurkServer.Instance.getInstance(groupId).bindOperation -> - TurkServer.log - _userId: doc.userId - _meta: "active" - _timestamp: doc.lastActivity # Also overridden + return TurkServer.Instance.getInstance(groupId).bindOperation(function() { + TurkServer.log({ + _userId: doc.userId, + _meta: "active", + _timestamp: doc.lastActivity + }); // Also overridden - attemptCallbacks(activeCallbacks, this, "Exception in user active callback") - return + attemptCallbacks(activeCallbacks, this, "Exception in user active callback"); + } , { - userId: doc.userId + userId: doc.userId, event: "active" - } + }); +}; -### +/* Hook up callbacks to events and observers -### +*/ -UserStatus.events.on "connectionLogin", sessionReconnect -# Logout / Idle are done at user level -UserStatus.events.on "connectionActive", sessionActive +UserStatus.events.on("connectionLogin", sessionReconnect); +// Logout / Idle are done at user level +UserStatus.events.on("connectionActive", sessionActive); -# This is triggered from individual connection changes via multiplexing in -# user-status. Note that `observe` is used instead of `observeChanges` because -# we're interested in the contents of the entire user document when someone goes -# online/offline or idle/active. -Meteor.startup -> +// This is triggered from individual connection changes via multiplexing in +// user-status. Note that `observe` is used instead of `observeChanges` because +// we're interested in the contents of the entire user document when someone goes +// online/offline or idle/active. +Meteor.startup(function() { Meteor.users.find({ - "admin": {$exists: false} # Excluding admin - "status.online": true # User is online + "admin": {$exists: false}, // Excluding admin + "status.online": true // User is online }).observe({ - added: userReconnect + added: userReconnect, removed: userDisconnect - }) + }); - Meteor.users.find({ - "admin": {$exists: false} # Excluding admin - "status.idle": true # User is idle + return Meteor.users.find({ + "admin": {$exists: false}, // Excluding admin + "status.idle": true // User is idle }).observe({ added: userIdle - }) + }); +}); -### +/* Test handlers - assuming user-status is working correctly, we create these convenience functions for testing users coming online and offline TODO: we might want to make these tests end-to-end so that they ensure all of the user-status functionality is working as well. -### +*/ TestUtils.connCallbacks = { - sessionReconnect: (doc) -> - sessionReconnect(doc) - userReconnect( Meteor.users.findOne(doc.userId) ) + sessionReconnect(doc) { + sessionReconnect(doc); + return userReconnect( Meteor.users.findOne(doc.userId) ); + }, - sessionDisconnect: (doc) -> - userDisconnect( Meteor.users.findOne(doc.userId) ) + sessionDisconnect(doc) { + return userDisconnect( Meteor.users.findOne(doc.userId) ); + }, - sessionIdle: (doc) -> - # We need to set the status.lastActivity field here, as in user-status, - # because the callback expects to read its value - Meteor.users.update doc.userId, - $set: {"status.lastActivity": doc.lastActivity } + sessionIdle(doc) { + // We need to set the status.lastActivity field here, as in user-status, + // because the callback expects to read its value + Meteor.users.update(doc.userId, + {$set: {"status.lastActivity": doc.lastActivity }}); - userIdle( Meteor.users.findOne(doc.userId) ) + return userIdle( Meteor.users.findOne(doc.userId) ); + }, - sessionActive: sessionActive -} + sessionActive +}; -### +/* Methods -### - -Meteor.methods - "ts-set-username": (username) -> - # TODO may need validation here due to bad browsers/bad people - userId = Meteor.userId() - return unless userId - - # No directOperation needed here since partitioner recognizes username as - # a unique index - if Meteor.users.findOne(username: username)? - throw new Meteor.Error(409, ErrMsg.usernameTaken) - - Meteor.users.update userId, - $set: {username: username} - - "ts-submit-exitdata": (doc, panel) -> - userId = Meteor.userId() - throw new Meteor.Error(403, ErrMsg.authErr) unless userId - - # TODO what if this doesn't exist? - asst = TurkServer.Assignment.currentAssignment() - # mark assignment as completed and save the data - asst.setCompleted(doc) - - # Update worker contact info - # TODO update API for writing panel data. - # TODO don't overwrite panel data if we don't need to. - if panel? - asst.setWorkerData { - contact: panel.contact +*/ + +Meteor.methods({ + "ts-set-username"(username) { + // TODO may need validation here due to bad browsers/bad people + const userId = Meteor.userId(); + if (!userId) { return; } + + // No directOperation needed here since partitioner recognizes username as + // a unique index + if (Meteor.users.findOne({username}) != null) { + throw new Meteor.Error(409, ErrMsg.usernameTaken); + } + + return Meteor.users.update(userId, + {$set: {username}}); + }, + + "ts-submit-exitdata"(doc, panel) { + let token; + const userId = Meteor.userId(); + if (!userId) { throw new Meteor.Error(403, ErrMsg.authErr); } + + // TODO what if this doesn't exist? + const asst = TurkServer.Assignment.currentAssignment(); + // mark assignment as completed and save the data + asst.setCompleted(doc); + + // Update worker contact info + // TODO update API for writing panel data. + // TODO don't overwrite panel data if we don't need to. + if (panel != null) { + asst.setWorkerData({ + contact: panel.contact, available: { - times: panel.times + times: panel.times, updated: new Date } - } - - # Destroy the token for this connection, so that a resume login will not - # be used for future HITs. Returning true should cause the HIT to submit on - # the client side, but if that doesn't work, the user will be logged out. - if (token = Accounts._getLoginToken(this.connection.id)) - # This $pulls tokens from services.resume.loginTokens, and should work - # in the same way that Accounts._expireTokens effects cleanup. - Accounts.destroyToken(userId, token) - - # return true to auto submit the HIT - return true + }); + } + + // Destroy the token for this connection, so that a resume login will not + // be used for future HITs. Returning true should cause the HIT to submit on + // the client side, but if that doesn't work, the user will be logged out. + if (token = Accounts._getLoginToken(this.connection.id)) { + // This $pulls tokens from services.resume.loginTokens, and should work + // in the same way that Accounts._expireTokens effects cleanup. + Accounts.destroyToken(userId, token); + } + + // return true to auto submit the HIT + return true; + } +}); diff --git a/server/lobby_server.js b/server/lobby_server.js index 0be415d..3af842f 100644 --- a/server/lobby_server.js +++ b/server/lobby_server.js @@ -1,110 +1,145 @@ -EventEmitter = Npm.require('events').EventEmitter - -# TODO add index on LobbyStatus if needed - -class TurkServer.Lobby - constructor: (@batchId) -> - check(@batchId, String) - @events = new EventEmitter() - - addAssignment: (asst) -> - throw new Error("unexpected batchId") if asst.batchId isnt @batchId - - # Insert or update status in lobby - LobbyStatus.upsert asst.userId, - # Simply {status: false} caused https://github.com/meteor/meteor/issues/1552 - $set: - batchId: @batchId +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { + EventEmitter +} = Npm.require('events'); + +// TODO add index on LobbyStatus if needed + +TurkServer.Lobby = class Lobby { + constructor(batchId) { + this.batchId = batchId; + check(this.batchId, String); + this.events = new EventEmitter(); + } + + addAssignment(asst) { + if (asst.batchId !== this.batchId) { throw new Error("unexpected batchId"); } + + // Insert or update status in lobby + LobbyStatus.upsert(asst.userId, { + // Simply {status: false} caused https://github.com/meteor/meteor/issues/1552 + $set: { + batchId: this.batchId, asstId: asst.asstId + } + } + ); - Meteor.users.update asst.userId, - $set: + Meteor.users.update(asst.userId, { + $set: { "turkserver.state": "lobby" - - Meteor.defer => @events.emit "user-join", asst - - getAssignments: (selector) -> - selector = _.extend selector || {}, - batchId: @batchId - TurkServer.Assignment.getAssignment(record.asstId) for record in LobbyStatus.find(selector).fetch() - - # TODO move status updates into specific assigners - toggleStatus: (userId) -> - existing = LobbyStatus.findOne(userId) - throw new Meteor.Error(403, ErrMsg.userNotInLobbyErr) unless existing - newStatus = not existing.status - LobbyStatus.update userId, - $set: { status: newStatus } - - asst = TurkServer.Assignment.getCurrentUserAssignment(userId) - Meteor.defer => @events.emit "user-status", asst, newStatus - - # Takes a group of users from the lobby without triggering the user-leave event. - pluckUsers: (userIds) -> - LobbyStatus.remove {_id : $in: userIds } - - removeAssignment: (asst) -> - # TODO check for batchId here - if LobbyStatus.remove(asst.userId) > 0 - Meteor.defer => @events.emit "user-leave", asst - -# Publish lobby contents for a particular batch, as well as users -# TODO can we simplify this by publishing users with turkserver.state = "lobby", -# if we use batch IDs in a smart way? -Meteor.publish "lobby", (batchId) -> - return [] unless batchId? - sub = this - - handle = LobbyStatus.find({batchId}).observeChanges - added: (id, fields) -> - sub.added("ts.lobby", id, fields) - sub.added("users", id, Meteor.users.findOne(id, fields: username: 1)) - changed: (id, fields) -> - sub.changed("ts.lobby", id, fields) - removed: (id) -> - sub.removed("ts.lobby", id) - sub.removed("users", id) - - sub.ready() - sub.onStop -> handle.stop() - -# Publish lobby config information for active batches with lobby and grouping -# TODO publish this based on the batch of the active user -Meteor.publish null, -> - sub = this - subHandle = Batches.find({ - active: true - lobby: true - grouping: "groupSize" + } + } + ); + + return Meteor.defer(() => this.events.emit("user-join", asst)); + } + + getAssignments(selector) { + selector = _.extend(selector || {}, + {batchId: this.batchId}); + return Array.from(LobbyStatus.find(selector).fetch()).map((record) => TurkServer.Assignment.getAssignment(record.asstId)); + } + + // TODO move status updates into specific assigners + toggleStatus(userId) { + const existing = LobbyStatus.findOne(userId); + if (!existing) { throw new Meteor.Error(403, ErrMsg.userNotInLobbyErr); } + const newStatus = !existing.status; + LobbyStatus.update(userId, + {$set: { status: newStatus }}); + + const asst = TurkServer.Assignment.getCurrentUserAssignment(userId); + return Meteor.defer(() => this.events.emit("user-status", asst, newStatus)); + } + + // Takes a group of users from the lobby without triggering the user-leave event. + pluckUsers(userIds) { + return LobbyStatus.remove({_id : {$in: userIds} }); + } + + removeAssignment(asst) { + // TODO check for batchId here + if (LobbyStatus.remove(asst.userId) > 0) { + return Meteor.defer(() => this.events.emit("user-leave", asst)); + } + } +}; + +// Publish lobby contents for a particular batch, as well as users +// TODO can we simplify this by publishing users with turkserver.state = "lobby", +// if we use batch IDs in a smart way? +Meteor.publish("lobby", function(batchId) { + if (batchId == null) { return []; } + const sub = this; + + const handle = LobbyStatus.find({batchId}).observeChanges({ + added(id, fields) { + sub.added("ts.lobby", id, fields); + return sub.added("users", id, Meteor.users.findOne(id, {fields: {username: 1}})); + }, + changed(id, fields) { + return sub.changed("ts.lobby", id, fields); + }, + removed(id) { + sub.removed("ts.lobby", id); + return sub.removed("users", id); + } + }); + + sub.ready(); + return sub.onStop(() => handle.stop()); +}); + +// Publish lobby config information for active batches with lobby and grouping +// TODO publish this based on the batch of the active user +Meteor.publish(null, function() { + const sub = this; + const subHandle = Batches.find({ + active: true, + lobby: true, + grouping: "groupSize", groupVal: { $exists: true } }, - fields: { groupVal: 1 } - ).observeChanges - added: (id, fields) -> - sub.added "ts.config", "lobbyThreshold", { value: fields.groupVal } - changed: (id, fields) -> - sub.changed "ts.config", "lobbyThreshold", { value: fields.groupVal } - removed: (id) -> - sub.removed "ts.config", "lobbyThreshold" - - sub.ready() - sub.onStop -> subHandle.stop() - -# Check for lobby state -Meteor.methods - "toggleStatus" : -> - userId = Meteor.userId() - throw new Meteor.Error(403, ErrMsg.userIdErr) unless userId - - TurkServer.Batch.currentBatch().lobby.toggleStatus(userId) - @unblock() - - -# Clear lobby status on startup -# Just clear lobby users for assignment, but not lobby state -Meteor.startup -> - LobbyStatus.remove {} - -# Meteor.users.update { "turkserver.state": "lobby" }, -# $unset: {"turkserver.state": null} -# , {multi: true} + {fields: { groupVal: 1 }} + ).observeChanges({ + added(id, fields) { + return sub.added("ts.config", "lobbyThreshold", { value: fields.groupVal }); + }, + changed(id, fields) { + return sub.changed("ts.config", "lobbyThreshold", { value: fields.groupVal }); + }, + removed(id) { + return sub.removed("ts.config", "lobbyThreshold"); + } + }); + + sub.ready(); + return sub.onStop(() => subHandle.stop()); +}); + +// Check for lobby state +Meteor.methods({ + "toggleStatus"() { + const userId = Meteor.userId(); + if (!userId) { throw new Meteor.Error(403, ErrMsg.userIdErr); } + + TurkServer.Batch.currentBatch().lobby.toggleStatus(userId); + return this.unblock(); + } +}); + + +// Clear lobby status on startup +// Just clear lobby users for assignment, but not lobby state +Meteor.startup(() => LobbyStatus.remove({})); + +// Meteor.users.update { "turkserver.state": "lobby" }, +// $unset: {"turkserver.state": null} +// , {multi: true} diff --git a/server/logging.js b/server/logging.js index 6f63513..0d1b4cc 100644 --- a/server/logging.js +++ b/server/logging.js @@ -1,30 +1,44 @@ -Logs._ensureIndex - _groupId: 1 +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +Logs._ensureIndex({ + _groupId: 1, _timestamp: 1 +}); -# Save group and timestamp for each log request -Logs.before.insert (userId, doc) -> - # Never log admin actions - # TODO this means admin-initiated teardown events aren't recorded - return false if Meteor.users.findOne(userId)?.admin - groupId = Partitioner._currentGroup.get() +// Save group and timestamp for each log request +Logs.before.insert(function(userId, doc) { + // Never log admin actions + // TODO this means admin-initiated teardown events aren't recorded + if (__guard__(Meteor.users.findOne(userId), x => x.admin)) { return false; } + let groupId = Partitioner._currentGroup.get(); - unless groupId - throw new Meteor.Error(403, ErrMsg.userIdErr) unless userId - groupId = Partitioner.getUserGroup(userId) - throw new Meteor.Error(403, ErrMsg.groupErr) unless groupId + if (!groupId) { + if (!userId) { throw new Meteor.Error(403, ErrMsg.userIdErr); } + groupId = Partitioner.getUserGroup(userId); + if (!groupId) { throw new Meteor.Error(403, ErrMsg.groupErr); } + } - doc._userId = userId if userId - doc._groupId = groupId - doc._timestamp ?= new Date() # Allow specification of custom timestamps - return true + if (userId) { doc._userId = userId; } + doc._groupId = groupId; + if (doc._timestamp == null) { doc._timestamp = new Date(); } // Allow specification of custom timestamps + return true; +}); -TurkServer.log = (doc, callback) -> - Logs.insert(doc, callback) +TurkServer.log = (doc, callback) => Logs.insert(doc, callback); -Meteor.methods - "ts-log": (doc) -> - Meteor._debug("Warning; received log request for anonymous user: ", doc) unless Meteor.userId() - Logs.insert(doc) - return +Meteor.methods({ + "ts-log"(doc) { + if (!Meteor.userId()) { Meteor._debug("Warning; received log request for anonymous user: ", doc); } + Logs.insert(doc); + } +}); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/server/turkserver.js b/server/turkserver.js index 26a967b..3bb0fc2 100644 --- a/server/turkserver.js +++ b/server/turkserver.js @@ -1,43 +1,52 @@ -# Collection modifiers, in case running on insecure - -TurkServer.isAdminRule = (userId) -> Meteor.users.findOne(userId).admin is true - -adminOnly = - insert: TurkServer.isAdminRule - update: TurkServer.isAdminRule +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +// Collection modifiers, in case running on insecure + +TurkServer.isAdminRule = userId => Meteor.users.findOne(userId).admin === true; + +const adminOnly = { + insert: TurkServer.isAdminRule, + update: TurkServer.isAdminRule, remove: TurkServer.isAdminRule +}; -always = - insert: -> true - update: -> true - remove: -> true +const always = { + insert() { return true; }, + update() { return true; }, + remove() { return true; } +}; -### +/* Batches Treatments Experiments -### +*/ -Batches.allow(adminOnly) -Treatments.allow(adminOnly) +Batches.allow(adminOnly); +Treatments.allow(adminOnly); -Treatments._ensureIndex {name: 1}, {unique: 1} +Treatments._ensureIndex({name: 1}, {unique: 1}); -# Allow admin to make emergency adjustments to the lobby collection just in case -LobbyStatus.allow(adminOnly) +// Allow admin to make emergency adjustments to the lobby collection just in case +LobbyStatus.allow(adminOnly); -# Only server should update these -Experiments.deny(always) -Logs.deny(always) -RoundTimers.deny(always) +// Only server should update these +Experiments.deny(always); +Logs.deny(always); +RoundTimers.deny(always); -# Create an index on experiments +// Create an index on experiments Experiments._ensureIndex({ batchId: 1, - endTime: 1 # non-sparse ensures that running experiments are indexed -}) + endTime: 1 // non-sparse ensures that running experiments are indexed +}); -### +/* Workers Assignments @@ -45,73 +54,80 @@ Experiments._ensureIndex({ Qualifications HITTypes HITs -### +*/ -Workers.allow(adminOnly) -Assignments.deny(always) +Workers.allow(adminOnly); +Assignments.deny(always); -WorkerEmails.allow(adminOnly) -Qualifications.allow(adminOnly) -HITTypes.allow(adminOnly) -HITs.allow(adminOnly) +WorkerEmails.allow(adminOnly); +Qualifications.allow(adminOnly); +HITTypes.allow(adminOnly); +HITs.allow(adminOnly); -# XXX remove this check for release -try - HITTypes._dropIndex("HITTypeId_1") - console.log "Dropped old index on HITTypeId" +// XXX remove this check for release +try { + HITTypes._dropIndex("HITTypeId_1"); + console.log("Dropped old index on HITTypeId"); +} catch (error) {} -# Index HITTypes, but only for those that exist +// Index HITTypes, but only for those that exist HITTypes._ensureIndex({HITTypeId: 1}, { - name: "HITTypeId_1_sparse" + name: "HITTypeId_1_sparse", unique: 1, sparse: 1 -}) +}); -HITs._ensureIndex {HITId: 1}, {unique: 1} +HITs._ensureIndex({HITId: 1}, {unique: 1}); -# TODO more careful indices on these collections +// TODO more careful indices on these collections -# Index on unique assignment-worker pairs -Assignments._ensureIndex - hitId: 1 - assignmentId: 1 +// Index on unique assignment-worker pairs +Assignments._ensureIndex({ + hitId: 1, + assignmentId: 1, workerId: 1 -, { unique: 1 } +} +, { unique: 1 }); -# Allow fast lookup of a worker's HIT assignments by status -Assignments._ensureIndex - workerId: 1 +// Allow fast lookup of a worker's HIT assignments by status +Assignments._ensureIndex({ + workerId: 1, status: 1 +}); -# Allow lookup of assignments by batch and submitTime (completed vs incomplete) -Assignments._ensureIndex - batchId: 1 +// Allow lookup of assignments by batch and submitTime (completed vs incomplete) +Assignments._ensureIndex({ + batchId: 1, submitTime: 1 +}); -# TODO deprecated index -try - Assignments._dropIndex - batchId: 1 +// TODO deprecated index +try { + Assignments._dropIndex({ + batchId: 1, acceptTime: 1 + }); +} catch (error1) {} -### +/* Data publications -### - -# Publish turkserver user fields to a user -Meteor.publish null, -> - return null unless @userId - - cursors = [ - Meteor.users.find(@userId, - fields: { turkserver: 1 }) - ] - - # Current user assignment data, including idle and disconnection time - # This won't be sent for the admin user - if (workerId = Meteor.users.findOne(@userId)?.workerId)? - cursors.push Assignments.find({ - workerId: workerId +*/ + +// Publish turkserver user fields to a user +Meteor.publish(null, function() { + let workerId; + if (!this.userId) { return null; } + + const cursors = [ + Meteor.users.find(this.userId, + {fields: { turkserver: 1 }}) + ]; + + // Current user assignment data, including idle and disconnection time + // This won't be sent for the admin user + if ((workerId = __guard__(Meteor.users.findOne(this.userId), x => x.workerId)) != null) { + cursors.push(Assignments.find({ + workerId, status: "assigned" }, { fields: { @@ -120,53 +136,71 @@ Meteor.publish null, -> bonusPayment: 1 } }) + ); + } - return cursors + return cursors; +}); -Meteor.publish "tsTreatments", (names) -> - return [] unless names? and names[0]? +Meteor.publish("tsTreatments", function(names) { + if ((names == null) || (names[0] == null)) { return []; } check(names, [String]); return Treatments.find({name: { $in: names }}); +}); -# Publish current experiment for a user, if it exists -# This includes the data sent to the admin user -Meteor.publish "tsCurrentExperiment", (group) -> - return unless @userId +// Publish current experiment for a user, if it exists +// This includes the data sent to the admin user +Meteor.publish("tsCurrentExperiment", function(group) { + if (!this.userId) { return; } return [ Experiments.find(group), - RoundTimers.find() # Partitioned by group - ] - -# For the preview page and test logins, need to publish the list of batches. -# TODO make this a bit more secure -Meteor.publish "tsLoginBatches", (batchId) -> - # Never send the batch list to logged-in users. - return [] if @userId? - - # If an erroneous batchId was sent, don't just send the whole list. - if arguments.length > 0 and batchId? - return Batches.find(batchId) - else - return Batches.find() - -Meteor.publish null, -> - return [] unless @userId? - # Publish specific batch if logged in - # This should work for now because an assignment is made upon login - return [] unless (workerId = Meteor.users.findOne(@userId)?.workerId)? - - sub = this - handle = Assignments.find({workerId, status: "assigned"}).observeChanges - added: (id, fields) -> - batchId = Assignments.findOne(id).batchId - sub.added "ts.batches", batchId, Batches.findOne(batchId) - removed: (id) -> - batchId = Assignments.findOne(id).batchId - sub.removed "ts.batches", batchId - - sub.ready() - sub.onStop -> handle.stop() - -TurkServer.startup = (func) -> - Meteor.startup -> - Partitioner.directOperation(func) + RoundTimers.find() // Partitioned by group + ]; +}); + +// For the preview page and test logins, need to publish the list of batches. +// TODO make this a bit more secure +Meteor.publish("tsLoginBatches", function(batchId) { + // Never send the batch list to logged-in users. + if (this.userId != null) { return []; } + + // If an erroneous batchId was sent, don't just send the whole list. + if ((arguments.length > 0) && (batchId != null)) { + return Batches.find(batchId); + } else { + return Batches.find(); + } +}); + +Meteor.publish(null, function() { + let workerId; + if (this.userId == null) { return []; } + // Publish specific batch if logged in + // This should work for now because an assignment is made upon login + if ((workerId = __guard__(Meteor.users.findOne(this.userId), x => x.workerId)) == null) { return []; } + + const sub = this; + const handle = Assignments.find({workerId, status: "assigned"}).observeChanges({ + added(id, fields) { + const { + batchId + } = Assignments.findOne(id); + return sub.added("ts.batches", batchId, Batches.findOne(batchId)); + }, + removed(id) { + const { + batchId + } = Assignments.findOne(id); + return sub.removed("ts.batches", batchId); + } + }); + + sub.ready(); + return sub.onStop(() => handle.stop()); +}); + +TurkServer.startup = func => Meteor.startup(() => Partitioner.directOperation(func)); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/tests/admin_tests.js b/tests/admin_tests.js index 67ebb68..0ef8045 100644 --- a/tests/admin_tests.js +++ b/tests/admin_tests.js @@ -1,180 +1,218 @@ -batchId = "mturkBatch" -hitTypeId = "mturkHITType" - -# Create dummy batch and HIT Type -Batches.upsert { _id: batchId }, { _id: batchId } - -HITTypes.upsert {HITTypeId: hitTypeId}, - $set: { batchId } - -# Temporarily disable the admin check during these tests -_checkAdmin = TurkServer.checkAdmin - -withCleanup = TestUtils.getCleanupWrapper - before: -> - Batches.upsert batchId, $set: +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const batchId = "mturkBatch"; +const hitTypeId = "mturkHITType"; + +// Create dummy batch and HIT Type +Batches.upsert({ _id: batchId }, { _id: batchId }); + +HITTypes.upsert({HITTypeId: hitTypeId}, + {$set: { batchId }}); + +// Temporarily disable the admin check during these tests +const _checkAdmin = TurkServer.checkAdmin; + +const withCleanup = TestUtils.getCleanupWrapper({ + before() { + Batches.upsert(batchId, { $set: { active: false } - TurkServer.checkAdmin = -> - - after: -> - HITs.remove { HITTypeId: hitTypeId } + }); + return TurkServer.checkAdmin = function() {}; + }, - # Clean up emails and workers created for testing e-mails - WorkerEmails.remove({}) - Workers.remove({test: "admin"}) + after() { + HITs.remove({ HITTypeId: hitTypeId }); - TestUtils.mturkAPI.handler = null - TurkServer.checkAdmin = _checkAdmin + // Clean up emails and workers created for testing e-mails + WorkerEmails.remove({}); + Workers.remove({test: "admin"}); -Tinytest.add "admin - create HIT for active batch", withCleanup (test) -> + TestUtils.mturkAPI.handler = null; + return TurkServer.checkAdmin = _checkAdmin; + } +}); - newHitId = Random.id() - TestUtils.mturkAPI.handler = (op, params) -> - switch op - when "CreateHIT" then newHitId - when "GetHIT" then { CreationTime: new Date } # Stub out the GetHIT call with some arbitrary data +Tinytest.add("admin - create HIT for active batch", withCleanup(function(test) { - Batches.upsert batchId, $set: { active: true } + const newHitId = Random.id(); + TestUtils.mturkAPI.handler = function(op, params) { + switch (op) { + case "CreateHIT": return newHitId; + case "GetHIT": return { CreationTime: new Date }; // Stub out the GetHIT call with some arbitrary data + } + }; - # test - Meteor.call "ts-admin-create-hit", hitTypeId, {} + Batches.upsert(batchId, {$set: { active: true }}); - hit = HITs.findOne(HITId: newHitId) + // test + Meteor.call("ts-admin-create-hit", hitTypeId, {}); - test.isTrue(hit) - test.equal hit.HITId, newHitId - test.equal hit.HITTypeId, hitTypeId + const hit = HITs.findOne({HITId: newHitId}); -Tinytest.add "admin - create HIT for inactive batch", withCleanup (test) -> + test.isTrue(hit); + test.equal(hit.HITId, newHitId); + return test.equal(hit.HITTypeId, hitTypeId); +}) +); - test.throws -> - Meteor.call "ts-admin-create-hit", hitTypeId, {} - , (e) -> e.error is 403 +Tinytest.add("admin - create HIT for inactive batch", withCleanup(test => test.throws(() => Meteor.call("ts-admin-create-hit", hitTypeId, {}) +, e => e.error === 403)) +); -Tinytest.add "admin - extend HIT for active batch", withCleanup (test) -> +Tinytest.add("admin - extend HIT for active batch", withCleanup(function(test) { - HITId = Random.id() - HITs.insert { HITId, HITTypeId: hitTypeId } - Batches.upsert batchId, $set: { active: true } + const HITId = Random.id(); + HITs.insert({ HITId, HITTypeId: hitTypeId }); + Batches.upsert(batchId, {$set: { active: true }}); - # Need to return something for GetHIT else complaining from Mongo et al - TestUtils.mturkAPI.handler = (op, params) -> - switch op - when "GetHIT" then { HITId } + // Need to return something for GetHIT else complaining from Mongo et al + TestUtils.mturkAPI.handler = function(op, params) { + switch (op) { + case "GetHIT": return { HITId }; + } + }; - Meteor.call "ts-admin-extend-hit", { HITId } + return Meteor.call("ts-admin-extend-hit", { HITId });})); -Tinytest.add "admin - extend HIT for inactive batch", withCleanup (test) -> +Tinytest.add("admin - extend HIT for inactive batch", withCleanup(function(test) { - HITId = Random.id() - HITs.insert { HITId, HITTypeId: hitTypeId } + const HITId = Random.id(); + HITs.insert({ HITId, HITTypeId: hitTypeId }); - test.throws -> - Meteor.call "ts-admin-extend-hit", { HITId } - (e) -> e.error is 403 + test.throws(() => Meteor.call("ts-admin-extend-hit", { HITId })); + return e => e.error === 403; +}) +); -Tinytest.add "admin - email - create message from existing", withCleanup (test) -> - workers = (Random.id() for i in [1..100]) +Tinytest.add("admin - email - create message from existing", withCleanup(function(test) { + const workers = (__range__(1, 100, true).map((i) => Random.id())); - existingId = WorkerEmails.insert - subject: "test" - message: "test message" + const existingId = WorkerEmails.insert({ + subject: "test", + message: "test message", recipients: workers + }); - subject = "test2" - message = "another test message" + const subject = "test2"; + const message = "another test message"; - newId = Meteor.call "ts-admin-create-message", subject, message, existingId + const newId = Meteor.call("ts-admin-create-message", subject, message, existingId); - newEmail = WorkerEmails.findOne(newId) + const newEmail = WorkerEmails.findOne(newId); - test.equal newEmail.subject, subject - test.equal newEmail.message, message - test.length newEmail.recipients, workers.length - test.isTrue _.isEqual(newEmail.recipients, workers) - test.isFalse newEmail.sentTime + test.equal(newEmail.subject, subject); + test.equal(newEmail.message, message); + test.length(newEmail.recipients, workers.length); + test.isTrue(_.isEqual(newEmail.recipients, workers)); + return test.isFalse(newEmail.sentTime); +}) +); -Tinytest.add "admin - email - send and record message", withCleanup (test) -> - # Create fake workers - workerIds = ( Workers.insert({test: "admin"}) for x in [1..100] ) - test.equal workerIds.length, 100 +Tinytest.add("admin - email - send and record message", withCleanup(function(test) { + // Create fake workers + const workerIds = ( __range__(1, 100, true).map((x) => Workers.insert({test: "admin"})) ); + test.equal(workerIds.length, 100); - subject = "test sending" - message = "test sending message" + const subject = "test sending"; + let message = "test sending message"; - emailId = WorkerEmails.insert - subject: subject - message: message + const emailId = WorkerEmails.insert({ + subject, + message, recipients: workerIds - - # Record all the API calls that were made - apiWorkers = [] - TestUtils.mturkAPI.handler = (op, params) -> - test.equal params.Subject, subject - test.equal params.MessageText, message - apiWorkers = apiWorkers.concat(params.WorkerId) - - message = Meteor.call "ts-admin-send-message", emailId - # First word is the number of messages sent - # XXX this test may be a little janky - count = parseInt(message.split(" ")[0]) - - test.equal count, workerIds.length - test.length apiWorkers, workerIds.length - test.isTrue _.isEqual(apiWorkers, workerIds) - - # Test that email sending got saved to workers - checkedWorkers = 0 - Workers.find({_id: $in: workerIds}).forEach (worker) -> - test.equal worker.emailsReceived[0], emailId - checkedWorkers++ - - test.equal checkedWorkers, workerIds.length - - # Test that sent date was recorded - test.instanceOf WorkerEmails.findOne(emailId).sentTime, Date - -Tinytest.add "admin - assign worker qualification", withCleanup (test) -> - qual = "blahblah" - value = 2 - workerId = Workers.insert({}) - - TestUtils.mturkAPI.handler = (op, params) -> - test.equal op, "AssignQualification" - test.equal params.QualificationTypeId, qual - test.equal params.WorkerId, workerId - test.equal params.IntegerValue, value - test.equal params.SendNotification, false - - TurkServer.Util.assignQualification(workerId, qual, value, false) - - # Check that worker has been updated - worker = Workers.findOne(workerId) - test.equal worker.quals[0].id, qual - test.equal worker.quals[0].value, 2 - -Tinytest.add "admin - update worker qualification", withCleanup (test) -> - qual = "blahblah" - value = 10 - - workerId = Workers.insert({ + }); + + // Record all the API calls that were made + let apiWorkers = []; + TestUtils.mturkAPI.handler = function(op, params) { + test.equal(params.Subject, subject); + test.equal(params.MessageText, message); + return apiWorkers = apiWorkers.concat(params.WorkerId); + }; + + message = Meteor.call("ts-admin-send-message", emailId); + // First word is the number of messages sent + // XXX this test may be a little janky + const count = parseInt(message.split(" ")[0]); + + test.equal(count, workerIds.length); + test.length(apiWorkers, workerIds.length); + test.isTrue(_.isEqual(apiWorkers, workerIds)); + + // Test that email sending got saved to workers + let checkedWorkers = 0; + Workers.find({_id: {$in: workerIds}}).forEach(function(worker) { + test.equal(worker.emailsReceived[0], emailId); + return checkedWorkers++; + }); + + test.equal(checkedWorkers, workerIds.length); + + // Test that sent date was recorded + return test.instanceOf(WorkerEmails.findOne(emailId).sentTime, Date); +}) +); + +Tinytest.add("admin - assign worker qualification", withCleanup(function(test) { + const qual = "blahblah"; + const value = 2; + const workerId = Workers.insert({}); + + TestUtils.mturkAPI.handler = function(op, params) { + test.equal(op, "AssignQualification"); + test.equal(params.QualificationTypeId, qual); + test.equal(params.WorkerId, workerId); + test.equal(params.IntegerValue, value); + return test.equal(params.SendNotification, false); + }; + + TurkServer.Util.assignQualification(workerId, qual, value, false); + + // Check that worker has been updated + const worker = Workers.findOne(workerId); + test.equal(worker.quals[0].id, qual); + return test.equal(worker.quals[0].value, 2); +}) +); + +Tinytest.add("admin - update worker qualification", withCleanup(function(test) { + const qual = "blahblah"; + const value = 10; + + const workerId = Workers.insert({ quals: [ { - id: qual + id: qual, value: 2 } ] - }) - - TestUtils.mturkAPI.handler = (op, params) -> - test.equal op, "UpdateQualificationScore" - test.equal params.QualificationTypeId, qual - test.equal params.SubjectId, workerId - test.equal params.IntegerValue, value - - TurkServer.Util.assignQualification(workerId, qual, value, false) - - # Check that worker has been updated - worker = Workers.findOne(workerId) - - test.length worker.quals, 1 - test.equal worker.quals[0].id, qual - test.equal worker.quals[0].value, value + }); + + TestUtils.mturkAPI.handler = function(op, params) { + test.equal(op, "UpdateQualificationScore"); + test.equal(params.QualificationTypeId, qual); + test.equal(params.SubjectId, workerId); + return test.equal(params.IntegerValue, value); + }; + + TurkServer.Util.assignQualification(workerId, qual, value, false); + + // Check that worker has been updated + const worker = Workers.findOne(workerId); + + test.length(worker.quals, 1); + test.equal(worker.quals[0].id, qual); + return test.equal(worker.quals[0].value, value); +}) +); + +function __range__(left, right, inclusive) { + let range = []; + let ascending = left < right; + let end = !inclusive ? right : ascending ? right + 1 : right - 1; + for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) { + range.push(i); + } + return range; +} \ No newline at end of file diff --git a/tests/assigner_tests.js b/tests/assigner_tests.js index e61d465..eb40100 100644 --- a/tests/assigner_tests.js +++ b/tests/assigner_tests.js @@ -1,171 +1,194 @@ -batch = null - -withCleanup = TestUtils.getCleanupWrapper - before: -> - # Create a random batch and corresponding lobby for assigner tests - batchId = Batches.insert({}) - batch = TurkServer.Batch.getBatch(batchId) - after: -> - Experiments.remove { batchId: batch.batchId } - Assignments.remove { batchId: batch.batchId } - -tutorialTreatments = [ "tutorial" ] -groupTreatments = [ "group" ] - -TurkServer.ensureTreatmentExists - name: "tutorial" -TurkServer.ensureTreatmentExists - name: "group" - -createAssignment = -> - workerId = Random.id() - userId = Accounts.insertUserDoc {}, { workerId } - return TurkServer.Assignment.createAssignment - batchId: batch.batchId - hitId: Random.id() - assignmentId: Random.id() - workerId: workerId - acceptTime: new Date() +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS202: Simplify dynamic range loops + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let batch = null; + +const withCleanup = TestUtils.getCleanupWrapper({ + before() { + // Create a random batch and corresponding lobby for assigner tests + const batchId = Batches.insert({}); + return batch = TurkServer.Batch.getBatch(batchId); + }, + after() { + Experiments.remove({ batchId: batch.batchId }); + return Assignments.remove({ batchId: batch.batchId }); + }}); + +const tutorialTreatments = [ "tutorial" ]; +const groupTreatments = [ "group" ]; + +TurkServer.ensureTreatmentExists({ + name: "tutorial"}); +TurkServer.ensureTreatmentExists({ + name: "group"}); + +const createAssignment = function() { + const workerId = Random.id(); + const userId = Accounts.insertUserDoc({}, { workerId }); + return TurkServer.Assignment.createAssignment({ + batchId: batch.batchId, + hitId: Random.id(), + assignmentId: Random.id(), + workerId, + acceptTime: new Date(), status: "assigned" + }); +}; -Tinytest.add "assigners - tutorialGroup - assigner picks up existing instance", withCleanup (test) -> - assigner = new TurkServer.Assigners.TutorialGroupAssigner(tutorialTreatments, groupTreatments) +Tinytest.add("assigners - tutorialGroup - assigner picks up existing instance", withCleanup(function(test) { + const assigner = new TurkServer.Assigners.TutorialGroupAssigner(tutorialTreatments, groupTreatments); - instance = batch.createInstance(groupTreatments) - instance.setup() + const instance = batch.createInstance(groupTreatments); + instance.setup(); - batch.setAssigner(assigner) + batch.setAssigner(assigner); - test.equal assigner.instance, instance - test.equal assigner.autoAssign, true + test.equal(assigner.instance, instance); + return test.equal(assigner.autoAssign, true); +}) +); -Tinytest.add "assigners - tutorialGroup - initial lobby gets tutorial", withCleanup (test) -> - assigner = new TurkServer.Assigners.TutorialGroupAssigner(tutorialTreatments, groupTreatments) - batch.setAssigner(assigner) +Tinytest.add("assigners - tutorialGroup - initial lobby gets tutorial", withCleanup(function(test) { + const assigner = new TurkServer.Assigners.TutorialGroupAssigner(tutorialTreatments, groupTreatments); + batch.setAssigner(assigner); - test.equal assigner.autoAssign, false + test.equal(assigner.autoAssign, false); - asst = createAssignment() - TestUtils.connCallbacks.sessionReconnect {userId: asst.userId} + const asst = createAssignment(); + TestUtils.connCallbacks.sessionReconnect({userId: asst.userId}); - TestUtils.sleep(150) # YES!! + TestUtils.sleep(150); // YES!! - user = Meteor.users.findOne(asst.userId) - instances = asst.getInstances() + const user = Meteor.users.findOne(asst.userId); + const instances = asst.getInstances(); - test.equal user.turkserver.state, "experiment" - test.length instances, 1 + test.equal(user.turkserver.state, "experiment"); + test.length(instances, 1); - test.equal LobbyStatus.find(batchId: batch.batchId).count(), 0 - exp = Experiments.findOne(instances[0].id) - test.equal exp.treatments, tutorialTreatments + test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 0); + const exp = Experiments.findOne(instances[0].id); + return test.equal(exp.treatments, tutorialTreatments); +}) +); -Tinytest.add "assigners - tutorialGroup - autoAssign event triggers properly", withCleanup (test) -> +Tinytest.add("assigners - tutorialGroup - autoAssign event triggers properly", withCleanup(function(test) { - assigner = new TurkServer.Assigners.TutorialGroupAssigner(tutorialTreatments, groupTreatments) - batch.setAssigner(assigner) + const assigner = new TurkServer.Assigners.TutorialGroupAssigner(tutorialTreatments, groupTreatments); + batch.setAssigner(assigner); - asst = createAssignment() - # Pretend we already have a tutorial done - tutorialInstance = batch.createInstance(tutorialTreatments) - tutorialInstance.setup() - tutorialInstance.addAssignment(asst) - tutorialInstance.teardown() + const asst = createAssignment(); + // Pretend we already have a tutorial done + const tutorialInstance = batch.createInstance(tutorialTreatments); + tutorialInstance.setup(); + tutorialInstance.addAssignment(asst); + tutorialInstance.teardown(); - TestUtils.sleep(100) # So the user joins the lobby properly + TestUtils.sleep(100); // So the user joins the lobby properly - user = Meteor.users.findOne(asst.userId) - instances = asst.getInstances() + let user = Meteor.users.findOne(asst.userId); + let instances = asst.getInstances(); - test.equal user.turkserver.state, "lobby" - test.length instances, 1 + test.equal(user.turkserver.state, "lobby"); + test.length(instances, 1); - batch.lobby.events.emit("auto-assign") + batch.lobby.events.emit("auto-assign"); - TestUtils.sleep(100) + TestUtils.sleep(100); - user = Meteor.users.findOne(asst.userId) - instances = asst.getInstances() + user = Meteor.users.findOne(asst.userId); + instances = asst.getInstances(); - test.equal user.turkserver.state, "experiment" - test.length instances, 2 + test.equal(user.turkserver.state, "experiment"); + test.length(instances, 2); - test.equal LobbyStatus.find(batchId: batch.batchId).count(), 0 - exp = Experiments.findOne(instances[1].id) - test.equal exp.treatments, groupTreatments + test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 0); + const exp = Experiments.findOne(instances[1].id); + return test.equal(exp.treatments, groupTreatments); +}) +); -Tinytest.add "assigners - tutorialGroup - final send to exit survey", withCleanup (test) -> +Tinytest.add("assigners - tutorialGroup - final send to exit survey", withCleanup(function(test) { - assigner = new TurkServer.Assigners.TutorialGroupAssigner(tutorialTreatments, groupTreatments) - batch.setAssigner(assigner) + const assigner = new TurkServer.Assigners.TutorialGroupAssigner(tutorialTreatments, groupTreatments); + batch.setAssigner(assigner); - asst = createAssignment() - # Pretend we already have a tutorial done - tutorialInstance = batch.createInstance(tutorialTreatments) - tutorialInstance.setup() - tutorialInstance.addAssignment(asst) - tutorialInstance.teardown() + const asst = createAssignment(); + // Pretend we already have a tutorial done + const tutorialInstance = batch.createInstance(tutorialTreatments); + tutorialInstance.setup(); + tutorialInstance.addAssignment(asst); + tutorialInstance.teardown(); - TestUtils.sleep(100) # So the user joins the lobby properly + TestUtils.sleep(100); // So the user joins the lobby properly - groupInstance = batch.createInstance(groupTreatments) - groupInstance.setup() - groupInstance.addAssignment(asst) - groupInstance.teardown() + const groupInstance = batch.createInstance(groupTreatments); + groupInstance.setup(); + groupInstance.addAssignment(asst); + groupInstance.teardown(); - TestUtils.sleep(100) + TestUtils.sleep(100); - user = Meteor.users.findOne(asst.userId) - instances = asst.getInstances() + const user = Meteor.users.findOne(asst.userId); + const instances = asst.getInstances(); - test.equal user.turkserver.state, "exitsurvey" - test.length instances, 2 + test.equal(user.turkserver.state, "exitsurvey"); + return test.length(instances, 2); +}) +); -# Setup for multi tests below -TurkServer.ensureTreatmentExists - name: "tutorial" +// Setup for multi tests below +TurkServer.ensureTreatmentExists({ + name: "tutorial"}); -TurkServer.ensureTreatmentExists - name: "parallel_worlds" +TurkServer.ensureTreatmentExists({ + name: "parallel_worlds"}); -multiGroupTreatments = [ "parallel_worlds" ] +const multiGroupTreatments = [ "parallel_worlds" ]; -### +/* Randomized multi-group assigner -### +*/ -Tinytest.add "assigners - tutorialRandomizedGroup - initial lobby gets tutorial", withCleanup (test) -> - assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( - tutorialTreatments, multiGroupTreatments, [8, 16, 32]) +Tinytest.add("assigners - tutorialRandomizedGroup - initial lobby gets tutorial", withCleanup(function(test) { + const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( + tutorialTreatments, multiGroupTreatments, [8, 16, 32]); - batch.setAssigner(assigner) + batch.setAssigner(assigner); - asst = createAssignment() - TestUtils.connCallbacks.sessionReconnect {userId: asst.userId} + const asst = createAssignment(); + TestUtils.connCallbacks.sessionReconnect({userId: asst.userId}); - TestUtils.sleep(150) + TestUtils.sleep(150); - user = Meteor.users.findOne(asst.userId) - instances = asst.getInstances() + const user = Meteor.users.findOne(asst.userId); + const instances = asst.getInstances(); - # should be in experiment - test.equal user.turkserver.state, "experiment" - test.length instances, 1 - # should not be in lobby - test.equal LobbyStatus.find(batchId: batch.batchId).count(), 0 - # should be in a tutorial - exp = Experiments.findOne(instances[0].id) - test.equal exp.treatments, tutorialTreatments + // should be in experiment + test.equal(user.turkserver.state, "experiment"); + test.length(instances, 1); + // should not be in lobby + test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 0); + // should be in a tutorial + const exp = Experiments.findOne(instances[0].id); + return test.equal(exp.treatments, tutorialTreatments); +}) +); -Tinytest.add "assigners - tutorialRandomizedGroup - send to exit survey", withCleanup (test) -> - assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( - tutorialTreatments, multiGroupTreatments, [8, 16, 32]) +Tinytest.add("assigners - tutorialRandomizedGroup - send to exit survey", withCleanup(function(test) { + const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( + tutorialTreatments, multiGroupTreatments, [8, 16, 32]); - batch.setAssigner(assigner) + batch.setAssigner(assigner); - asst = createAssignment() - # Pretend we already have two instances done - Assignments.update asst.asstId, + const asst = createAssignment(); + // Pretend we already have two instances done + Assignments.update(asst.asstId, { $push: { instances: { $each: [ @@ -174,441 +197,500 @@ Tinytest.add "assigners - tutorialRandomizedGroup - send to exit survey", withCl ] } } + }); - TestUtils.connCallbacks.sessionReconnect({userId: asst.userId}) + TestUtils.connCallbacks.sessionReconnect({userId: asst.userId}); - TestUtils.sleep(100) + TestUtils.sleep(100); - user = Meteor.users.findOne(asst.userId) - instances = asst.getInstances() + const user = Meteor.users.findOne(asst.userId); + const instances = asst.getInstances(); - test.equal user.turkserver.state, "exitsurvey" - test.length instances, 2 + test.equal(user.turkserver.state, "exitsurvey"); + return test.length(instances, 2); +}) +); -Tinytest.add "assigners - tutorialRandomizedGroup - set up instances", withCleanup (test) -> - assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( - tutorialTreatments, multiGroupTreatments, [8, 16, 32]) +Tinytest.add("assigners - tutorialRandomizedGroup - set up instances", withCleanup(function(test) { + const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( + tutorialTreatments, multiGroupTreatments, [8, 16, 32]); - batch.setAssigner(assigner) + batch.setAssigner(assigner); - assigner.setup() + assigner.setup(); - # Verify that four instances were created with the right treatments - created = Experiments.find({ batchId: batch.batchId }).fetch() + // Verify that four instances were created with the right treatments + const created = Experiments.find({ batchId: batch.batchId }).fetch(); - test.length created, 4 + test.length(created, 4); - # Sort by group size and test - created.sort (a, b) -> - if a.treatments[0] is "parallel_worlds" then 1 - else if b.treatments[0] is "parallel_worlds" then -1 - # grab the part after "group_" - else parseInt(a.treatments[0].substring(6)) - parseInt(b.treatments[0].substring(6)) + // Sort by group size and test + created.sort(function(a, b) { + if (a.treatments[0] === "parallel_worlds") { return 1; + } else if (b.treatments[0] === "parallel_worlds") { return -1; + // grab the part after "group_" + } else { return parseInt(a.treatments[0].substring(6)) - parseInt(b.treatments[0].substring(6)); } + }); - test.equal created[0].treatments, [ "group_8", "parallel_worlds" ] - test.equal created[1].treatments, [ "group_16", "parallel_worlds" ] - test.equal created[2].treatments, [ "group_32", "parallel_worlds" ] - # Buffer group - test.equal created[3].treatments, [ "parallel_worlds" ] + test.equal(created[0].treatments, [ "group_8", "parallel_worlds" ]); + test.equal(created[1].treatments, [ "group_16", "parallel_worlds" ]); + test.equal(created[2].treatments, [ "group_32", "parallel_worlds" ]); + // Buffer group + test.equal(created[3].treatments, [ "parallel_worlds" ]); - # Test that there are 56 randomization slots now with the right allocation - test.isFalse assigner.autoAssign - test.isTrue assigner.bufferInstanceId + // Test that there are 56 randomization slots now with the right allocation + test.isFalse(assigner.autoAssign); + test.isTrue(assigner.bufferInstanceId); - test.length assigner.instanceSlots, 56 - test.equal assigner.instanceSlotIndex, 0 + test.length(assigner.instanceSlots, 56); + test.equal(assigner.instanceSlotIndex, 0); - allocation = _.countBy(assigner.instanceSlots, Object) - test.equal allocation[created[0]._id], 8 - test.equal allocation[created[1]._id], 16 - test.equal allocation[created[2]._id], 32 + const allocation = _.countBy(assigner.instanceSlots, Object); + test.equal(allocation[created[0]._id], 8); + test.equal(allocation[created[1]._id], 16); + test.equal(allocation[created[2]._id], 32); - # Calling setup again should not do anything - assigner.setup() + // Calling setup again should not do anything + assigner.setup(); - test.length Experiments.find({ batchId: batch.batchId }).fetch(), 4 + return test.length(Experiments.find({ batchId: batch.batchId }).fetch(), 4); +}) +); -Tinytest.add "assigners - tutorialRandomizedGroup - set up reusing existing instances", withCleanup (test) -> - groupArr = [ +Tinytest.add("assigners - tutorialRandomizedGroup - set up reusing existing instances", withCleanup(function(test) { + const groupArr = [ 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 8, 16, 32 - ] + ]; - assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( - tutorialTreatments, multiGroupTreatments, groupArr) + const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( + tutorialTreatments, multiGroupTreatments, groupArr); - # Create one existing treatment of group size 1 - instance = batch.createInstance( ["group_1"].concat(multiGroupTreatments) ) - instance.setup() + // Create one existing treatment of group size 1 + const instance = batch.createInstance( ["group_1"].concat(multiGroupTreatments) ); + instance.setup(); - batch.setAssigner(assigner) + batch.setAssigner(assigner); - assigner.setup() + assigner.setup(); - # Verify that 18 instances were created with the right treatments - created = Experiments.find({ batchId: batch.batchId }).fetch() + // Verify that 18 instances were created with the right treatments + const created = Experiments.find({ batchId: batch.batchId }).fetch(); - test.length created, 18 + test.length(created, 18); - # Test that there are 56 randomization slots now with the right allocation - test.isFalse assigner.autoAssign - test.isTrue assigner.bufferInstanceId + // Test that there are 56 randomization slots now with the right allocation + test.isFalse(assigner.autoAssign); + test.isTrue(assigner.bufferInstanceId); - test.length assigner.instanceSlots, 80 - test.equal assigner.instanceSlotIndex, 0 + test.length(assigner.instanceSlots, 80); + return test.equal(assigner.instanceSlotIndex, 0); +}) +); -Tinytest.add "assigners - tutorialRandomizedGroup - pick up existing instances", withCleanup (test) -> - groupArr = [8, 16, 32] - assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( - tutorialTreatments, multiGroupTreatments, groupArr) +Tinytest.add("assigners - tutorialRandomizedGroup - pick up existing instances", withCleanup(function(test) { + const groupArr = [8, 16, 32]; + const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( + tutorialTreatments, multiGroupTreatments, groupArr); - # Generate the config that the group assigner would have - groupConfig = TurkServer.Assigners.TutorialRandomizedGroupAssigner - .generateConfig(groupArr, multiGroupTreatments) + // Generate the config that the group assigner would have + const groupConfig = TurkServer.Assigners.TutorialRandomizedGroupAssigner + .generateConfig(groupArr, multiGroupTreatments); - created = [] + const created = []; - for conf, i in groupConfig - instance = batch.createInstance(conf.treatments) - instance.setup() + for (let i = 0; i < groupConfig.length; i++) { + const conf = groupConfig[i]; + const instance = batch.createInstance(conf.treatments); + instance.setup(); - created.push(instance.groupId) + created.push(instance.groupId); + } - batch.setAssigner(assigner) + batch.setAssigner(assigner); - # Test that there are 56 randomization slots now with the right allocation - test.isFalse assigner.autoAssign - test.isTrue assigner.bufferInstanceId + // Test that there are 56 randomization slots now with the right allocation + test.isFalse(assigner.autoAssign); + test.isTrue(assigner.bufferInstanceId); - test.length assigner.instanceSlots, 56 - test.equal assigner.instanceSlotIndex, 0 + test.length(assigner.instanceSlots, 56); + test.equal(assigner.instanceSlotIndex, 0); - allocation = _.countBy(assigner.instanceSlots, Object) - test.equal allocation[created[0]], 8 - test.equal allocation[created[1]], 16 - test.equal allocation[created[2]], 32 + const allocation = _.countBy(assigner.instanceSlots, Object); + test.equal(allocation[created[0]], 8); + test.equal(allocation[created[1]], 16); + return test.equal(allocation[created[2]], 32); +}) +); -Tinytest.add "assigners - tutorialRandomizedGroup - resume with partial allocation", withCleanup (test) -> - groupArr = [8, 16, 32] - assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( - tutorialTreatments, multiGroupTreatments, groupArr) +Tinytest.add("assigners - tutorialRandomizedGroup - resume with partial allocation", withCleanup(function(test) { + const groupArr = [8, 16, 32]; + const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( + tutorialTreatments, multiGroupTreatments, groupArr); - # Generate the config that the group assigner would have - groupConfig = TurkServer.Assigners.TutorialRandomizedGroupAssigner - .generateConfig(groupArr, multiGroupTreatments) + // Generate the config that the group assigner would have + const groupConfig = TurkServer.Assigners.TutorialRandomizedGroupAssigner + .generateConfig(groupArr, multiGroupTreatments); - created = [] + const created = []; - for conf, i in groupConfig - instance = batch.createInstance(conf.treatments) - instance.setup() + for (let i = 0; i < groupConfig.length; i++) { + const conf = groupConfig[i]; + const instance = batch.createInstance(conf.treatments); + instance.setup(); - # Fill each group half full - for j in [1..conf.size/2] - asst = createAssignment() + // Fill each group half full + for (let j = 1, end = conf.size/2, asc = 1 <= end; asc ? j <= end : j >= end; asc ? j++ : j--) { + const asst = createAssignment(); - # Pretend like this instance did the tutorial - tutorialInstance = batch.createInstance(tutorialTreatments) - tutorialInstance.setup() - tutorialInstance.addAssignment(asst) - tutorialInstance.teardown() + // Pretend like this instance did the tutorial + const tutorialInstance = batch.createInstance(tutorialTreatments); + tutorialInstance.setup(); + tutorialInstance.addAssignment(asst); + tutorialInstance.teardown(); - instance.addAssignment(asst) - - created.push(instance.groupId) - - # Run it - batch.setAssigner(assigner) - - # Test that there are 28 randomization slots now with the right allocation - # auto-assign should be enabled because there are people in it - test.isTrue assigner.autoAssign - test.isTrue assigner.bufferInstanceId - - test.length assigner.instanceSlots, 28 - test.equal assigner.instanceSlotIndex, 0 - - allocation = _.countBy(assigner.instanceSlots, Object) - test.equal allocation[created[0]], 8/2 - test.equal allocation[created[1]], 16/2 - test.equal allocation[created[2]], 32/2 - -Tinytest.add "assigners - tutorialRandomizedGroup - resume with fully allocated groups", withCleanup (test) -> - groupArr = [8, 16, 32] - assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( - tutorialTreatments, multiGroupTreatments, groupArr) - - # Generate the config that the group assigner would have - groupConfig = TurkServer.Assigners.TutorialRandomizedGroupAssigner - .generateConfig(groupArr, multiGroupTreatments) - - created = [] - - for conf, i in groupConfig - instance = batch.createInstance(conf.treatments) - instance.setup() - - # Fill each group half full - for j in [1..conf.size] - asst = createAssignment() + instance.addAssignment(asst); + } - # Pretend like this instance did the tutorial - tutorialInstance = batch.createInstance(tutorialTreatments) - tutorialInstance.setup() - tutorialInstance.addAssignment(asst) - tutorialInstance.teardown() + created.push(instance.groupId); + } - instance.addAssignment(asst) + // Run it + batch.setAssigner(assigner); - created.push(instance.groupId) + // Test that there are 28 randomization slots now with the right allocation + // auto-assign should be enabled because there are people in it + test.isTrue(assigner.autoAssign); + test.isTrue(assigner.bufferInstanceId); - # Run it - batch.setAssigner(assigner) + test.length(assigner.instanceSlots, 28); + test.equal(assigner.instanceSlotIndex, 0); - # auto-assign should be enabled because there are people in it - test.isTrue assigner.autoAssign - test.isTrue assigner.bufferInstanceId + const allocation = _.countBy(assigner.instanceSlots, Object); + test.equal(allocation[created[0]], 8/2); + test.equal(allocation[created[1]], 16/2); + return test.equal(allocation[created[2]], 32/2); +}) +); - test.length assigner.instanceSlots, 0 - test.equal assigner.instanceSlotIndex, 0 +Tinytest.add("assigners - tutorialRandomizedGroup - resume with fully allocated groups", withCleanup(function(test) { + const groupArr = [8, 16, 32]; + const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( + tutorialTreatments, multiGroupTreatments, groupArr); -Tinytest.add "assigners - tutorialRandomizedGroup - assign with waiting room and sequential", withCleanup (test) -> - groupArr = [8, 16, 32] + // Generate the config that the group assigner would have + const groupConfig = TurkServer.Assigners.TutorialRandomizedGroupAssigner + .generateConfig(groupArr, multiGroupTreatments); - assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( - tutorialTreatments, multiGroupTreatments, groupArr) + const created = []; - batch.setAssigner(assigner) + for (let i = 0; i < groupConfig.length; i++) { + const conf = groupConfig[i]; + const instance = batch.createInstance(conf.treatments); + instance.setup(); - assigner.setup() # Create instances + // Fill each group half full + for (let j = 1, end = conf.size, asc = 1 <= end; asc ? j <= end : j >= end; asc ? j++ : j--) { + const asst = createAssignment(); - test.isFalse assigner.autoAssign - test.length assigner.instanceSlots, 56 - test.equal assigner.instanceSlotIndex, 0 + // Pretend like this instance did the tutorial + const tutorialInstance = batch.createInstance(tutorialTreatments); + tutorialInstance.setup(); + tutorialInstance.addAssignment(asst); + tutorialInstance.teardown(); - # Get the config that the group assigner would have - groupConfigMulti = assigner.groupConfig + instance.addAssignment(asst); + } - assts = (createAssignment() for i in [1..64]) + created.push(instance.groupId); + } - # Pretend they have all done the tutorial - for asst in assts - Assignments.update asst.asstId, - $push: { instances: { id: Random.id() } } + // Run it + batch.setAssigner(assigner); - # Make the first half join - for i in [0..27] - TestUtils.connCallbacks.sessionReconnect({userId: assts[i].userId}) + // auto-assign should be enabled because there are people in it + test.isTrue(assigner.autoAssign); + test.isTrue(assigner.bufferInstanceId); - TestUtils.sleep(500) # Give enough time for lobby functions to process + test.length(assigner.instanceSlots, 0); + return test.equal(assigner.instanceSlotIndex, 0); +}) +); - # should have 32 users in lobby - test.equal LobbyStatus.find(batchId: batch.batchId).count(), 28 +Tinytest.add("assigners - tutorialRandomizedGroup - assign with waiting room and sequential", withCleanup(function(test) { + let exp, groupSize, instance, users; + let i; + const groupArr = [8, 16, 32]; - # Run auto-assign - assigner.assignAll() + const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( + tutorialTreatments, multiGroupTreatments, groupArr); - test.isTrue assigner.autoAssign - test.length assigner.instanceSlots, 56 - test.equal assigner.instanceSlotIndex, 28 + batch.setAssigner(assigner); - TestUtils.sleep(500) # Give enough time for lobby functions to process - # should have 0 users in lobby - test.equal LobbyStatus.find(batchId: batch.batchId).count(), 0 + assigner.setup(); // Create instances - exps = Experiments.find({ batchId: batch.batchId }).fetch() + test.isFalse(assigner.autoAssign); + test.length(assigner.instanceSlots, 56); + test.equal(assigner.instanceSlotIndex, 0); - # Check that the groups have the right size and treatments - totalAdded = 0 - for exp in exps - instance = TurkServer.Instance.getInstance(exp._id) - groupSize = instance.treatment().groupSize + // Get the config that the group assigner would have + const groupConfigMulti = assigner.groupConfig; - if groupSize? - users = instance.users() - test.isTrue users.length < groupSize - totalAdded += users.length - else # Buffer group should be empty - test.length instance.users(), 0 + const assts = ((() => { + const result = []; + for (i = 1; i <= 64; i++) { + result.push(createAssignment()); + } + return result; + })()); + + // Pretend they have all done the tutorial + for (let asst of Array.from(assts)) { + Assignments.update(asst.asstId, + {$push: { instances: { id: Random.id() } }}); + } + + // Make the first half join + for (i = 0; i <= 27; i++) { + TestUtils.connCallbacks.sessionReconnect({userId: assts[i].userId}); + } + + TestUtils.sleep(500); // Give enough time for lobby functions to process + + // should have 32 users in lobby + test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 28); + + // Run auto-assign + assigner.assignAll(); + + test.isTrue(assigner.autoAssign); + test.length(assigner.instanceSlots, 56); + test.equal(assigner.instanceSlotIndex, 28); + + TestUtils.sleep(500); // Give enough time for lobby functions to process + // should have 0 users in lobby + test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 0); + + const exps = Experiments.find({ batchId: batch.batchId }).fetch(); + + // Check that the groups have the right size and treatments + let totalAdded = 0; + for (exp of Array.from(exps)) { + instance = TurkServer.Instance.getInstance(exp._id); + ({ + groupSize + } = instance.treatment()); + + if (groupSize != null) { + users = instance.users(); + test.isTrue(users.length < groupSize); + totalAdded += users.length; + } else { // Buffer group should be empty + test.length(instance.users(), 0); + } + } - test.equal totalAdded, 28 + test.equal(totalAdded, 28); - # Fill in remaining users - for i in [28..63] - TestUtils.connCallbacks.sessionReconnect({userId: assts[i].userId}) + // Fill in remaining users + for (i = 28; i <= 63; i++) { + TestUtils.connCallbacks.sessionReconnect({userId: assts[i].userId}); + } - test.isTrue assigner.autoAssign - test.length assigner.instanceSlots, 56 - test.equal assigner.instanceSlotIndex, 56 + test.isTrue(assigner.autoAssign); + test.length(assigner.instanceSlots, 56); + test.equal(assigner.instanceSlotIndex, 56); - TestUtils.sleep(800) + TestUtils.sleep(800); - # Should have no one in lobby - test.equal LobbyStatus.find(batchId: batch.batchId).count(), 0 + // Should have no one in lobby + test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 0); - # All groups should be filled with 8 in buffer - totalAdded = 0 - for exp in exps - instance = TurkServer.Instance.getInstance(exp._id) - groupSize = instance.treatment().groupSize + // All groups should be filled with 8 in buffer + totalAdded = 0; + for (exp of Array.from(exps)) { + instance = TurkServer.Instance.getInstance(exp._id); + ({ + groupSize + } = instance.treatment()); - users = instance.users() + users = instance.users(); - if groupSize? - test.length users, groupSize - totalAdded += users.length - else # Buffer group should have 8 users - test.length users, 8 - totalAdded += users.length + if (groupSize != null) { + test.length(users, groupSize); + totalAdded += users.length; + } else { // Buffer group should have 8 users + test.length(users, 8); + totalAdded += users.length; + } + } - test.equal totalAdded, 64 + test.equal(totalAdded, 64); - # Test auto-stopping - lastInstance = TurkServer.Instance.getInstance(assigner.bufferInstanceId) - lastInstance.teardown() + // Test auto-stopping + const lastInstance = TurkServer.Instance.getInstance(assigner.bufferInstanceId); + lastInstance.teardown(); - slackerAsst = createAssignment() + const slackerAsst = createAssignment(); - Assignments.update slackerAsst.asstId, - $push: { instances: { id: Random.id() } } + Assignments.update(slackerAsst.asstId, + {$push: { instances: { id: Random.id() } }}); - TestUtils.connCallbacks.sessionReconnect({userId: slackerAsst.userId}) + TestUtils.connCallbacks.sessionReconnect({userId: slackerAsst.userId}); - TestUtils.sleep(150) + TestUtils.sleep(150); - # ensure that user is still in lobby - user = Meteor.users.findOne(slackerAsst.userId) - instances = slackerAsst.getInstances() + // ensure that user is still in lobby + const user = Meteor.users.findOne(slackerAsst.userId); + const instances = slackerAsst.getInstances(); - # should still be in lobby - test.equal user.turkserver.state, "lobby" - test.length instances, 1 - test.equal LobbyStatus.find(batchId: batch.batchId).count(), 1 + // should still be in lobby + test.equal(user.turkserver.state, "lobby"); + test.length(instances, 1); + return test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 1); +}) +); -### +/* Multi-group assigner -### - -Tinytest.add "assigners - tutorialMultiGroup - initial lobby gets tutorial", withCleanup (test) -> - assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( - tutorialTreatments, multiGroupTreatments, [16, 16]) - batch.setAssigner(assigner) - - asst = createAssignment() - TestUtils.connCallbacks.sessionReconnect {userId: asst.userId} - - TestUtils.sleep(150) # YES!! - - user = Meteor.users.findOne(asst.userId) - instances = asst.getInstances() - - # should be in experiment - test.equal user.turkserver.state, "experiment" - test.length instances, 1 - # should not be in lobby - test.equal LobbyStatus.find(batchId: batch.batchId).count(), 0 - # should be in a tutorial - exp = Experiments.findOne(instances[0].id) - test.equal exp.treatments, tutorialTreatments - -Tinytest.add "assigners - tutorialMultiGroup - resumes from partial", withCleanup (test) -> - groupArr = [ 1, 1, 1, 1, 2, 2, 4, 4, 8, 16, 32, 16, 8, 4, 4, 2, 2, 1, 1, 1, 1 ] - - assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( - tutorialTreatments, multiGroupTreatments, groupArr) - - # Generate the config that the group assigner would have - groupConfigMulti = TurkServer.Assigners.TutorialMultiGroupAssigner - .generateConfig(groupArr, multiGroupTreatments) - - borkedGroup = 10 - filledAmount = 16 - - # Say we are in the middle of the group of 32: index 10 - for conf, i in groupConfigMulti - break if i == borkedGroup - - instance = batch.createInstance(conf.treatments) - instance.setup() - - for j in [1..conf.size] - asst = createAssignment() - - # Pretend like this instance did the tutorial - tutorialInstance = batch.createInstance(tutorialTreatments) - tutorialInstance.setup() - tutorialInstance.addAssignment(asst) - tutorialInstance.teardown() - - instance.addAssignment(asst) +*/ + +Tinytest.add("assigners - tutorialMultiGroup - initial lobby gets tutorial", withCleanup(function(test) { + const assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( + tutorialTreatments, multiGroupTreatments, [16, 16]); + batch.setAssigner(assigner); + + const asst = createAssignment(); + TestUtils.connCallbacks.sessionReconnect({userId: asst.userId}); + + TestUtils.sleep(150); // YES!! + + const user = Meteor.users.findOne(asst.userId); + const instances = asst.getInstances(); + + // should be in experiment + test.equal(user.turkserver.state, "experiment"); + test.length(instances, 1); + // should not be in lobby + test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 0); + // should be in a tutorial + const exp = Experiments.findOne(instances[0].id); + return test.equal(exp.treatments, tutorialTreatments); +}) +); + +Tinytest.add("assigners - tutorialMultiGroup - resumes from partial", withCleanup(function(test) { + let conf, instance, j; + let asc1, end1; + const groupArr = [ 1, 1, 1, 1, 2, 2, 4, 4, 8, 16, 32, 16, 8, 4, 4, 2, 2, 1, 1, 1, 1 ]; + + const assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( + tutorialTreatments, multiGroupTreatments, groupArr); + + // Generate the config that the group assigner would have + const groupConfigMulti = TurkServer.Assigners.TutorialMultiGroupAssigner + .generateConfig(groupArr, multiGroupTreatments); + + const borkedGroup = 10; + const filledAmount = 16; + + // Say we are in the middle of the group of 32: index 10 + for (let i = 0; i < groupConfigMulti.length; i++) { + var asc, end; + conf = groupConfigMulti[i]; + if (i === borkedGroup) { break; } + + instance = batch.createInstance(conf.treatments); + instance.setup(); + + for (j = 1, end = conf.size, asc = 1 <= end; asc ? j <= end : j >= end; asc ? j++ : j--) { + const asst = createAssignment(); + + // Pretend like this instance did the tutorial + const tutorialInstance = batch.createInstance(tutorialTreatments); + tutorialInstance.setup(); + tutorialInstance.addAssignment(asst); + tutorialInstance.teardown(); + + instance.addAssignment(asst); + } + } - conf = groupConfigMulti[borkedGroup] - instance = batch.createInstance(conf.treatments) - instance.setup() - ( instance.addAssignment(createAssignment()) for j in [1..filledAmount] ) + conf = groupConfigMulti[borkedGroup]; + instance = batch.createInstance(conf.treatments); + instance.setup(); + for (j = 1, end1 = filledAmount, asc1 = 1 <= end1; asc1 ? j <= end1 : j >= end1; asc1 ? j++ : j--) { instance.addAssignment(createAssignment()); } - batch.setAssigner(assigner) + batch.setAssigner(assigner); - test.equal assigner.currentGroup, borkedGroup - test.equal assigner.currentInstance, instance - test.equal assigner.currentFilled, filledAmount + test.equal(assigner.currentGroup, borkedGroup); + test.equal(assigner.currentInstance, instance); + return test.equal(assigner.currentFilled, filledAmount); +}) +); -Tinytest.add "assigners - tutorialMultiGroup - resumes on group boundary", withCleanup (test) -> - groupArr = [ 1, 1, 1, 1, 2, 2, 4, 4, 8, 16, 32, 16, 8, 4, 4, 2, 2, 1, 1, 1, 1 ] +Tinytest.add("assigners - tutorialMultiGroup - resumes on group boundary", withCleanup(function(test) { + let instance; + const groupArr = [ 1, 1, 1, 1, 2, 2, 4, 4, 8, 16, 32, 16, 8, 4, 4, 2, 2, 1, 1, 1, 1 ]; - assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( - tutorialTreatments, multiGroupTreatments, groupArr) + const assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( + tutorialTreatments, multiGroupTreatments, groupArr); - # Generate the config that the group assigner would have - groupConfigMulti = TurkServer.Assigners.TutorialMultiGroupAssigner - .generateConfig(groupArr, multiGroupTreatments) + // Generate the config that the group assigner would have + const groupConfigMulti = TurkServer.Assigners.TutorialMultiGroupAssigner + .generateConfig(groupArr, multiGroupTreatments); - borkedGroup = 2 + const borkedGroup = 2; - # Say we are in the middle of the group of 32: index 10 - for conf, i in groupConfigMulti - break if i == borkedGroup + // Say we are in the middle of the group of 32: index 10 + for (let i = 0; i < groupConfigMulti.length; i++) { + const conf = groupConfigMulti[i]; + if (i === borkedGroup) { break; } - instance = batch.createInstance(conf.treatments) - instance.setup() + instance = batch.createInstance(conf.treatments); + instance.setup(); - for j in [1..conf.size] - asst = createAssignment() + for (let j = 1, end = conf.size, asc = 1 <= end; asc ? j <= end : j >= end; asc ? j++ : j--) { + const asst = createAssignment(); - # Pretend like this instance did the tutorial - tutorialInstance = batch.createInstance(tutorialTreatments) - tutorialInstance.setup() - tutorialInstance.addAssignment(asst) - tutorialInstance.teardown() + // Pretend like this instance did the tutorial + const tutorialInstance = batch.createInstance(tutorialTreatments); + tutorialInstance.setup(); + tutorialInstance.addAssignment(asst); + tutorialInstance.teardown(); - instance.addAssignment(asst) + instance.addAssignment(asst); + } + } - batch.setAssigner(assigner) + batch.setAssigner(assigner); - test.equal assigner.currentGroup, borkedGroup - 1 - test.equal assigner.currentInstance, instance - test.equal assigner.currentFilled, groupConfigMulti[borkedGroup - 1].size + test.equal(assigner.currentGroup, borkedGroup - 1); + test.equal(assigner.currentInstance, instance); + test.equal(assigner.currentFilled, groupConfigMulti[borkedGroup - 1].size); - # Test reconfiguration into new groups - newArray = [16, 16] - assigner.configure(newArray) + // Test reconfiguration into new groups + const newArray = [16, 16]; + assigner.configure(newArray); - test.equal assigner.groupArray, newArray - test.equal assigner.currentGroup, -1 - test.equal assigner.currentInstance, null - test.equal assigner.currentFilled, 0 + test.equal(assigner.groupArray, newArray); + test.equal(assigner.currentGroup, -1); + test.equal(assigner.currentInstance, null); + return test.equal(assigner.currentFilled, 0); +}) +); -Tinytest.add "assigners - tutorialMultiGroup - send to exit survey", withCleanup (test) -> - assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( - tutorialTreatments, multiGroupTreatments, [16, 16]) +Tinytest.add("assigners - tutorialMultiGroup - send to exit survey", withCleanup(function(test) { + const assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( + tutorialTreatments, multiGroupTreatments, [16, 16]); - batch.setAssigner(assigner) + batch.setAssigner(assigner); - asst = createAssignment() - # Pretend we already have two instances done - Assignments.update asst.asstId, + const asst = createAssignment(); + // Pretend we already have two instances done + Assignments.update(asst.asstId, { $push: { instances: { $each: [ @@ -617,92 +699,108 @@ Tinytest.add "assigners - tutorialMultiGroup - send to exit survey", withCleanup ] } } + }); - TestUtils.connCallbacks.sessionReconnect({userId: asst.userId}) + TestUtils.connCallbacks.sessionReconnect({userId: asst.userId}); - TestUtils.sleep(100) + TestUtils.sleep(100); - user = Meteor.users.findOne(asst.userId) - instances = asst.getInstances() + const user = Meteor.users.findOne(asst.userId); + const instances = asst.getInstances(); - test.equal user.turkserver.state, "exitsurvey" - test.length instances, 2 + test.equal(user.turkserver.state, "exitsurvey"); + return test.length(instances, 2); +}) +); -Tinytest.add "assigners - tutorialMultiGroup - simultaneous multiple assignment", withCleanup (test) -> - groupArr = [ 1, 1, 1, 1, 2, 2, 4, 4, 8, 16, 32 ] +Tinytest.add("assigners - tutorialMultiGroup - simultaneous multiple assignment", withCleanup(function(test) { + let asst; + let i; + const groupArr = [ 1, 1, 1, 1, 2, 2, 4, 4, 8, 16, 32 ]; - assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( - tutorialTreatments, multiGroupTreatments, groupArr) + const assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( + tutorialTreatments, multiGroupTreatments, groupArr); - batch.setAssigner(assigner) + batch.setAssigner(assigner); - # Get the config that the group assigner would have - groupConfigMulti = assigner.groupConfig + // Get the config that the group assigner would have + const groupConfigMulti = assigner.groupConfig; - assts = (createAssignment() for i in [1..80]) + const assts = ((() => { + const result = []; + for (i = 1; i <= 80; i++) { + result.push(createAssignment()); + } + return result; + })()); - # Pretend they have all done the tutorial - for asst in assts - Assignments.update asst.asstId, - $push: { instances: { id: Random.id() } } + // Pretend they have all done the tutorial + for (asst of Array.from(assts)) { + Assignments.update(asst.asstId, + {$push: { instances: { id: Random.id() } }}); + } - # Make them all join simultaneously - lobby join is deferred - for asst in assts - # TODO some sort of weirdness (write fence?) prevents us from deferring these - TestUtils.connCallbacks.sessionReconnect({userId: asst.userId}) + // Make them all join simultaneously - lobby join is deferred + for (asst of Array.from(assts)) { + // TODO some sort of weirdness (write fence?) prevents us from deferring these + TestUtils.connCallbacks.sessionReconnect({userId: asst.userId}); + } - TestUtils.sleep(500) # Give enough time for lobby functions to process + TestUtils.sleep(500); // Give enough time for lobby functions to process - exps = Experiments.find({batchId: batch.batchId}, {sort: {startTime: 1}}).fetch() + const exps = Experiments.find({batchId: batch.batchId}, {sort: {startTime: 1}}).fetch(); - # Check that the groups have the right size and treatments - i = 0 - while i < groupConfigMulti.length - group = groupConfigMulti[i] - exp = exps[i] + // Check that the groups have the right size and treatments + i = 0; + while (i < groupConfigMulti.length) { + const group = groupConfigMulti[i]; + const exp = exps[i]; - test.equal(exp.treatments[0], group.treatments[0]) - test.equal(exp.treatments[1], group.treatments[1]) + test.equal(exp.treatments[0], group.treatments[0]); + test.equal(exp.treatments[1], group.treatments[1]); - test.equal(exp.users.length, group.size) + test.equal(exp.users.length, group.size); - i++ + i++; + } - test.length exps, groupConfigMulti.length + test.length(exps, groupConfigMulti.length); - # Should have people in lobby - test.equal LobbyStatus.find(batchId: batch.batchId).count(), 8 + // Should have people in lobby + test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 8); - # Test auto-stopping - lastInstance = TurkServer.Instance.getInstance(exps[exps.length - 1]._id) - lastInstance.teardown() + // Test auto-stopping + const lastInstance = TurkServer.Instance.getInstance(exps[exps.length - 1]._id); + lastInstance.teardown(); - slackerAsst = createAssignment() + const slackerAsst = createAssignment(); - Assignments.update slackerAsst.asstId, - $push: { instances: { id: Random.id() } } + Assignments.update(slackerAsst.asstId, + {$push: { instances: { id: Random.id() } }}); - TestUtils.connCallbacks.sessionReconnect({userId: slackerAsst.userId}) + TestUtils.connCallbacks.sessionReconnect({userId: slackerAsst.userId}); - TestUtils.sleep(150) + TestUtils.sleep(150); - # assigner should have stopped - test.equal assigner.stopped, true + // assigner should have stopped + test.equal(assigner.stopped, true); - # ensure that user is still in lobby - user = Meteor.users.findOne(slackerAsst.userId) - instances = slackerAsst.getInstances() + // ensure that user is still in lobby + const user = Meteor.users.findOne(slackerAsst.userId); + const instances = slackerAsst.getInstances(); - # should still be in lobby - test.equal user.turkserver.state, "lobby" - test.length instances, 1 - test.equal LobbyStatus.find(batchId: batch.batchId).count(), 9 + // should still be in lobby + test.equal(user.turkserver.state, "lobby"); + test.length(instances, 1); + test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 9); - # Test resetting, if we launch new set a different day - batch.lobby.events.emit("reset-multi-groups") + // Test resetting, if we launch new set a different day + batch.lobby.events.emit("reset-multi-groups"); - test.equal assigner.stopped, false - test.equal assigner.groupArray, groupArr # Still same config - test.equal assigner.currentGroup, -1 - test.equal assigner.currentInstance, null - test.equal assigner.currentFilled, 0 + test.equal(assigner.stopped, false); + test.equal(assigner.groupArray, groupArr); // Still same config + test.equal(assigner.currentGroup, -1); + test.equal(assigner.currentInstance, null); + return test.equal(assigner.currentFilled, 0); +}) +); diff --git a/tests/auth_tests.js b/tests/auth_tests.js index 4b7a7f6..2fa07bc 100644 --- a/tests/auth_tests.js +++ b/tests/auth_tests.js @@ -1,385 +1,465 @@ -hitType = "authHitType" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const hitType = "authHitType"; -hitId = "authHitId" -hitId2 = "authHitId2" +const hitId = "authHitId"; +const hitId2 = "authHitId2"; -assignmentId = "authAssignmentId" -assignmentId2 = "authAssignmentId2" +const assignmentId = "authAssignmentId"; +const assignmentId2 = "authAssignmentId2"; -workerId = "authWorkerId" -workerId2 = "authWorkerId2" +const workerId = "authWorkerId"; +const workerId2 = "authWorkerId2"; -experimentId = "authExperimentId" +const experimentId = "authExperimentId"; -# Ensure that users with these workerIds exist -Meteor.users.upsert "authUser1", $set: {workerId} -Meteor.users.upsert "authUser2", $set: {workerId: workerId2} +// Ensure that users with these workerIds exist +Meteor.users.upsert("authUser1", {$set: {workerId}}); +Meteor.users.upsert("authUser2", {$set: {workerId: workerId2}}); -authBatchId = "authBatch" -otherBatchId = "someOtherBatch" +const authBatchId = "authBatch"; +const otherBatchId = "someOtherBatch"; -# Set up a dummy batch -unless Batches.findOne(authBatchId)? - Batches.insert(_id: authBatchId) +// Set up a dummy batch +if (Batches.findOne(authBatchId) == null) { + Batches.insert({_id: authBatchId}); +} -# Set up a dummy HIT type and HITs -HITTypes.upsert HITTypeId: hitType, - $set: +// Set up a dummy HIT type and HITs +HITTypes.upsert({HITTypeId: hitType}, { + $set: { batchId: authBatchId -HITs.upsert HITId: hitId, - $set: HITTypeId: hitType -HITs.upsert HITId: hitId2, - $set: HITTypeId: hitType - -# We can use the after wrapper here because the tests are synchronous -withCleanup = TestUtils.getCleanupWrapper - before: -> - Batches.update authBatchId, - $set: active: true - $unset: allowReturns: null - after: -> - # Only remove assignments created here to avoid side effects on server-client tests - Assignments.remove($or: [ {batchId: authBatchId}, {batchId: otherBatchId} ]) - -Tinytest.add "auth - with first time hit assignment", withCleanup (test) -> - asst = TestUtils.authenticateWorker - batchId: authBatchId - hitId: hitId - assignmentId: assignmentId - workerId: workerId - - # Test in-memory stored values - test.equal asst.batchId, authBatchId - test.equal asst.hitId, hitId - test.equal asst.assignmentId, assignmentId - test.equal asst.workerId, workerId - test.equal asst.userId, "authUser1" - - # Test database storage - record = Assignments.findOne - hitId: hitId - assignmentId: assignmentId - - test.isTrue(record) - test.equal(record.workerId, workerId, "workerId not saved") - test.equal(record.batchId, authBatchId) - -Tinytest.add "auth - reject incorrect batch", withCleanup (test) -> - testFunc = -> TestUtils.authenticateWorker - batchId: otherBatchId - hitId: hitId - assignmentId: assignmentId - workerId: workerId - - test.throws testFunc, (e) -> - e.error is 403 and e.reason is ErrMsg.unexpectedBatch - -Tinytest.add "auth - connection to inactive batch is rejected", withCleanup (test) -> - # Active is set to back to true on cleanup - Batches.update(authBatchId, $unset: active: false) - - testFunc = -> TestUtils.authenticateWorker - batchId: authBatchId - hitId: hitId - assignmentId: assignmentId - workerId: workerId - - test.throws testFunc, (e) -> - e.error is 403 and e.reason is ErrMsg.batchInactive - -Tinytest.add "auth - reconnect - with existing hit assignment", withCleanup (test) -> - Assignments.insert - batchId: authBatchId - hitId: hitId - assignmentId: assignmentId - workerId: workerId + } +} +); +HITs.upsert({HITId: hitId}, + {$set: {HITTypeId: hitType}}); +HITs.upsert({HITId: hitId2}, + {$set: {HITTypeId: hitType}}); + +// We can use the after wrapper here because the tests are synchronous +const withCleanup = TestUtils.getCleanupWrapper({ + before() { + return Batches.update(authBatchId, { + $set: { active: true + }, + $unset: { allowReturns: null + } + } + ); + }, + after() { + // Only remove assignments created here to avoid side effects on server-client tests + return Assignments.remove({$or: [ {batchId: authBatchId}, {batchId: otherBatchId} ]}); + } +}); + +Tinytest.add("auth - with first time hit assignment", withCleanup(function(test) { + const asst = TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId, + assignmentId, + workerId + }); + + // Test in-memory stored values + test.equal(asst.batchId, authBatchId); + test.equal(asst.hitId, hitId); + test.equal(asst.assignmentId, assignmentId); + test.equal(asst.workerId, workerId); + test.equal(asst.userId, "authUser1"); + + // Test database storage + const record = Assignments.findOne({ + hitId, + assignmentId + }); + + test.isTrue(record); + test.equal(record.workerId, workerId, "workerId not saved"); + return test.equal(record.batchId, authBatchId); +}) +); + +Tinytest.add("auth - reject incorrect batch", withCleanup(function(test) { + const testFunc = () => TestUtils.authenticateWorker({ + batchId: otherBatchId, + hitId, + assignmentId, + workerId + }); + + return test.throws(testFunc, e => (e.error === 403) && (e.reason === ErrMsg.unexpectedBatch)); +}) +); + +Tinytest.add("auth - connection to inactive batch is rejected", withCleanup(function(test) { + // Active is set to back to true on cleanup + Batches.update(authBatchId, {$unset: {active: false}}); + + const testFunc = () => TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId, + assignmentId, + workerId + }); + + return test.throws(testFunc, e => (e.error === 403) && (e.reason === ErrMsg.batchInactive)); +}) +); + +Tinytest.add("auth - reconnect - with existing hit assignment", withCleanup(function(test) { + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, status: "assigned" - - # This needs to return an assignment - asst = TestUtils.authenticateWorker - batchId: authBatchId - hitId: hitId - assignmentId : assignmentId - workerId: workerId - - record = Assignments.findOne - hitId: hitId - assignmentId: assignmentId - workerId: workerId - - test.equal(asst, TurkServer.Assignment.getAssignment(record._id)) - test.equal asst.batchId, authBatchId - test.equal asst.hitId, hitId - test.equal asst.assignmentId, assignmentId - test.equal asst.workerId, workerId - test.equal asst.userId, "authUser1" - - test.equal(record.status, "assigned") - -Tinytest.add "auth - reconnect - with existing hit after batch is inactive", withCleanup (test) -> - # Active is set to back to true on cleanup - Batches.update(authBatchId, $unset: active: false) - - Assignments.insert - batchId: authBatchId - hitId: hitId - assignmentId: assignmentId - workerId: workerId + }); + + // This needs to return an assignment + const asst = TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId, + assignmentId, + workerId + }); + + const record = Assignments.findOne({ + hitId, + assignmentId, + workerId + }); + + test.equal(asst, TurkServer.Assignment.getAssignment(record._id)); + test.equal(asst.batchId, authBatchId); + test.equal(asst.hitId, hitId); + test.equal(asst.assignmentId, assignmentId); + test.equal(asst.workerId, workerId); + test.equal(asst.userId, "authUser1"); + + return test.equal(record.status, "assigned"); +}) +); + +Tinytest.add("auth - reconnect - with existing hit after batch is inactive", withCleanup(function(test) { + // Active is set to back to true on cleanup + Batches.update(authBatchId, {$unset: {active: false}}); + + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, status: "assigned" - - TestUtils.authenticateWorker - batchId: authBatchId - hitId: hitId - assignmentId : assignmentId - workerId: workerId - - record = Assignments.findOne - hitId: hitId - assignmentId: assignmentId - workerId: workerId - - test.equal(record.status, "assigned") - -Tinytest.add "auth - with overlapping hit in experiment", withCleanup (test) -> - Assignments.insert - batchId: authBatchId - hitId: hitId - assignmentId: assignmentId - workerId: workerId - status: "assigned" - experimentId: experimentId - - # Authenticate with different worker - asst = TestUtils.authenticateWorker - batchId: authBatchId - hitId: hitId - assignmentId : assignmentId + }); + + TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId, + assignmentId, + workerId + }); + + const record = Assignments.findOne({ + hitId, + assignmentId, + workerId + }); + + return test.equal(record.status, "assigned"); +}) +); + +Tinytest.add("auth - with overlapping hit in experiment", withCleanup(function(test) { + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, + status: "assigned", + experimentId + }); + + // Authenticate with different worker + const asst = TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId, + assignmentId, workerId: workerId2 + }); - prevRecord = Assignments.findOne - hitId: hitId - assignmentId: assignmentId - workerId: workerId + const prevRecord = Assignments.findOne({ + hitId, + assignmentId, + workerId + }); - newRecord = Assignments.findOne - hitId: hitId - assignmentId: assignmentId + const newRecord = Assignments.findOne({ + hitId, + assignmentId, workerId: workerId2 + }); - test.isTrue(asst) - test.equal(asst, TurkServer.Assignment.getAssignment(newRecord._id)) + test.isTrue(asst); + test.equal(asst, TurkServer.Assignment.getAssignment(newRecord._id)); - test.equal(prevRecord.status, "returned") + test.equal(prevRecord.status, "returned"); - test.equal(newRecord.status, "assigned") + return test.equal(newRecord.status, "assigned"); +}) +); -Tinytest.add "auth - with overlapping hit completed", withCleanup (test) -> - # This case should not happen often - Assignments.insert - batchId: authBatchId - hitId: hitId - assignmentId: assignmentId - workerId: workerId +Tinytest.add("auth - with overlapping hit completed", withCleanup(function(test) { + // This case should not happen often + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, status: "completed" + }); - # Authenticate with different worker - asst = TestUtils.authenticateWorker - batchId: authBatchId - hitId: hitId - assignmentId : assignmentId + // Authenticate with different worker + const asst = TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId, + assignmentId, workerId: workerId2 + }); - prevRecord = Assignments.findOne - hitId: hitId - assignmentId: assignmentId - workerId: workerId + const prevRecord = Assignments.findOne({ + hitId, + assignmentId, + workerId + }); - newRecord = Assignments.findOne - hitId: hitId - assignmentId: assignmentId + const newRecord = Assignments.findOne({ + hitId, + assignmentId, workerId: workerId2 + }); - test.isTrue(asst) - test.equal(asst, TurkServer.Assignment.getAssignment(newRecord._id)) + test.isTrue(asst); + test.equal(asst, TurkServer.Assignment.getAssignment(newRecord._id)); - test.equal(prevRecord.status, "completed") + test.equal(prevRecord.status, "completed"); - test.equal(newRecord.status, "assigned") + return test.equal(newRecord.status, "assigned"); +}) +); -Tinytest.add "auth - same worker completed hit", withCleanup (test) -> - Assignments.insert - batchId: authBatchId - hitId: hitId - assignmentId: assignmentId - workerId: workerId +Tinytest.add("auth - same worker completed hit", withCleanup(function(test) { + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, status: "completed" - - testFunc = -> TestUtils.authenticateWorker - batchId: authBatchId - hitId: hitId, - assignmentId : assignmentId - workerId: workerId - - test.throws testFunc, (e) -> - e.error is 403 and e.reason is ErrMsg.alreadyCompleted - -Tinytest.add "auth - limit - concurrent across hits", withCleanup (test) -> - Assignments.insert - batchId: authBatchId - hitId: hitId - assignmentId: assignmentId - workerId: workerId + }); + + const testFunc = () => TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId, + assignmentId, + workerId + }); + + return test.throws(testFunc, e => (e.error === 403) && (e.reason === ErrMsg.alreadyCompleted)); +}) +); + +Tinytest.add("auth - limit - concurrent across hits", withCleanup(function(test) { + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, status: "assigned" + }); - testFunc = -> TestUtils.authenticateWorker - batchId: authBatchId + const testFunc = () => TestUtils.authenticateWorker({ + batchId: authBatchId, hitId: hitId2, - assignmentId : assignmentId2 - workerId: workerId - - test.throws testFunc, (e) -> - e.error is 403 and e.reason is ErrMsg.simultaneousLimit - -# Not sure this test needs to exist because only 1 assignment per worker for a HIT -Tinytest.add "auth - limit - concurrent across assts", withCleanup (test) -> - Assignments.insert - batchId: authBatchId - hitId: hitId - assignmentId: assignmentId - workerId: workerId + assignmentId : assignmentId2, + workerId + }); + + return test.throws(testFunc, e => (e.error === 403) && (e.reason === ErrMsg.simultaneousLimit)); +}) +); + +// Not sure this test needs to exist because only 1 assignment per worker for a HIT +Tinytest.add("auth - limit - concurrent across assts", withCleanup(function(test) { + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, status: "assigned" - - testFunc = -> TestUtils.authenticateWorker - batchId: authBatchId - hitId: hitId, - assignmentId : assignmentId2 - workerId: workerId - - test.throws testFunc, (e) -> - e.error is 403 and e.reason is ErrMsg.simultaneousLimit - -Tinytest.add "auth - limit - too many total", withCleanup (test) -> - Assignments.insert - batchId: authBatchId - hitId: hitId - assignmentId: assignmentId - workerId: workerId + }); + + const testFunc = () => TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId, + assignmentId : assignmentId2, + workerId + }); + + return test.throws(testFunc, e => (e.error === 403) && (e.reason === ErrMsg.simultaneousLimit)); +}) +); + +Tinytest.add("auth - limit - too many total", withCleanup(function(test) { + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, status: "completed" - # Should not trigger concurrent limit + }); + // Should not trigger concurrent limit - testFunc = -> TestUtils.authenticateWorker - batchId: authBatchId - hitId: hitId2 - assignmentId : assignmentId2 - workerId: workerId - - test.throws testFunc, (e) -> e.error is 403 and e.reason is ErrMsg.batchLimit - -Tinytest.add "auth - limit - returns not allowed in batch", withCleanup (test) -> - Assignments.insert - batchId: authBatchId - hitId: hitId - assignmentId: assignmentId - workerId: workerId + const testFunc = () => TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId: hitId2, + assignmentId : assignmentId2, + workerId + }); + + return test.throws(testFunc, e => (e.error === 403) && (e.reason === ErrMsg.batchLimit)); +}) +); + +Tinytest.add("auth - limit - returns not allowed in batch", withCleanup(function(test) { + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, status: "returned" - # Should not trigger concurrent limit - - testFunc = -> TestUtils.authenticateWorker - batchId: authBatchId - hitId: hitId2 - assignmentId : assignmentId2 - workerId: workerId + }); + // Should not trigger concurrent limit - test.throws testFunc, (e) -> e.error is 403 and e.reason is ErrMsg.batchLimit + const testFunc = () => TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId: hitId2, + assignmentId : assignmentId2, + workerId + }); + + return test.throws(testFunc, e => (e.error === 403) && (e.reason === ErrMsg.batchLimit)); +}) +); + +Tinytest.add("auth - limit - returns allowed in batch", withCleanup(function(test) { + Batches.update(authBatchId, {$set: {allowReturns: true}}); + + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, + status: "returned" + }); -Tinytest.add "auth - limit - returns allowed in batch", withCleanup (test) -> - Batches.update(authBatchId, $set: allowReturns: true) + const asst = TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId: hitId2, + assignmentId : assignmentId2, + workerId + }); - Assignments.insert - batchId: authBatchId - hitId: hitId - assignmentId: assignmentId - workerId: workerId - status: "returned" + const prevRecord = Assignments.findOne({ + hitId, + assignmentId, + workerId + }); - asst = TestUtils.authenticateWorker - batchId: authBatchId + const newRecord = Assignments.findOne({ hitId: hitId2, - assignmentId : assignmentId2 - workerId: workerId - - prevRecord = Assignments.findOne - hitId: hitId - assignmentId: assignmentId - workerId: workerId - - newRecord = Assignments.findOne - hitId: hitId2 - assignmentId: assignmentId2 - workerId: workerId - - test.isTrue(asst) - test.equal(asst, TurkServer.Assignment.getAssignment(newRecord._id)) - - test.equal(prevRecord.status, "returned") - test.equal(prevRecord.batchId, authBatchId) - - test.equal(newRecord.status, "assigned") - test.equal(newRecord.batchId, authBatchId) - -Tinytest.add "auth - limit - allowed after previous batch", withCleanup (test) -> - Assignments.insert - batchId: otherBatchId - hitId: hitId - assignmentId: assignmentId - workerId: workerId + assignmentId: assignmentId2, + workerId + }); + + test.isTrue(asst); + test.equal(asst, TurkServer.Assignment.getAssignment(newRecord._id)); + + test.equal(prevRecord.status, "returned"); + test.equal(prevRecord.batchId, authBatchId); + + test.equal(newRecord.status, "assigned"); + return test.equal(newRecord.batchId, authBatchId); +}) +); + +Tinytest.add("auth - limit - allowed after previous batch", withCleanup(function(test) { + Assignments.insert({ + batchId: otherBatchId, + hitId, + assignmentId, + workerId, status: "completed" - # Should not trigger concurrent limit + }); + // Should not trigger concurrent limit - asst = TestUtils.authenticateWorker - batchId: authBatchId + const asst = TestUtils.authenticateWorker({ + batchId: authBatchId, hitId: hitId2, - assignmentId : assignmentId2 - workerId: workerId - - prevRecord = Assignments.findOne - hitId: hitId - assignmentId: assignmentId - workerId: workerId + assignmentId : assignmentId2, + workerId + }); - newRecord = Assignments.findOne - hitId: hitId2 - assignmentId: assignmentId2 - workerId: workerId + const prevRecord = Assignments.findOne({ + hitId, + assignmentId, + workerId + }); - test.isTrue(asst) - test.equal(asst, TurkServer.Assignment.getAssignment(newRecord._id)) - - test.equal(prevRecord.status, "completed") - test.equal(prevRecord.batchId, "someOtherBatch") - - test.equal(newRecord.status, "assigned") - test.equal(newRecord.batchId, authBatchId) - -# Worker is used for the test below -Meteor.users.upsert "testWorker", $set: {workerId: "testingWorker"} - -Tinytest.add "auth - testing HIT login doesn't require existing HIT", withCleanup (test) -> - asst = TestUtils.authenticateWorker - batchId: authBatchId - hitId: "testingHIT" - assignmentId: "testingAsst" - workerId: "testingWorker" + const newRecord = Assignments.findOne({ + hitId: hitId2, + assignmentId: assignmentId2, + workerId + }); + + test.isTrue(asst); + test.equal(asst, TurkServer.Assignment.getAssignment(newRecord._id)); + + test.equal(prevRecord.status, "completed"); + test.equal(prevRecord.batchId, "someOtherBatch"); + + test.equal(newRecord.status, "assigned"); + return test.equal(newRecord.batchId, authBatchId); +}) +); + +// Worker is used for the test below +Meteor.users.upsert("testWorker", {$set: {workerId: "testingWorker"}}); + +Tinytest.add("auth - testing HIT login doesn't require existing HIT", withCleanup(function(test) { + const asst = TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId: "testingHIT", + assignmentId: "testingAsst", + workerId: "testingWorker", test: true + }); - # Test database storage - record = Assignments.findOne - hitId: "testingHIT" + // Test database storage + const record = Assignments.findOne({ + hitId: "testingHIT", assignmentId: "testingAsst" + }); - test.isTrue(asst) - test.equal(asst, TurkServer.Assignment.getAssignment(record._id)) + test.isTrue(asst); + test.equal(asst, TurkServer.Assignment.getAssignment(record._id)); - test.isTrue(record) - test.equal(record.workerId, "testingWorker") - test.equal(record.batchId, authBatchId) + test.isTrue(record); + test.equal(record.workerId, "testingWorker"); + return test.equal(record.batchId, authBatchId); +}) +); diff --git a/tests/connection_tests.js b/tests/connection_tests.js index fea27a1..74734ee 100644 --- a/tests/connection_tests.js +++ b/tests/connection_tests.js @@ -1,268 +1,321 @@ -batchId = "connectionBatch" - -Batches.upsert { _id: batchId }, { _id: batchId } - -batch = TurkServer.Batch.getBatch(batchId) - -hitId = "connectionHitId" -assignmentId = "connectionAsstId" -workerId = "connectionWorkerId" - -userId = "connectionUserId" - -Meteor.users.upsert userId, $set: {workerId} - -asst = null - -instanceId = "connectionInstance" -instance = batch.createInstance() - -# Create an assignment. Should only be used at most once per test case. -createAssignment = -> - TurkServer.Assignment.createAssignment { - batchId - hitId - assignmentId - workerId - acceptTime: new Date() - status: "assigned" - } - -withCleanup = TestUtils.getCleanupWrapper - before: -> - after: -> - # Remove user from lobby - batch.lobby.removeAssignment(asst) - # Clear user group - Partitioner.clearUserGroup(userId) - # Clear any assignments we created - Assignments.remove {batchId} - # Unset user state - Meteor.users.update userId, - $unset: +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const batchId = "connectionBatch"; + +Batches.upsert({ _id: batchId }, { _id: batchId }); + +const batch = TurkServer.Batch.getBatch(batchId); + +const hitId = "connectionHitId"; +const assignmentId = "connectionAsstId"; +const workerId = "connectionWorkerId"; + +const userId = "connectionUserId"; + +Meteor.users.upsert(userId, {$set: {workerId}}); + +let asst = null; + +const instanceId = "connectionInstance"; +const instance = batch.createInstance(); + +// Create an assignment. Should only be used at most once per test case. +const createAssignment = () => TurkServer.Assignment.createAssignment({ + batchId, + hitId, + assignmentId, + workerId, + acceptTime: new Date(), + status: "assigned" +}); + +const withCleanup = TestUtils.getCleanupWrapper({ + before() {}, + after() { + // Remove user from lobby + batch.lobby.removeAssignment(asst); + // Clear user group + Partitioner.clearUserGroup(userId); + // Clear any assignments we created + Assignments.remove({batchId}); + // Unset user state + return Meteor.users.update(userId, { + $unset: { "turkserver.state": null - -Tinytest.add "connection - get existing assignment creates and preserves object", withCleanup (test) -> - asstId = Assignments.insert { - batchId - hitId - assignmentId - workerId - acceptTime: new Date() - status: "assigned" + } + } + ); } +}); + +Tinytest.add("connection - get existing assignment creates and preserves object", withCleanup(function(test) { + const asstId = Assignments.insert({ + batchId, + hitId, + assignmentId, + workerId, + acceptTime: new Date(), + status: "assigned" + }); - asst = TurkServer.Assignment.getAssignment asstId - asst2 = TurkServer.Assignment.getAssignment asstId + asst = TurkServer.Assignment.getAssignment(asstId); + const asst2 = TurkServer.Assignment.getAssignment(asstId); - test.equal asst2, asst + return test.equal(asst2, asst); +}) +); -Tinytest.add "connection - assignment object preserved upon creation", withCleanup (test) -> - asst = createAssignment() - asst2 = TurkServer.Assignment.getAssignment asst.asstId +Tinytest.add("connection - assignment object preserved upon creation", withCleanup(function(test) { + asst = createAssignment(); + const asst2 = TurkServer.Assignment.getAssignment(asst.asstId); - test.equal asst2, asst + return test.equal(asst2, asst); +}) +); -Tinytest.add "connection - get active user assignment", withCleanup (test) -> - asst = createAssignment() - asst2 = TurkServer.Assignment.getCurrentUserAssignment asst.userId +Tinytest.add("connection - get active user assignment", withCleanup(function(test) { + asst = createAssignment(); + const asst2 = TurkServer.Assignment.getCurrentUserAssignment(asst.userId); - test.equal asst2, asst + return test.equal(asst2, asst); +}) +); -Tinytest.add "connection - assignment removed from cache after return", withCleanup (test) -> - asst = createAssignment() +Tinytest.add("connection - assignment removed from cache after return", withCleanup(function(test) { + asst = createAssignment(); asst.setReturned(); - # Let cache cleanup do its thing + // Let cache cleanup do its thing TestUtils.sleep(200); - test.isUndefined TurkServer.Assignment.getCurrentUserAssignment(asst.userId) + return test.isUndefined(TurkServer.Assignment.getCurrentUserAssignment(asst.userId)); +}) +); -Tinytest.add "connection - assignment removed from cache after completion", withCleanup (test) -> - asst = createAssignment() +Tinytest.add("connection - assignment removed from cache after completion", withCleanup(function(test) { + asst = createAssignment(); asst.showExitSurvey(); asst.setCompleted({}); - # Let cache cleanup do its thing + // Let cache cleanup do its thing TestUtils.sleep(200); - test.isUndefined TurkServer.Assignment.getCurrentUserAssignment(asst.userId) + return test.isUndefined(TurkServer.Assignment.getCurrentUserAssignment(asst.userId)); +}) +); -Tinytest.add "connection - user added to lobby", withCleanup (test) -> - asst = createAssignment() - TestUtils.connCallbacks.sessionReconnect { userId } +Tinytest.add("connection - user added to lobby", withCleanup(function(test) { + asst = createAssignment(); + TestUtils.connCallbacks.sessionReconnect({ userId }); - lobbyUsers = batch.lobby.getAssignments() - user = Meteor.users.findOne(userId) + const lobbyUsers = batch.lobby.getAssignments(); + const user = Meteor.users.findOne(userId); - test.equal lobbyUsers.length, 1 - test.equal lobbyUsers[0], asst - test.equal lobbyUsers[0].userId, userId + test.equal(lobbyUsers.length, 1); + test.equal(lobbyUsers[0], asst); + test.equal(lobbyUsers[0].userId, userId); - test.equal user.turkserver.state, "lobby" + return test.equal(user.turkserver.state, "lobby"); +}) +); -Tinytest.add "connection - user disconnecting and reconnecting to lobby", withCleanup (test) -> - asst = createAssignment() +Tinytest.add("connection - user disconnecting and reconnecting to lobby", withCleanup(function(test) { + asst = createAssignment(); - TestUtils.connCallbacks.sessionReconnect { userId } + TestUtils.connCallbacks.sessionReconnect({ userId }); - TestUtils.connCallbacks.sessionDisconnect { userId } + TestUtils.connCallbacks.sessionDisconnect({ userId }); - lobbyUsers = batch.lobby.getAssignments() - user = Meteor.users.findOne(userId) + let lobbyUsers = batch.lobby.getAssignments(); + let user = Meteor.users.findOne(userId); - test.equal lobbyUsers.length, 0 - test.equal user.turkserver.state, "lobby" + test.equal(lobbyUsers.length, 0); + test.equal(user.turkserver.state, "lobby"); - TestUtils.connCallbacks.sessionReconnect { userId } + TestUtils.connCallbacks.sessionReconnect({ userId }); - lobbyUsers = batch.lobby.getAssignments() - user = Meteor.users.findOne(userId) + lobbyUsers = batch.lobby.getAssignments(); + user = Meteor.users.findOne(userId); - test.equal lobbyUsers.length, 1 - test.equal lobbyUsers[0], asst - test.equal lobbyUsers[0].userId, userId - test.equal user.turkserver.state, "lobby" + test.equal(lobbyUsers.length, 1); + test.equal(lobbyUsers[0], asst); + test.equal(lobbyUsers[0].userId, userId); + return test.equal(user.turkserver.state, "lobby"); +}) +); -Tinytest.add "connection - user sent to exit survey", withCleanup (test) -> - asst = createAssignment() - asst.showExitSurvey() +Tinytest.add("connection - user sent to exit survey", withCleanup(function(test) { + asst = createAssignment(); + asst.showExitSurvey(); - user = Meteor.users.findOne(userId) + const user = Meteor.users.findOne(userId); - test.equal user.turkserver.state, "exitsurvey" + return test.equal(user.turkserver.state, "exitsurvey"); +}) +); -Tinytest.add "connection - user submitting HIT", withCleanup (test) -> - asst = createAssignment() +Tinytest.add("connection - user submitting HIT", withCleanup(function(test) { + asst = createAssignment(); - Meteor.users.update userId, - $set: + Meteor.users.update(userId, { + $set: { "turkserver.state": "exitsurvey" + } + } + ); - exitData = {foo: "bar"} + const exitData = {foo: "bar"}; - asst.setCompleted( exitData ) + asst.setCompleted( exitData ); - user = Meteor.users.findOne(userId) - asstData = Assignments.findOne(asst.asstId) + const user = Meteor.users.findOne(userId); + const asstData = Assignments.findOne(asst.asstId); - test.isFalse user.turkserver?.state + test.isFalse(user.turkserver != null ? user.turkserver.state : undefined); - test.isTrue asst.isCompleted() - test.equal asstData.status, "completed" - test.instanceOf asstData.submitTime, Date - test.equal asstData.exitdata, exitData + test.isTrue(asst.isCompleted()); + test.equal(asstData.status, "completed"); + test.instanceOf(asstData.submitTime, Date); + return test.equal(asstData.exitdata, exitData); +}) +); -Tinytest.add "connection - improper submission of HIT", withCleanup (test) -> - asst = createAssignment() +Tinytest.add("connection - improper submission of HIT", withCleanup(function(test) { + asst = createAssignment(); - test.throws -> - asst.setCompleted {} - , (e) -> e.error is 403 and e.reason is ErrMsg.stateErr + return test.throws(() => asst.setCompleted({}) + , e => (e.error === 403) && (e.reason === ErrMsg.stateErr)); +}) +); -Tinytest.add "connection - set assignment as returned", withCleanup (test) -> - asst = createAssignment() - TestUtils.connCallbacks.sessionReconnect { userId } +Tinytest.add("connection - set assignment as returned", withCleanup(function(test) { + asst = createAssignment(); + TestUtils.connCallbacks.sessionReconnect({ userId }); - asst.setReturned() + asst.setReturned(); - user = Meteor.users.findOne(userId) - asstData = Assignments.findOne(asst.asstId) + const user = Meteor.users.findOne(userId); + const asstData = Assignments.findOne(asst.asstId); - test.equal asstData.status, "returned" - test.isFalse user.turkserver?.state + test.equal(asstData.status, "returned"); + return test.isFalse(user.turkserver != null ? user.turkserver.state : undefined); +}) +); -Tinytest.add "connection - user resuming into instance", withCleanup (test) -> - asst = createAssignment() - instance.addAssignment(asst) - TestUtils.connCallbacks.sessionReconnect { userId } +Tinytest.add("connection - user resuming into instance", withCleanup(function(test) { + asst = createAssignment(); + instance.addAssignment(asst); + TestUtils.connCallbacks.sessionReconnect({ userId }); - user = Meteor.users.findOne(userId) + const user = Meteor.users.findOne(userId); - test.equal batch.lobby.getAssignments().length, 0 - test.equal user.turkserver.state, "experiment" + test.equal(batch.lobby.getAssignments().length, 0); + return test.equal(user.turkserver.state, "experiment"); +}) +); -Tinytest.add "connection - user resuming into exit survey", withCleanup (test) -> - asst = createAssignment() - Meteor.users.update userId, - $set: +Tinytest.add("connection - user resuming into exit survey", withCleanup(function(test) { + asst = createAssignment(); + Meteor.users.update(userId, { + $set: { "turkserver.state": "exitsurvey" + } + } + ); - TestUtils.connCallbacks.sessionReconnect { userId } + TestUtils.connCallbacks.sessionReconnect({ userId }); - user = Meteor.users.findOne(userId) + const user = Meteor.users.findOne(userId); - test.equal batch.lobby.getAssignments().length, 0 - test.equal user.turkserver.state, "exitsurvey" + test.equal(batch.lobby.getAssignments().length, 0); + return test.equal(user.turkserver.state, "exitsurvey"); +}) +); -Tinytest.add "connection - set payment amount", withCleanup (test) -> - asst = createAssignment() - test.isFalse asst.getPayment() +Tinytest.add("connection - set payment amount", withCleanup(function(test) { + asst = createAssignment(); + test.isFalse(asst.getPayment()); - amount = 1.00 + const amount = 1.00; - asst.setPayment(amount) - test.equal asst.getPayment(), amount + asst.setPayment(amount); + test.equal(asst.getPayment(), amount); - asst.addPayment(1.50) - test.equal asst.getPayment(), 2.50 + asst.addPayment(1.50); + return test.equal(asst.getPayment(), 2.50); +}) +); -Tinytest.add "connection - increment null payment amount", withCleanup (test) -> - asst = createAssignment() - test.isFalse asst.getPayment() +Tinytest.add("connection - increment null payment amount", withCleanup(function(test) { + asst = createAssignment(); + test.isFalse(asst.getPayment()); - amount = 1.00 - asst.addPayment(amount) - test.equal asst.getPayment(), amount + const amount = 1.00; + asst.addPayment(amount); + return test.equal(asst.getPayment(), amount); +}) +); -Tinytest.add "connection - pay worker bonus", withCleanup (test) -> - asst = createAssignment() +Tinytest.add("connection - pay worker bonus", withCleanup(function(test) { + asst = createAssignment(); - test.isFalse(asst._data().bonusPaid) + test.isFalse(asst._data().bonusPaid); - amount = 10.00 - asst.setPayment(amount) + const amount = 10.00; + asst.setPayment(amount); - message = "Thanks for your work!" - asst.payBonus(message) + const message = "Thanks for your work!"; + asst.payBonus(message); - test.equal TestUtils.mturkAPI.op, "GrantBonus" - test.equal TestUtils.mturkAPI.params.WorkerId, asst.workerId - test.equal TestUtils.mturkAPI.params.AssignmentId, asst.assignmentId - test.equal TestUtils.mturkAPI.params.BonusAmount.Amount, amount - test.equal TestUtils.mturkAPI.params.BonusAmount.CurrencyCode, "USD" - test.equal TestUtils.mturkAPI.params.Reason, message + test.equal(TestUtils.mturkAPI.op, "GrantBonus"); + test.equal(TestUtils.mturkAPI.params.WorkerId, asst.workerId); + test.equal(TestUtils.mturkAPI.params.AssignmentId, asst.assignmentId); + test.equal(TestUtils.mturkAPI.params.BonusAmount.Amount, amount); + test.equal(TestUtils.mturkAPI.params.BonusAmount.CurrencyCode, "USD"); + test.equal(TestUtils.mturkAPI.params.Reason, message); - asstData = asst._data() - test.equal asstData.bonusPayment, amount - test.equal asstData.bonusMessage, message - test.instanceOf asstData.bonusPaid, Date + const asstData = asst._data(); + test.equal(asstData.bonusPayment, amount); + test.equal(asstData.bonusMessage, message); + return test.instanceOf(asstData.bonusPaid, Date); +}) +); -Tinytest.add "connection - throw on set/inc payment when bonus paid", withCleanup (test) -> - asst = createAssignment() +Tinytest.add("connection - throw on set/inc payment when bonus paid", withCleanup(function(test) { + asst = createAssignment(); - Assignments.update asst.asstId, - $set: - bonusPayment: 0.01 - bonusPaid: new Date + Assignments.update(asst.asstId, { + $set: { + bonusPayment: 0.01, + bonusPaid: new Date, bonusMessage: "blah" + } + } + ); - amount = 1.00 + const amount = 1.00; - test.throws -> asst.setPayment(amount) - test.equal asst.getPayment(), 0.01 + test.throws(() => asst.setPayment(amount)); + test.equal(asst.getPayment(), 0.01); - test.throws -> asst.addPayment(1.50) - test.equal asst.getPayment(), 0.01 + test.throws(() => asst.addPayment(1.50)); + return test.equal(asst.getPayment(), 0.01); +}) +); -Tinytest.add "connection - throw on double payments", withCleanup (test) -> - asst = createAssignment() +Tinytest.add("connection - throw on double payments", withCleanup(function(test) { + asst = createAssignment(); - amount = 10.00 - asst.setPayment(amount) + const amount = 10.00; + asst.setPayment(amount); - message = "Thanks for your work!" - asst.payBonus(message) + const message = "Thanks for your work!"; + asst.payBonus(message); - test.throws -> - asst.payBonus(message) + return test.throws(() => asst.payBonus(message)); +}) +); diff --git a/tests/experiment_client_tests.js b/tests/experiment_client_tests.js index 14491b1..3109ce6 100644 --- a/tests/experiment_client_tests.js +++ b/tests/experiment_client_tests.js @@ -1,290 +1,343 @@ -if Meteor.isServer - - # Set up a treatment for testing - TurkServer.ensureTreatmentExists - name: "expWorldTreatment" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +if (Meteor.isServer) { + + // Set up a treatment for testing + TurkServer.ensureTreatmentExists({ + name: "expWorldTreatment", fooProperty: "bar" + }); - TurkServer.ensureTreatmentExists - name: "expUserTreatment" + TurkServer.ensureTreatmentExists({ + name: "expUserTreatment", foo2: "baz" - - # Some functions to make sure things are set up for the client login - Accounts.validateLoginAttempt (info) -> - return unless info.allowed # Don't handle if login is being rejected - userId = info.user._id - - Partitioner.clearUserGroup(userId) # Remove any previous user group - return true - - Accounts.onLogin (info) -> - userId = info.user._id - - # Worker and assignment should have already been created at this point - asst = TurkServer.Assignment.getCurrentUserAssignment(userId) - - # Reset assignment for this worker - Assignments.upsert asst.asstId, - $unset: instances: null, - $unset: treatments: null - - asst.getBatch().createInstance(["expWorldTreatment"]).addAssignment(asst) - - asst.addTreatment("expUserTreatment") - - Meteor._debug "Remote client logged in" - - Meteor.methods - getAssignmentData: -> - userId = Meteor.userId() - throw new Meteor.Error(500, "Not logged in") unless userId - workerId = Meteor.users.findOne(userId).workerId - return Assignments.findOne({workerId, status: "assigned"}) - - setAssignmentPayment: (amount) -> + }); + + // Some functions to make sure things are set up for the client login + Accounts.validateLoginAttempt(function(info) { + if (!info.allowed) { return; } // Don't handle if login is being rejected + const userId = info.user._id; + + Partitioner.clearUserGroup(userId); // Remove any previous user group + return true; + }); + + Accounts.onLogin(function(info) { + const userId = info.user._id; + + // Worker and assignment should have already been created at this point + const asst = TurkServer.Assignment.getCurrentUserAssignment(userId); + + // Reset assignment for this worker + Assignments.upsert(asst.asstId, { + $unset: { instances: null, + $unset: { treatments: null + } + } + } + ); + + asst.getBatch().createInstance(["expWorldTreatment"]).addAssignment(asst); + + asst.addTreatment("expUserTreatment"); + + return Meteor._debug("Remote client logged in"); + }); + + Meteor.methods({ + getAssignmentData() { + const userId = Meteor.userId(); + if (!userId) { throw new Meteor.Error(500, "Not logged in"); } + const { + workerId + } = Meteor.users.findOne(userId); + return Assignments.findOne({workerId, status: "assigned"}); + }, + + setAssignmentPayment(amount) { TurkServer.Assignment.currentAssignment().setPayment(amount); - return + }, - setAssignmentInstanceData: (arr) -> - selector = - workerId: Meteor.user().workerId + setAssignmentInstanceData(arr) { + const selector = { + workerId: Meteor.user().workerId, status: "assigned" + }; - unless Assignments.update(selector, $set: {instances: arr}) > 0 - throw new Meteor.Error(400, "Could not find assignment to update") - return - - endAssignmentInstance: (returnToLobby) -> - TurkServer.Instance.currentInstance().teardown(returnToLobby) - return - - getServerTreatment: -> - return TurkServer.treatment() - -if Meteor.isClient - tol = 20 # range in ms that we can be off in adjacent cols - big_tol = 500 # max range we tolerate in a round trip to the server (async method) - - expectedTreatment = { - fooProperty: "bar" # world - foo2: "baz" # user - } - - checkTreatments = (test, obj) -> - for k, v of expectedTreatment - test.equal obj[k], v, "for key #{k} actual value #{obj[k]} doesn't match expected value #{v}" - - Tinytest.addAsync "experiment - client - login and creation of assignment metadata", (test, next) -> - InsecureLogin.ready -> - test.isTrue Meteor.userId() - next() - - Tinytest.addAsync "experiment - client - IP address saved", (test, next) -> - returned = false - Meteor.call "getAssignmentData", (err, res) -> - returned = true - test.isFalse err - console.log "Got assignment data", JSON.stringify(res) - - test.isTrue res?.ipAddr?[0] - test.equal res?.userAgent?[0], navigator.userAgent unless Package['test-in-console']? - - next() - - fail = -> - test.fail() - next() - - simplePoll (-> returned), (->), fail, 2000 - - Tinytest.addAsync "experiment - client - received experiment and treatment", (test, next) -> - treatment = null - - verify = -> - console.info "Got treatment ", treatment - - test.isTrue Experiments.findOne() - test.isTrue treatment - - # Test world-level treatment - # No _id or name sent over the wire - worldTreatment = TurkServer.treatment("expWorldTreatment") - test.isFalse worldTreatment._id - test.isTrue worldTreatment.name - test.equal worldTreatment.fooProperty, "bar" - - # Test user-level treatment - userTreatment = TurkServer.treatment("expUserTreatment") - test.isFalse userTreatment._id - test.isTrue userTreatment.name - test.equal userTreatment.foo2, "baz" - - checkTreatments(test, TurkServer.treatment()) - - next() - - fail = -> - test.fail() - next() - - # Poll until both treatments arrives - simplePoll (-> - treatment = TurkServer.treatment() - return true if treatment.treatments.length - ), verify, fail, 2000 - - Tinytest.addAsync "experiment - assignment - test treatments on server", (test, next) -> - # Even though this is a "client" test, it is testing a server function - # because assignment treatments are different on the client and server - Meteor.call "getServerTreatment", (err, res) -> - test.fail() if err? - - checkTreatments(test, res) - next() - - Tinytest.addAsync "experiment - client - current payment variable", (test, next) -> - amount = 0.42 - - Meteor.call "setAssignmentPayment", amount, (err, res) -> - test.equal TurkServer.currentPayment(), amount - next() - - Tinytest.addAsync "experiment - assignment - assignment metadata and local time vars", (test, next) -> - asstData = null - - verify = -> - console.info "Got assignmentData ", asstData - - test.isTrue asstData.instances - test.isTrue asstData.instances[0] - - test.isTrue TurkServer.Timers.joinedTime() > 0 - test.equal TurkServer.Timers.idleTime(), 0 - test.equal TurkServer.Timers.disconnectedTime(), 0 - - test.isTrue Math.abs(TurkServer.Timers.activeTime() - TurkServer.Timers.joinedTime()) < 10 - - next() - - fail = -> - test.fail() - next() - - # Poll until treatment data arrives - simplePoll (-> - asstData = Assignments.findOne() - return true if asstData? - ), verify, fail, 2000 + if (!(Assignments.update(selector, {$set: {instances: arr}}) > 0)) { + throw new Meteor.Error(400, "Could not find assignment to update"); + } + }, + + endAssignmentInstance(returnToLobby) { + TurkServer.Instance.currentInstance().teardown(returnToLobby); + }, + + getServerTreatment() { + return TurkServer.treatment(); + } + }); +} + +if (Meteor.isClient) { + const tol = 20; // range in ms that we can be off in adjacent cols + const big_tol = 500; // max range we tolerate in a round trip to the server (async method) + + const expectedTreatment = { + fooProperty: "bar", // world + foo2: "baz" // user + }; + + const checkTreatments = (test, obj) => (() => { + const result = []; + for (let k in expectedTreatment) { + const v = expectedTreatment[k]; + result.push(test.equal(obj[k], v, `for key ${k} actual value ${obj[k]} doesn't match expected value ${v}`)); + } + return result; + })(); + + Tinytest.addAsync("experiment - client - login and creation of assignment metadata", (test, next) => InsecureLogin.ready(function() { + test.isTrue(Meteor.userId()); + return next(); + })); + + Tinytest.addAsync("experiment - client - IP address saved", function(test, next) { + let returned = false; + Meteor.call("getAssignmentData", function(err, res) { + returned = true; + test.isFalse(err); + console.log("Got assignment data", JSON.stringify(res)); + + test.isTrue(__guard__(res != null ? res.ipAddr : undefined, x => x[0])); + if (Package['test-in-console'] == null) { test.equal(__guard__(res != null ? res.userAgent : undefined, x1 => x1[0]), navigator.userAgent); } + + return next(); + }); + + const fail = function() { + test.fail(); + return next(); + }; + + return simplePoll((() => returned), (function() {}), fail, 2000); + }); + + Tinytest.addAsync("experiment - client - received experiment and treatment", function(test, next) { + let treatment = null; + + const verify = function() { + console.info("Got treatment ", treatment); + + test.isTrue(Experiments.findOne()); + test.isTrue(treatment); + + // Test world-level treatment + // No _id or name sent over the wire + const worldTreatment = TurkServer.treatment("expWorldTreatment"); + test.isFalse(worldTreatment._id); + test.isTrue(worldTreatment.name); + test.equal(worldTreatment.fooProperty, "bar"); + + // Test user-level treatment + const userTreatment = TurkServer.treatment("expUserTreatment"); + test.isFalse(userTreatment._id); + test.isTrue(userTreatment.name); + test.equal(userTreatment.foo2, "baz"); + + checkTreatments(test, TurkServer.treatment()); + + return next(); + }; + + const fail = function() { + test.fail(); + return next(); + }; + + // Poll until both treatments arrives + return simplePoll((function() { + treatment = TurkServer.treatment(); + if (treatment.treatments.length) { return true; } + }), verify, fail, 2000); + }); + + Tinytest.addAsync("experiment - assignment - test treatments on server", (test, next) => // Even though this is a "client" test, it is testing a server function + // because assignment treatments are different on the client and server + Meteor.call("getServerTreatment", function(err, res) { + if (err != null) { test.fail(); } + + checkTreatments(test, res); + return next(); + })); + + Tinytest.addAsync("experiment - client - current payment variable", function(test, next) { + const amount = 0.42; + + return Meteor.call("setAssignmentPayment", amount, function(err, res) { + test.equal(TurkServer.currentPayment(), amount); + return next(); + }); + }); + + Tinytest.addAsync("experiment - assignment - assignment metadata and local time vars", function(test, next) { + let asstData = null; + + const verify = function() { + console.info("Got assignmentData ", asstData); + + test.isTrue(asstData.instances); + test.isTrue(asstData.instances[0]); + + test.isTrue(TurkServer.Timers.joinedTime() > 0); + test.equal(TurkServer.Timers.idleTime(), 0); + test.equal(TurkServer.Timers.disconnectedTime(), 0); + + test.isTrue(Math.abs(TurkServer.Timers.activeTime() - TurkServer.Timers.joinedTime()) < 10); + + return next(); + }; + + const fail = function() { + test.fail(); + return next(); + }; + + // Poll until treatment data arrives + return simplePoll((function() { + asstData = Assignments.findOne(); + if (asstData != null) { return true; } + }), verify, fail, 2000); + }); - Tinytest.addAsync "experiment - assignment - no time fields", (test, next) -> - fields = [ + Tinytest.addAsync("experiment - assignment - no time fields", function(test, next) { + const fields = [ { - id: TurkServer.group() + id: TurkServer.group(), joinTime: new Date(TimeSync.serverTime()) } - ] + ]; - Meteor.call "setAssignmentInstanceData", fields, (err, res) -> - test.isFalse err - Deps.flush() # Help out the emboxed value thingies + return Meteor.call("setAssignmentInstanceData", fields, function(err, res) { + test.isFalse(err); + Deps.flush(); // Help out the emboxed value thingies - test.equal TurkServer.Timers.idleTime(), 0 - test.equal TurkServer.Timers.disconnectedTime(), 0 + test.equal(TurkServer.Timers.idleTime(), 0); + test.equal(TurkServer.Timers.disconnectedTime(), 0); - joinedTime = TurkServer.Timers.joinedTime() - activeTime = TurkServer.Timers.activeTime() + const joinedTime = TurkServer.Timers.joinedTime(); + const activeTime = TurkServer.Timers.activeTime(); - test.isTrue joinedTime >= 0 - test.isTrue joinedTime < big_tol + test.isTrue(joinedTime >= 0); + test.isTrue(joinedTime < big_tol); - test.isTrue activeTime >= 0 + test.isTrue(activeTime >= 0); - test.equal UI._globalHelpers.tsIdleTime(), "0:00:00" - test.equal UI._globalHelpers.tsDisconnectedTime(), "0:00:00" + test.equal(UI._globalHelpers.tsIdleTime(), "0:00:00"); + test.equal(UI._globalHelpers.tsDisconnectedTime(), "0:00:00"); - next() + return next(); + }); + }); - Tinytest.addAsync "experiment - assignment - joined time computation", (test, next) -> - fields = [ + Tinytest.addAsync("experiment - assignment - joined time computation", function(test, next) { + const fields = [ { - id: TurkServer.group() - joinTime: new Date(TimeSync.serverTime() - 3000) - idleTime: 1000 + id: TurkServer.group(), + joinTime: new Date(TimeSync.serverTime() - 3000), + idleTime: 1000, disconnectedTime: 2000 } - ] + ]; - Meteor.call "setAssignmentInstanceData", fields, (err, res) -> - test.isFalse err - Deps.flush() # Help out the emboxed value thingies + return Meteor.call("setAssignmentInstanceData", fields, function(err, res) { + test.isFalse(err); + Deps.flush(); // Help out the emboxed value thingies - test.equal TurkServer.Timers.idleTime(), 1000 - test.equal TurkServer.Timers.disconnectedTime(), 2000 + test.equal(TurkServer.Timers.idleTime(), 1000); + test.equal(TurkServer.Timers.disconnectedTime(), 2000); - joinedTime = TurkServer.Timers.joinedTime() - activeTime = TurkServer.Timers.activeTime() + const joinedTime = TurkServer.Timers.joinedTime(); + const activeTime = TurkServer.Timers.activeTime(); - test.isTrue joinedTime >= 3000 - test.isTrue joinedTime < 3000 + big_tol - test.isTrue Math.abs(activeTime + 3000 - joinedTime) < tol - test.isTrue activeTime >= 0 + test.isTrue(joinedTime >= 3000); + test.isTrue(joinedTime < (3000 + big_tol)); + test.isTrue(Math.abs((activeTime + 3000) - joinedTime) < tol); + test.isTrue(activeTime >= 0); - test.equal UI._globalHelpers.tsIdleTime(), "0:00:01" - test.equal UI._globalHelpers.tsDisconnectedTime(), "0:00:02" + test.equal(UI._globalHelpers.tsIdleTime(), "0:00:01"); + test.equal(UI._globalHelpers.tsDisconnectedTime(), "0:00:02"); - next() + return next(); + }); + }); - Tinytest.addAsync "experiment - instance - instance ended state", (test, next) -> - # In experiment. not ended - test.isTrue TurkServer.inExperiment() - test.isFalse TurkServer.instanceEnded() + Tinytest.addAsync("experiment - instance - instance ended state", function(test, next) { + // In experiment. not ended + test.isTrue(TurkServer.inExperiment()); + test.isFalse(TurkServer.instanceEnded()); - Meteor.call "endAssignmentInstance", false, (err, res) -> - test.isTrue TurkServer.inExperiment() - test.isTrue TurkServer.instanceEnded() + return Meteor.call("endAssignmentInstance", false, function(err, res) { + test.isTrue(TurkServer.inExperiment()); + test.isTrue(TurkServer.instanceEnded()); - next() + return next(); + }); + }); - ### + /* Next test edits instance fields, so client APIs may break state - ### + */ - Tinytest.addAsync "experiment - instance - client selects correct instance of - multiple", (test, next) -> - fields = [ + Tinytest.addAsync(`experiment - instance - client selects correct instance of \ +multiple`, function(test, next) { + const fields = [ { - id: Random.id() - joinTime: new Date(TimeSync.serverTime() - 3600*1000) - idleTime: 3000 + id: Random.id(), + joinTime: new Date(TimeSync.serverTime() - (3600*1000)), + idleTime: 3000, disconnectedTime: 5000 }, { - id: TurkServer.group() - joinTime: new Date(TimeSync.serverTime() - 5000) - idleTime: 1000 + id: TurkServer.group(), + joinTime: new Date(TimeSync.serverTime() - 5000), + idleTime: 1000, disconnectedTime: 2000 } - ] + ]; + + return Meteor.call("setAssignmentInstanceData", fields, function(err, res) { + test.isFalse(err); + Deps.flush(); // Help out the emboxed value thingies - Meteor.call "setAssignmentInstanceData", fields, (err, res) -> - test.isFalse err - Deps.flush() # Help out the emboxed value thingies + test.equal(TurkServer.Timers.idleTime(), 1000); + test.equal(TurkServer.Timers.disconnectedTime(), 2000); - test.equal TurkServer.Timers.idleTime(), 1000 - test.equal TurkServer.Timers.disconnectedTime(), 2000 + const joinedTime = TurkServer.Timers.joinedTime(); + const activeTime = TurkServer.Timers.activeTime(); - joinedTime = TurkServer.Timers.joinedTime() - activeTime = TurkServer.Timers.activeTime() + test.isTrue(joinedTime >= 5000); + test.isTrue(joinedTime < (5000 + big_tol)); - test.isTrue joinedTime >= 5000 - test.isTrue joinedTime < 5000 + big_tol + test.isTrue(Math.abs((activeTime + 3000) - joinedTime) < tol); + test.isTrue(activeTime >= 0); // Should not be negative - test.isTrue Math.abs(activeTime + 3000 - joinedTime) < tol - test.isTrue activeTime >= 0 # Should not be negative + test.equal(UI._globalHelpers.tsIdleTime(), "0:00:01"); + test.equal(UI._globalHelpers.tsDisconnectedTime(), "0:00:02"); - test.equal UI._globalHelpers.tsIdleTime(), "0:00:01" - test.equal UI._globalHelpers.tsDisconnectedTime(), "0:00:02" + return next(); + }); + }); +} - next() + // TODO: add a test for submitting HIT and verify that resume token is removed - # TODO: add a test for submitting HIT and verify that resume token is removed +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/tests/experiment_tests.js b/tests/experiment_tests.js index 1fdfd25..8379cc4 100644 --- a/tests/experiment_tests.js +++ b/tests/experiment_tests.js @@ -1,563 +1,617 @@ -Doobie = new Mongo.Collection("experiment_test") - -Partitioner.partitionCollection Doobie - -setupContext = undefined -reconnectContext = undefined -disconnectContext = undefined -idleContext = undefined -activeContext = undefined - -TurkServer.initialize -> setupContext = @ - -TurkServer.initialize -> - Doobie.insert - foo: "bar" - - # Test deferred insert - Meteor.defer -> - Doobie.insert - bar: "baz" - -TurkServer.onConnect -> reconnectContext = this -TurkServer.onDisconnect -> disconnectContext = this -TurkServer.onIdle -> idleContext = this -TurkServer.onActive -> activeContext = this - -# Ensure batch exists -batchId = "expBatch" -Batches.upsert { _id: batchId }, { _id: batchId } - -# Set up a treatment for testing -TurkServer.ensureTreatmentExists - name: "fooTreatment" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS204: Change includes calls to have a more natural evaluation order + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Doobie = new Mongo.Collection("experiment_test"); + +Partitioner.partitionCollection(Doobie); + +let setupContext = undefined; +let reconnectContext = undefined; +let disconnectContext = undefined; +let idleContext = undefined; +let activeContext = undefined; + +TurkServer.initialize(function() { return setupContext = this; }); + +TurkServer.initialize(function() { + Doobie.insert({ + foo: "bar"}); + + // Test deferred insert + return Meteor.defer(() => Doobie.insert({ + bar: "baz"})); +}); + +TurkServer.onConnect(function() { return reconnectContext = this; }); +TurkServer.onDisconnect(function() { return disconnectContext = this; }); +TurkServer.onIdle(function() { return idleContext = this; }); +TurkServer.onActive(function() { return activeContext = this; }); + +// Ensure batch exists +const batchId = "expBatch"; +Batches.upsert({ _id: batchId }, { _id: batchId }); + +// Set up a treatment for testing +TurkServer.ensureTreatmentExists({ + name: "fooTreatment", fooProperty: "bar" +}); -batch = TurkServer.Batch.getBatch("expBatch") +const batch = TurkServer.Batch.getBatch("expBatch"); -createAssignment = -> - workerId = Random.id() - userId = Accounts.insertUserDoc {}, { +const createAssignment = function() { + const workerId = Random.id(); + const userId = Accounts.insertUserDoc({}, { + workerId, + turkserver: {state: "lobby"} // Created user goes in lobby + }); + return TurkServer.Assignment.createAssignment({ + batchId: "expBatch", + hitId: Random.id(), + assignmentId: Random.id(), workerId, - turkserver: {state: "lobby"} # Created user goes in lobby - } - return TurkServer.Assignment.createAssignment - batchId: "expBatch" - hitId: Random.id() - assignmentId: Random.id() - workerId: workerId - acceptTime: new Date() + acceptTime: new Date(), status: "assigned" + }); +}; + +const withCleanup = TestUtils.getCleanupWrapper({ + before() { + // Clear any callback records + setupContext = undefined; + reconnectContext = undefined; + disconnectContext = undefined; + idleContext = undefined; + return activeContext = undefined; + }, + + after() { + // Delete assignments + Assignments.remove({ batchId: "expBatch" }); + // Delete generated log entries + Experiments.find({ batchId: "expBatch" }).forEach(exp => Logs.remove({_groupId: exp._id})); + // Delete experiments + Experiments.remove({ batchId: "expBatch" }); + + // Clear contents of partitioned collection + return Doobie.direct.remove({}); + }}); + +const lastLog = groupId => Logs.findOne({_groupId: groupId}, {sort: {_timestamp: -1}}); + +Tinytest.add("experiment - batch - creation and retrieval", withCleanup(function(test) { + // First get should create, second get should return same object + // TODO: this test will only run as intended on the first try + const batch2 = TurkServer.Batch.getBatch("expBatch"); + + return test.equal(batch2, batch); +}) +); + +Tinytest.add(`experiment - assignment - currentAssignment in standalone \ +server code returns null`, test => test.equal(TurkServer.Assignment.currentAssignment(), null)); + +Tinytest.add("experiment - instance - throws error if doesn't exist", withCleanup(test => test.throws(() => TurkServer.Instance.getInstance("yabbadabbadoober"))) +); + +Tinytest.add("experiment - instance - create", withCleanup(function(test) { + const treatments = [ "fooTreatment" ]; + + // Create a new id to test specified ID + const serverInstanceId = Random.id(); + + const instance = batch.createInstance(treatments, {_id: serverInstanceId}); + test.equal(instance.groupId, serverInstanceId); + test.instanceOf(instance, TurkServer.Instance); + + // Batch and treatments recorded - no start time until someone joins + const instanceData = Experiments.findOne(serverInstanceId); + test.equal(instanceData.batchId, "expBatch"); + test.equal(instanceData.treatments, treatments); + + test.isFalse(instanceData.startTime); + + // Test that create meta event was recorded in log + const logEntry = lastLog(serverInstanceId); + test.isTrue(logEntry); + test.equal(logEntry != null ? logEntry._meta : undefined, "created"); + + // Getting the instance again should get the same one + const inst2 = TurkServer.Instance.getInstance(serverInstanceId); + return test.equal(inst2, instance); +}) +); + +Tinytest.add("experiment - instance - setup context", withCleanup(function(test) { + const treatments = [ "fooTreatment" ]; + const instance = batch.createInstance(treatments); + TestUtils.sleep(10); // Enforce different log timestamp + instance.setup(); + + test.isTrue(setupContext); + const treatment = setupContext != null ? setupContext.instance.treatment() : undefined; + + test.equal(instance.batch(), TurkServer.Batch.getBatch("expBatch")); + + test.isTrue(treatment); + test.isTrue(Array.from(treatment.treatments).includes("fooTreatment"), + test.equal(treatment.fooProperty, "bar")); + test.equal(setupContext != null ? setupContext.instance.groupId : undefined, instance.groupId); + + // Check that the init _meta event was logged with treatment info + const logEntry = lastLog(instance.groupId); + test.isTrue(logEntry); + test.equal(logEntry != null ? logEntry._meta : undefined, "initialized"); + test.equal(logEntry != null ? logEntry.treatmentData : undefined, treatment); + test.equal(logEntry != null ? logEntry.treatmentData.treatments : undefined, treatments); + return test.equal(logEntry != null ? logEntry.treatmentData.fooProperty : undefined, "bar"); +}) +); + +Tinytest.add("experiment - instance - teardown and log", withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); + TestUtils.sleep(10); // Enforce different log timestamp + instance.teardown(); + + const logEntry = lastLog(instance.groupId); + test.isTrue(logEntry); + test.equal(logEntry != null ? logEntry._meta : undefined, "teardown"); + + const instanceData = Experiments.findOne(instance.groupId); + return test.instanceOf(instanceData.endTime, Date); +}) +); -withCleanup = TestUtils.getCleanupWrapper - before: -> - # Clear any callback records - setupContext = undefined - reconnectContext = undefined - disconnectContext = undefined - idleContext = undefined - activeContext = undefined - - after: -> - # Delete assignments - Assignments.remove({ batchId: "expBatch" }) - # Delete generated log entries - Experiments.find({ batchId: "expBatch" }).forEach (exp) -> - Logs.remove({_groupId: exp._id}) - # Delete experiments - Experiments.remove({ batchId: "expBatch" }) - - # Clear contents of partitioned collection - Doobie.direct.remove {} - -lastLog = (groupId) -> Logs.findOne({_groupId: groupId}, {sort: _timestamp: -1}) - -Tinytest.add "experiment - batch - creation and retrieval", withCleanup (test) -> - # First get should create, second get should return same object - # TODO: this test will only run as intended on the first try - batch2 = TurkServer.Batch.getBatch("expBatch") - - test.equal batch2, batch - -Tinytest.add "experiment - assignment - currentAssignment in standalone - server code returns null", (test) -> - test.equal TurkServer.Assignment.currentAssignment(), null - -Tinytest.add "experiment - instance - throws error if doesn't exist", withCleanup (test) -> - test.throws -> - TurkServer.Instance.getInstance("yabbadabbadoober") - -Tinytest.add "experiment - instance - create", withCleanup (test) -> - treatments = [ "fooTreatment" ] - - # Create a new id to test specified ID - serverInstanceId = Random.id() - - instance = batch.createInstance(treatments, {_id: serverInstanceId}) - test.equal(instance.groupId, serverInstanceId) - test.instanceOf(instance, TurkServer.Instance) - - # Batch and treatments recorded - no start time until someone joins - instanceData = Experiments.findOne(serverInstanceId) - test.equal instanceData.batchId, "expBatch" - test.equal instanceData.treatments, treatments - - test.isFalse instanceData.startTime - - # Test that create meta event was recorded in log - logEntry = lastLog(serverInstanceId) - test.isTrue logEntry - test.equal logEntry?._meta, "created" - - # Getting the instance again should get the same one - inst2 = TurkServer.Instance.getInstance(serverInstanceId) - test.equal inst2, instance - -Tinytest.add "experiment - instance - setup context", withCleanup (test) -> - treatments = [ "fooTreatment" ] - instance = batch.createInstance(treatments) - TestUtils.sleep(10) # Enforce different log timestamp - instance.setup() - - test.isTrue setupContext - treatment = setupContext?.instance.treatment() - - test.equal instance.batch(), TurkServer.Batch.getBatch("expBatch") - - test.isTrue treatment - test.isTrue "fooTreatment" in treatment.treatments, - test.equal treatment.fooProperty, "bar" - test.equal setupContext?.instance.groupId, instance.groupId - - # Check that the init _meta event was logged with treatment info - logEntry = lastLog(instance.groupId) - test.isTrue logEntry - test.equal logEntry?._meta, "initialized" - test.equal logEntry?.treatmentData, treatment - test.equal logEntry?.treatmentData.treatments, treatments - test.equal logEntry?.treatmentData.fooProperty, "bar" - -Tinytest.add "experiment - instance - teardown and log", withCleanup (test) -> - instance = batch.createInstance([]) - instance.setup() - TestUtils.sleep(10) # Enforce different log timestamp - instance.teardown() - - logEntry = lastLog(instance.groupId) - test.isTrue logEntry - test.equal logEntry?._meta, "teardown" - - instanceData = Experiments.findOne(instance.groupId) - test.instanceOf instanceData.endTime, Date - -Tinytest.add "experiment - instance - get treatment on server", withCleanup (test) -> - instance = batch.createInstance(["fooTreatment"]) +Tinytest.add("experiment - instance - get treatment on server", withCleanup(function(test) { + const instance = batch.createInstance(["fooTreatment"]); - # Note this only tests world treatments. Assignment treatments have to be - # tested with the janky client setup. + // Note this only tests world treatments. Assignment treatments have to be + // tested with the janky client setup. - # However, This also tests accessing server treatments outside of a client context. - instance.bindOperation -> - treatment = TurkServer.treatment() - test.equal treatment.treatments[0], "fooTreatment" - test.equal treatment.fooProperty, "bar" + // However, This also tests accessing server treatments outside of a client context. + instance.bindOperation(function() { + const treatment = TurkServer.treatment(); + test.equal(treatment.treatments[0], "fooTreatment"); + return test.equal(treatment.fooProperty, "bar"); + }); - # Undefined outside of an experiment instance - treatment = TurkServer.treatment() - test.equal treatment.fooProperty, undefined + // Undefined outside of an experiment instance + const treatment = TurkServer.treatment(); + return test.equal(treatment.fooProperty, undefined); +}) +); -Tinytest.add "experiment - instance - global group", withCleanup (test) -> - instance = batch.createInstance([]) - instance.setup() # Inserts two items +Tinytest.add("experiment - instance - global group", withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); // Inserts two items - TestUtils.sleep(100) # Let deferred insert finish + TestUtils.sleep(100); // Let deferred insert finish - instance.bindOperation -> - Doobie.insert - foo2: "bar" + instance.bindOperation(() => Doobie.insert({ + foo2: "bar"})); - stuff = Partitioner.directOperation -> - Doobie.find().fetch() + const stuff = Partitioner.directOperation(() => Doobie.find().fetch()); - test.length stuff, 3 + test.length(stuff, 3); - # Setup insert - test.equal stuff[0].foo, "bar" - test.equal stuff[0]._groupId, instance.groupId - # Deferred insert - test.equal stuff[1].bar, "baz" - test.equal stuff[1]._groupId, instance.groupId - # Bound insert - test.equal stuff[2].foo2, "bar" - test.equal stuff[2]._groupId, instance.groupId + // Setup insert + test.equal(stuff[0].foo, "bar"); + test.equal(stuff[0]._groupId, instance.groupId); + // Deferred insert + test.equal(stuff[1].bar, "baz"); + test.equal(stuff[1]._groupId, instance.groupId); + // Bound insert + test.equal(stuff[2].foo2, "bar"); + return test.equal(stuff[2]._groupId, instance.groupId); +}) +); -Tinytest.add "experiment - assignment - reject adding user to ended instance", withCleanup (test) -> - instance = batch.createInstance([]) - instance.setup() +Tinytest.add("experiment - assignment - reject adding user to ended instance", withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - instance.teardown() + instance.teardown(); - asst = createAssignment() + const asst = createAssignment(); - test.throws -> - instance.addAssignment(asst) + test.throws(() => instance.addAssignment(asst)); - user = Meteor.users.findOne(asst.userId) - asstData = Assignments.findOne(asst.asstId) + const user = Meteor.users.findOne(asst.userId); + const asstData = Assignments.findOne(asst.asstId); - test.isFalse Partitioner.getUserGroup(asst.userId) - test.length instance.users(), 0 - test.equal user.turkserver.state, "lobby" + test.isFalse(Partitioner.getUserGroup(asst.userId)); + test.length(instance.users(), 0); + test.equal(user.turkserver.state, "lobby"); - test.isFalse asstData.instances + return test.isFalse(asstData.instances); +}) +); -Tinytest.add "experiment - assignment - addAssignment records start time and instance id", withCleanup (test) -> - instance = batch.createInstance([]) - instance.setup() +Tinytest.add("experiment - assignment - addAssignment records start time and instance id", withCleanup(function(test) { + let needle; + const instance = batch.createInstance([]); + instance.setup(); - asst = createAssignment() + const asst = createAssignment(); - instance.addAssignment(asst) + instance.addAssignment(asst); - user = Meteor.users.findOne(asst.userId) - asstData = Assignments.findOne(asst.asstId) - instanceData = Experiments.findOne(instance.groupId) + const user = Meteor.users.findOne(asst.userId); + const asstData = Assignments.findOne(asst.asstId); + const instanceData = Experiments.findOne(instance.groupId); - test.equal Partitioner.getUserGroup(asst.userId), instance.groupId + test.equal(Partitioner.getUserGroup(asst.userId), instance.groupId); - test.isTrue asst.userId in instance.users() - test.instanceOf instanceData.startTime, Date + test.isTrue((needle = asst.userId, Array.from(instance.users()).includes(needle))); + test.instanceOf(instanceData.startTime, Date); - test.equal user.turkserver.state, "experiment" - test.instanceOf(asstData.instances, Array) + test.equal(user.turkserver.state, "experiment"); + test.instanceOf(asstData.instances, Array); - test.isTrue asstData.instances[0] - test.equal asstData.instances[0].id, instance.groupId - test.isTrue asstData.instances[0].joinTime + test.isTrue(asstData.instances[0]); + test.equal(asstData.instances[0].id, instance.groupId); + return test.isTrue(asstData.instances[0].joinTime); +}) +); -Tinytest.add "experiment - assignment - second addAssignment does not change date", withCleanup (test) -> - instance = batch.createInstance([]) - instance.setup() +Tinytest.add("experiment - assignment - second addAssignment does not change date", withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - asst = createAssignment() + const asst = createAssignment(); - instance.addAssignment(asst) + instance.addAssignment(asst); - instanceData = Experiments.findOne(instance.groupId) - test.instanceOf instanceData.startTime, Date + let instanceData = Experiments.findOne(instance.groupId); + test.instanceOf(instanceData.startTime, Date); - startedDate = instanceData.startTime + const startedDate = instanceData.startTime; - TestUtils.sleep(10) - # Add a second user - asst2 = createAssignment() - instance.addAssignment(asst2) + TestUtils.sleep(10); + // Add a second user + const asst2 = createAssignment(); + instance.addAssignment(asst2); - instanceData = Experiments.findOne(instance.groupId) - # Should be the same date as originally - test.equal instanceData.startTime, startedDate + instanceData = Experiments.findOne(instance.groupId); + // Should be the same date as originally + return test.equal(instanceData.startTime, startedDate); +}) +); -Tinytest.add "experiment - assignment - teardown with returned assignment", withCleanup (test) -> - instance = batch.createInstance([]) - instance.setup() +Tinytest.add("experiment - assignment - teardown with returned assignment", withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - asst = createAssignment() + const asst = createAssignment(); - instance.addAssignment(asst) + instance.addAssignment(asst); - asst.setReturned() + asst.setReturned(); - instance.teardown() # This should not throw + instance.teardown(); // This should not throw - user = Meteor.users.findOne(asst.userId) - asstData = Assignments.findOne(asst.asstId) + const user = Meteor.users.findOne(asst.userId); + const asstData = Assignments.findOne(asst.asstId); - test.isFalse Partitioner.getUserGroup(asst.userId) - test.isFalse user.turkserver?.state - test.isTrue asstData.instances[0] - test.equal asstData.status, "returned" + test.isFalse(Partitioner.getUserGroup(asst.userId)); + test.isFalse(user.turkserver != null ? user.turkserver.state : undefined); + test.isTrue(asstData.instances[0]); + return test.equal(asstData.status, "returned"); +}) +); -Tinytest.add "experiment - assignment - user disconnect and reconnect", withCleanup (test) -> - instance = batch.createInstance([]) - instance.setup() +Tinytest.add("experiment - assignment - user disconnect and reconnect", withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - asst = createAssignment() + const asst = createAssignment(); - instance.addAssignment(asst) + instance.addAssignment(asst); - TestUtils.connCallbacks.sessionDisconnect - userId: asst.userId + TestUtils.connCallbacks.sessionDisconnect({ + userId: asst.userId}); - test.isTrue disconnectContext - test.equal disconnectContext?.event, "disconnected" - test.equal disconnectContext?.instance, instance - test.equal disconnectContext?.userId, asst.userId + test.isTrue(disconnectContext); + test.equal(disconnectContext != null ? disconnectContext.event : undefined, "disconnected"); + test.equal(disconnectContext != null ? disconnectContext.instance : undefined, instance); + test.equal(disconnectContext != null ? disconnectContext.userId : undefined, asst.userId); - asstData = Assignments.findOne(asst.asstId) + let asstData = Assignments.findOne(asst.asstId); - # TODO ensure the accounting here is done correctly - discTime = null + // TODO ensure the accounting here is done correctly + let discTime = null; - test.isTrue asstData.instances[0] - test.isTrue asstData.instances[0].joinTime - test.isTrue (discTime = asstData.instances[0].lastDisconnect) + test.isTrue(asstData.instances[0]); + test.isTrue(asstData.instances[0].joinTime); + test.isTrue((discTime = asstData.instances[0].lastDisconnect)); - TestUtils.connCallbacks.sessionReconnect - userId: asst.userId + TestUtils.connCallbacks.sessionReconnect({ + userId: asst.userId}); - test.isTrue reconnectContext - test.equal reconnectContext?.event, "connected" - test.equal reconnectContext?.instance, instance - test.equal reconnectContext?.userId, asst.userId + test.isTrue(reconnectContext); + test.equal(reconnectContext != null ? reconnectContext.event : undefined, "connected"); + test.equal(reconnectContext != null ? reconnectContext.instance : undefined, instance); + test.equal(reconnectContext != null ? reconnectContext.userId : undefined, asst.userId); - asstData = Assignments.findOne(asst.asstId) - test.isFalse asstData.instances[0].lastDisconnect - # We don't know the exact length of disconnection, but make sure it's in the right ballpark - test.isTrue asstData.instances[0].disconnectedTime > 0 - test.isTrue asstData.instances[0].disconnectedTime < Date.now() - discTime + asstData = Assignments.findOne(asst.asstId); + test.isFalse(asstData.instances[0].lastDisconnect); + // We don't know the exact length of disconnection, but make sure it's in the right ballpark + test.isTrue(asstData.instances[0].disconnectedTime > 0); + return test.isTrue(asstData.instances[0].disconnectedTime < (Date.now() - discTime)); +}) +); -Tinytest.add "experiment - assignment - user idle and re-activate", withCleanup (test) -> - instance = batch.createInstance([]) - instance.setup() +Tinytest.add("experiment - assignment - user idle and re-activate", withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - asst = createAssignment() + const asst = createAssignment(); - instance.addAssignment(asst) + instance.addAssignment(asst); - idleTime = new Date() + const idleTime = new Date(); - TestUtils.connCallbacks.sessionIdle - userId: asst.userId + TestUtils.connCallbacks.sessionIdle({ + userId: asst.userId, lastActivity: idleTime + }); - test.isTrue idleContext - test.equal idleContext?.event, "idle" - test.equal idleContext?.instance, instance - test.equal idleContext?.userId, asst.userId + test.isTrue(idleContext); + test.equal(idleContext != null ? idleContext.event : undefined, "idle"); + test.equal(idleContext != null ? idleContext.instance : undefined, instance); + test.equal(idleContext != null ? idleContext.userId : undefined, asst.userId); - asstData = Assignments.findOne(asst.asstId) - test.isTrue asstData.instances[0] - test.isTrue asstData.instances[0].joinTime - test.equal asstData.instances[0].lastIdle, idleTime + let asstData = Assignments.findOne(asst.asstId); + test.isTrue(asstData.instances[0]); + test.isTrue(asstData.instances[0].joinTime); + test.equal(asstData.instances[0].lastIdle, idleTime); - offset = 1000 - activeTime = new Date(idleTime.getTime() + offset) + const offset = 1000; + const activeTime = new Date(idleTime.getTime() + offset); - TestUtils.connCallbacks.sessionActive - userId: asst.userId + TestUtils.connCallbacks.sessionActive({ + userId: asst.userId, lastActivity: activeTime + }); - test.isTrue activeContext - test.equal activeContext?.event, "active" - test.equal activeContext?.instance, instance - test.equal activeContext?.userId, asst.userId + test.isTrue(activeContext); + test.equal(activeContext != null ? activeContext.event : undefined, "active"); + test.equal(activeContext != null ? activeContext.instance : undefined, instance); + test.equal(activeContext != null ? activeContext.userId : undefined, asst.userId); - asstData = Assignments.findOne(asst.asstId) - test.isFalse asstData.instances[0].lastIdle - test.equal asstData.instances[0].idleTime, offset + asstData = Assignments.findOne(asst.asstId); + test.isFalse(asstData.instances[0].lastIdle); + test.equal(asstData.instances[0].idleTime, offset); - # Another bout of inactivity - secondIdleTime = new Date(activeTime.getTime() + 5000) - secondActiveTime = new Date(secondIdleTime.getTime() + offset) + // Another bout of inactivity + const secondIdleTime = new Date(activeTime.getTime() + 5000); + const secondActiveTime = new Date(secondIdleTime.getTime() + offset); - TestUtils.connCallbacks.sessionIdle - userId: asst.userId + TestUtils.connCallbacks.sessionIdle({ + userId: asst.userId, lastActivity: secondIdleTime + }); - TestUtils.connCallbacks.sessionActive - userId: asst.userId + TestUtils.connCallbacks.sessionActive({ + userId: asst.userId, lastActivity: secondActiveTime + }); - asstData = Assignments.findOne(asst.asstId) - test.isFalse asstData.instances[0].lastIdle - test.equal asstData.instances[0].idleTime, offset + offset + asstData = Assignments.findOne(asst.asstId); + test.isFalse(asstData.instances[0].lastIdle); + return test.equal(asstData.instances[0].idleTime, offset + offset); +}) +); -Tinytest.add "experiment - assignment - user disconnect while idle", withCleanup (test) -> - instance = batch.createInstance([]) - instance.setup() +Tinytest.add("experiment - assignment - user disconnect while idle", withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - asst = createAssignment() + const asst = createAssignment(); - instance.addAssignment(asst) + instance.addAssignment(asst); - idleTime = new Date() + const idleTime = new Date(); - TestUtils.connCallbacks.sessionIdle - userId: asst.userId + TestUtils.connCallbacks.sessionIdle({ + userId: asst.userId, lastActivity: idleTime + }); - TestUtils.connCallbacks.sessionDisconnect - userId: asst.userId + TestUtils.connCallbacks.sessionDisconnect({ + userId: asst.userId}); - asstData = Assignments.findOne(asst.asstId) - test.isTrue asstData.instances[0].joinTime - # Check that idle fields exist - test.isFalse asstData.instances[0].lastIdle - test.isTrue asstData.instances[0].idleTime - # Check that disconnect fields exist - test.isTrue asstData.instances[0].lastDisconnect + const asstData = Assignments.findOne(asst.asstId); + test.isTrue(asstData.instances[0].joinTime); + // Check that idle fields exist + test.isFalse(asstData.instances[0].lastIdle); + test.isTrue(asstData.instances[0].idleTime); + // Check that disconnect fields exist + return test.isTrue(asstData.instances[0].lastDisconnect); +}) +); -Tinytest.add "experiment - assignment - idleness is cleared on reconnection", withCleanup (test) -> - instance = batch.createInstance([]) - instance.setup() +Tinytest.add("experiment - assignment - idleness is cleared on reconnection", withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - asst = createAssignment() + const asst = createAssignment(); - instance.addAssignment(asst) + instance.addAssignment(asst); - idleTime = new Date() + const idleTime = new Date(); - TestUtils.connCallbacks.sessionDisconnect - userId: asst.userId + TestUtils.connCallbacks.sessionDisconnect({ + userId: asst.userId}); - TestUtils.connCallbacks.sessionIdle - userId: asst.userId + TestUtils.connCallbacks.sessionIdle({ + userId: asst.userId, lastActivity: idleTime + }); - TestUtils.sleep(100) + TestUtils.sleep(100); - TestUtils.connCallbacks.sessionReconnect - userId: asst.userId + TestUtils.connCallbacks.sessionReconnect({ + userId: asst.userId}); - asstData = Assignments.findOne(asst.asstId) + const asstData = Assignments.findOne(asst.asstId); - test.isTrue asstData.instances[0].joinTime - # Check that idleness was not counted - test.isFalse asstData.instances[0].lastIdle - test.isFalse asstData.instances[0].idleTime - # Check that disconnect fields exist - test.isFalse asstData.instances[0].lastDisconnect - test.isTrue asstData.instances[0].disconnectedTime + test.isTrue(asstData.instances[0].joinTime); + // Check that idleness was not counted + test.isFalse(asstData.instances[0].lastIdle); + test.isFalse(asstData.instances[0].idleTime); + // Check that disconnect fields exist + test.isFalse(asstData.instances[0].lastDisconnect); + return test.isTrue(asstData.instances[0].disconnectedTime); +}) +); -Tinytest.add "experiment - assignment - teardown while disconnected", withCleanup (test) -> - instance = batch.createInstance([]) - instance.setup() +Tinytest.add("experiment - assignment - teardown while disconnected", withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - asst = createAssignment() + const asst = createAssignment(); - instance.addAssignment(asst) + instance.addAssignment(asst); - TestUtils.connCallbacks.sessionDisconnect - userId: asst.userId + TestUtils.connCallbacks.sessionDisconnect({ + userId: asst.userId}); - discTime = null - asstData = Assignments.findOne(asst.asstId) - test.isTrue(discTime = asstData.instances[0].lastDisconnect) + let discTime = null; + let asstData = Assignments.findOne(asst.asstId); + test.isTrue(discTime = asstData.instances[0].lastDisconnect); - instance.teardown() + instance.teardown(); - asstData = Assignments.findOne(asst.asstId) + asstData = Assignments.findOne(asst.asstId); - test.isFalse Partitioner.getUserGroup(asst.userId) + test.isFalse(Partitioner.getUserGroup(asst.userId)); - test.isTrue asstData.instances[0].leaveTime - test.isFalse asstData.instances[0].lastDisconnect - # We don't know the exact length of disconnection, but make sure it's in the right ballpark - test.isTrue asstData.instances[0].disconnectedTime > 0 - test.isTrue asstData.instances[0].disconnectedTime < Date.now() - discTime + test.isTrue(asstData.instances[0].leaveTime); + test.isFalse(asstData.instances[0].lastDisconnect); + // We don't know the exact length of disconnection, but make sure it's in the right ballpark + test.isTrue(asstData.instances[0].disconnectedTime > 0); + return test.isTrue(asstData.instances[0].disconnectedTime < (Date.now() - discTime)); +}) +); -Tinytest.add "experiment - assignment - teardown while idle", withCleanup (test) -> - instance = batch.createInstance([]) - instance.setup() +Tinytest.add("experiment - assignment - teardown while idle", withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - asst = createAssignment() + const asst = createAssignment(); - instance.addAssignment(asst) + instance.addAssignment(asst); - idleTime = new Date() + const idleTime = new Date(); - TestUtils.connCallbacks.sessionIdle - userId: asst.userId + TestUtils.connCallbacks.sessionIdle({ + userId: asst.userId, lastActivity: idleTime + }); - instance.teardown() + instance.teardown(); - asstData = Assignments.findOne(asst.asstId) + const asstData = Assignments.findOne(asst.asstId); - test.isFalse Partitioner.getUserGroup(asst.userId) + test.isFalse(Partitioner.getUserGroup(asst.userId)); - test.isTrue asstData.instances[0].leaveTime - test.isFalse asstData.instances[0].lastIdle - test.isTrue asstData.instances[0].idleTime + test.isTrue(asstData.instances[0].leaveTime); + test.isFalse(asstData.instances[0].lastIdle); + return test.isTrue(asstData.instances[0].idleTime); +}) +); -Tinytest.add "experiment - assignment - leave instance after teardown", withCleanup (test) -> - instance = batch.createInstance([]) - instance.setup() +Tinytest.add("experiment - assignment - leave instance after teardown", withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - asst = createAssignment() + const asst = createAssignment(); - instance.addAssignment(asst) + instance.addAssignment(asst); - # Immediately disconnect - TestUtils.connCallbacks.sessionDisconnect - userId: asst.userId + // Immediately disconnect + TestUtils.connCallbacks.sessionDisconnect({ + userId: asst.userId}); - instance.teardown(false) + instance.teardown(false); - # Wait a bit to ensure we have the right value; the above should have - # completed within this interval - TestUtils.sleep(200) + // Wait a bit to ensure we have the right value; the above should have + // completed within this interval + TestUtils.sleep(200); - # Could do either of the below - instance.sendUserToLobby(asst.userId) + // Could do either of the below + instance.sendUserToLobby(asst.userId); - asstData = Assignments.findOne(asst.asstId) + const asstData = Assignments.findOne(asst.asstId); - test.isFalse Partitioner.getUserGroup(asst.userId) + test.isFalse(Partitioner.getUserGroup(asst.userId)); - test.isTrue asstData.instances[0].leaveTime - test.isFalse asstData.instances[0].lastDisconnect - # We don't know the exact length of disconnection, but make sure it's in the right ballpark - test.isTrue asstData.instances[0].disconnectedTime > 0 - test.isTrue asstData.instances[0].disconnectedTime < 200 + test.isTrue(asstData.instances[0].leaveTime); + test.isFalse(asstData.instances[0].lastDisconnect); + // We don't know the exact length of disconnection, but make sure it's in the right ballpark + test.isTrue(asstData.instances[0].disconnectedTime > 0); + return test.isTrue(asstData.instances[0].disconnectedTime < 200); +}) +); -Tinytest.add "experiment - assignment - teardown and join second instance", withCleanup (test) -> - instance = batch.createInstance([]) - instance.setup() +Tinytest.add("experiment - assignment - teardown and join second instance", withCleanup(function(test) { + let needle, needle1; + const instance = batch.createInstance([]); + instance.setup(); - asst = createAssignment() + const asst = createAssignment(); - instance.addAssignment(asst) + instance.addAssignment(asst); - instance.teardown() + instance.teardown(); - user = Meteor.users.findOne(asst.userId) - asstData = Assignments.findOne(asst.asstId) + let user = Meteor.users.findOne(asst.userId); + let asstData = Assignments.findOne(asst.asstId); - test.isFalse Partitioner.getUserGroup(asst.userId) + test.isFalse(Partitioner.getUserGroup(asst.userId)); - test.isTrue asst.userId in instance.users() # Shouldn't have been removed - test.equal user.turkserver.state, "lobby" - test.instanceOf(asstData.instances, Array) + test.isTrue((needle = asst.userId, Array.from(instance.users()).includes(needle))); // Shouldn't have been removed + test.equal(user.turkserver.state, "lobby"); + test.instanceOf(asstData.instances, Array); - test.isTrue asstData.instances[0] - test.equal asstData.instances[0].id, instance.groupId - test.isTrue asstData.instances[0].joinTime - test.isTrue asstData.instances[0].leaveTime + test.isTrue(asstData.instances[0]); + test.equal(asstData.instances[0].id, instance.groupId); + test.isTrue(asstData.instances[0].joinTime); + test.isTrue(asstData.instances[0].leaveTime); - instance2 = batch.createInstance([]) - instance2.setup() + const instance2 = batch.createInstance([]); + instance2.setup(); - instance2.addAssignment(asst) + instance2.addAssignment(asst); - user = Meteor.users.findOne(asst.userId) + user = Meteor.users.findOne(asst.userId); - test.equal Partitioner.getUserGroup(asst.userId), instance2.groupId - test.equal user.turkserver.state, "experiment" + test.equal(Partitioner.getUserGroup(asst.userId), instance2.groupId); + test.equal(user.turkserver.state, "experiment"); - instance2.teardown() + instance2.teardown(); - user = Meteor.users.findOne(asst.userId) - asstData = Assignments.findOne(asst.asstId) + user = Meteor.users.findOne(asst.userId); + asstData = Assignments.findOne(asst.asstId); - test.isFalse Partitioner.getUserGroup(asst.userId) + test.isFalse(Partitioner.getUserGroup(asst.userId)); - test.isTrue asst.userId in instance2.users() # Shouldn't have been removed - test.equal user.turkserver.state, "lobby" - test.instanceOf(asstData.instances, Array) + test.isTrue((needle1 = asst.userId, Array.from(instance2.users()).includes(needle1))); // Shouldn't have been removed + test.equal(user.turkserver.state, "lobby"); + test.instanceOf(asstData.instances, Array); - # Make sure array-based updates worked - test.isTrue asstData.instances[1] - test.equal asstData.instances[1].id, instance2.groupId - test.notEqual asstData.instances[0].joinTime, asstData.instances[1].joinTime - test.notEqual asstData.instances[0].leaveTime, asstData.instances[1].leaveTime + // Make sure array-based updates worked + test.isTrue(asstData.instances[1]); + test.equal(asstData.instances[1].id, instance2.groupId); + test.notEqual(asstData.instances[0].joinTime, asstData.instances[1].joinTime); + return test.notEqual(asstData.instances[0].leaveTime, asstData.instances[1].leaveTime); +}) +); diff --git a/tests/helper_tests.js b/tests/helper_tests.js index b7b69df..93a8ea4 100644 --- a/tests/helper_tests.js +++ b/tests/helper_tests.js @@ -1,28 +1,33 @@ -# TODO: try implementing Meteor.isServer stuff with setUserId +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +// TODO: try implementing Meteor.isServer stuff with setUserId -if Meteor.isClient - Tinytest.addAsync "helpers - isAdmin", (test, next) -> - InsecureLogin.ready -> - # this should be straight up false - isFalse might take `undefined` for an answer. - test.equal TurkServer.isAdmin(), false - next() +if (Meteor.isClient) { + Tinytest.addAsync("helpers - isAdmin", (test, next) => InsecureLogin.ready(function() { + // this should be straight up false - isFalse might take `undefined` for an answer. + test.equal(TurkServer.isAdmin(), false); + return next(); + })); - Tinytest.addAsync "helpers - checkAdmin", (test, next) -> - test.throws -> - TurkServer.checkAdmin() - , (e) -> e.error is 403 and e.reason is ErrMsg.notAdminErr - next() + Tinytest.addAsync("helpers - checkAdmin", function(test, next) { + test.throws(() => TurkServer.checkAdmin() + , e => (e.error === 403) && (e.reason === ErrMsg.notAdminErr)); + return next(); + }); - Tinytest.addAsync "helpers - checkNotAdmin", (test, next) -> - TurkServer.checkNotAdmin() - test.ok() - next() + Tinytest.addAsync("helpers - checkNotAdmin", function(test, next) { + TurkServer.checkNotAdmin(); + test.ok(); + return next(); + }); +} -### +/* Timer helper tests - server/client -### -Tinytest.add "timers - formatMillis renders 0 properly", (test) -> - test.equal TurkServer.Util.formatMillis(0), "0:00:00" +*/ +Tinytest.add("timers - formatMillis renders 0 properly", test => test.equal(TurkServer.Util.formatMillis(0), "0:00:00")); -Tinytest.add "timers - formatMillis renders negative values properly", (test) -> - test.equal TurkServer.Util.formatMillis(-1000), "-0:00:01" +Tinytest.add("timers - formatMillis renders negative values properly", test => test.equal(TurkServer.Util.formatMillis(-1000), "-0:00:01")); diff --git a/tests/lobby_tests.js b/tests/lobby_tests.js index efc6308..fa68329 100644 --- a/tests/lobby_tests.js +++ b/tests/lobby_tests.js @@ -1,110 +1,142 @@ -if Meteor.isServer - # Create a batch to test the lobby on - batchId = "lobbyBatchTest" - Batches.upsert { _id: batchId }, { _id: batchId } - - lobby = TurkServer.Batch.getBatch(batchId).lobby - - userId = "lobbyUser" - - Meteor.users.upsert userId, +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +if (Meteor.isServer) { + // Create a batch to test the lobby on + const batchId = "lobbyBatchTest"; + Batches.upsert({ _id: batchId }, { _id: batchId }); + + const { + lobby + } = TurkServer.Batch.getBatch(batchId); + + const userId = "lobbyUser"; + + Meteor.users.upsert(userId, { $set: { workerId: "lobbyTestWorker" } + }); - Assignments.upsert { - batchId - hitId: "lobbyTestHIT" + Assignments.upsert({ + batchId, + hitId: "lobbyTestHIT", assignmentId: "lobbyTestAsst" - }, $set: - workerId: "lobbyTestWorker" + }, { $set: { + workerId: "lobbyTestWorker", status: "assigned" - - asst = TurkServer.Assignment.getCurrentUserAssignment(userId) - - joinedUserId = null - changedUserId = null - leftUserId = null - - lobby.events.on "user-join", (asst) -> joinedUserId = asst.userId - lobby.events.on "user-status", (asst) -> changedUserId = asst.userId - lobby.events.on "user-leave", (asst) -> leftUserId = asst.userId - - withCleanup = TestUtils.getCleanupWrapper - before: -> - lobby.pluckUsers [userId] - joinedUserId = null - changedUserId = null - leftUserId = null - after: -> - - # Basic tests just to make sure joining/leaving works as intended - Tinytest.addAsync "lobby - add user", withCleanup (test, next) -> - lobby.addAssignment(asst) - - Meteor.defer -> - test.equal joinedUserId, userId - - lobbyAssts = lobby.getAssignments() - test.length lobbyAssts, 1 - test.equal lobbyAssts[0], asst - test.equal lobbyAssts[0].userId, userId - - lobbyData = LobbyStatus.findOne(userId) - test.equal lobbyData.batchId, batchId - test.equal lobbyData.asstId, asst.asstId - - next() - - # TODO update this test for generalized lobby user state - Tinytest.addAsync "lobby - change state", withCleanup (test, next) -> - lobby.addAssignment(asst) - lobby.toggleStatus(asst.userId) - - lobbyUsers = lobby.getAssignments() - test.length lobbyUsers, 1 - test.equal lobbyUsers[0], asst - test.equal lobbyUsers[0].userId, userId - - # TODO: use better API for accessing user status - test.equal LobbyStatus.findOne(asst.userId)?.status, true - - Meteor.defer -> - test.equal changedUserId, userId - next() - - Tinytest.addAsync "lobby - remove user", withCleanup (test, next) -> - lobby.addAssignment(asst) - lobby.removeAssignment(asst) - - lobbyUsers = lobby.getAssignments() - test.length lobbyUsers, 0 - - Meteor.defer -> - test.equal leftUserId, userId - next() - - Tinytest.addAsync "lobby - remove nonexistent user", withCleanup (test, next) -> - # TODO create an assignment with some other state here - lobby.removeAssignment("rando") - - Meteor.defer -> - test.equal leftUserId, null - next() - -if Meteor.isClient - # TODO fix config test for lobby along with assigner lobby state - undefined -# Tinytest.addAsync "lobby - verify config", (test, next) -> -# groupSize = null -# -# verify = -> -# test.isTrue groupSize -# test.equal groupSize.value, 3 -# next() -# -# fail = -> -# test.fail() -# next() -# -# simplePoll (-> (groupSize = TSConfig.findOne("lobbyThreshold"))? ), verify, fail, 2000 + } +} + ); + + const asst = TurkServer.Assignment.getCurrentUserAssignment(userId); + + let joinedUserId = null; + let changedUserId = null; + let leftUserId = null; + + lobby.events.on("user-join", asst => joinedUserId = asst.userId); + lobby.events.on("user-status", asst => changedUserId = asst.userId); + lobby.events.on("user-leave", asst => leftUserId = asst.userId); + + const withCleanup = TestUtils.getCleanupWrapper({ + before() { + lobby.pluckUsers([userId]); + joinedUserId = null; + changedUserId = null; + return leftUserId = null; + }, + after() {} + }); + + // Basic tests just to make sure joining/leaving works as intended + Tinytest.addAsync("lobby - add user", withCleanup(function(test, next) { + lobby.addAssignment(asst); + + return Meteor.defer(function() { + test.equal(joinedUserId, userId); + + const lobbyAssts = lobby.getAssignments(); + test.length(lobbyAssts, 1); + test.equal(lobbyAssts[0], asst); + test.equal(lobbyAssts[0].userId, userId); + + const lobbyData = LobbyStatus.findOne(userId); + test.equal(lobbyData.batchId, batchId); + test.equal(lobbyData.asstId, asst.asstId); + + return next(); + }); + }) + ); + + // TODO update this test for generalized lobby user state + Tinytest.addAsync("lobby - change state", withCleanup(function(test, next) { + lobby.addAssignment(asst); + lobby.toggleStatus(asst.userId); + + const lobbyUsers = lobby.getAssignments(); + test.length(lobbyUsers, 1); + test.equal(lobbyUsers[0], asst); + test.equal(lobbyUsers[0].userId, userId); + + // TODO: use better API for accessing user status + test.equal(__guard__(LobbyStatus.findOne(asst.userId), x => x.status), true); + + return Meteor.defer(function() { + test.equal(changedUserId, userId); + return next(); + }); + }) + ); + + Tinytest.addAsync("lobby - remove user", withCleanup(function(test, next) { + lobby.addAssignment(asst); + lobby.removeAssignment(asst); + + const lobbyUsers = lobby.getAssignments(); + test.length(lobbyUsers, 0); + + return Meteor.defer(function() { + test.equal(leftUserId, userId); + return next(); + }); + }) + ); + + Tinytest.addAsync("lobby - remove nonexistent user", withCleanup(function(test, next) { + // TODO create an assignment with some other state here + lobby.removeAssignment("rando"); + + return Meteor.defer(function() { + test.equal(leftUserId, null); + return next(); + }); + }) + ); +} + +if (Meteor.isClient) { + // TODO fix config test for lobby along with assigner lobby state + undefined; +} +// Tinytest.addAsync "lobby - verify config", (test, next) -> +// groupSize = null +// +// verify = -> +// test.isTrue groupSize +// test.equal groupSize.value, 3 +// next() +// +// fail = -> +// test.fail() +// next() +// +// simplePoll (-> (groupSize = TSConfig.findOne("lobbyThreshold"))? ), verify, fail, 2000 + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/tests/logging_tests.js b/tests/logging_tests.js index 782baa3..3f39589 100644 --- a/tests/logging_tests.js +++ b/tests/logging_tests.js @@ -1,68 +1,85 @@ -# Server methods -if Meteor.isServer +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +// Server methods +if (Meteor.isServer) { - testGroup = "poop" + const testGroup = "poop"; - Meteor.methods - # Clear anything in logs for the given group - clearLogs: -> - throw new Meteor.Error(403, "no group assigned") unless (group = Partitioner.group())? - Logs.remove # Should be same as {}, but more explicit - _groupId: group - return - getLogs: (selector) -> - throw new Meteor.Error(403, "no group assigned") unless (group = Partitioner.group())? - selector = _.extend (selector || {}), - _groupId: group - return Logs.find(selector).fetch() + Meteor.methods({ + // Clear anything in logs for the given group + clearLogs() { + let group; + if ((group = Partitioner.group()) == null) { throw new Meteor.Error(403, "no group assigned"); } + Logs.remove({ // Should be same as {}, but more explicit + _groupId: group}); + }, + getLogs(selector) { + let group; + if ((group = Partitioner.group()) == null) { throw new Meteor.Error(403, "no group assigned"); } + selector = _.extend((selector || {}), + {_groupId: group}); + return Logs.find(selector).fetch(); + } + }); - Tinytest.add "logging - server group binding", (test) -> - Partitioner.bindGroup testGroup, -> - Meteor.call "clearLogs" - TurkServer.log - boo: "hoo" + Tinytest.add("logging - server group binding", function(test) { + Partitioner.bindGroup(testGroup, function() { + Meteor.call("clearLogs"); + return TurkServer.log({ + boo: "hoo"}); + }); - doc = Logs.findOne(boo: "hoo") + const doc = Logs.findOne({boo: "hoo"}); - test.equal doc.boo, "hoo" - test.isTrue doc._groupId - test.isTrue doc._timestamp + test.equal(doc.boo, "hoo"); + test.isTrue(doc._groupId); + return test.isTrue(doc._timestamp); + }); - Tinytest.add "logging - override timestamp", (test) -> - past = new Date(Date.now() - 1000) + Tinytest.add("logging - override timestamp", function(test) { + const past = new Date(Date.now() - 1000); - Partitioner.bindGroup testGroup, -> - Meteor.call "clearLogs" - TurkServer.log - boo: "hoo" + Partitioner.bindGroup(testGroup, function() { + Meteor.call("clearLogs"); + return TurkServer.log({ + boo: "hoo", _timestamp: past + }); + }); - doc = Logs.findOne(boo: "hoo") - test.isTrue doc._timestamp - test.equal doc._timestamp, past + const doc = Logs.findOne({boo: "hoo"}); + test.isTrue(doc._timestamp); + return test.equal(doc._timestamp, past); + }); +} -# Client methods -# These run after the experiment client tests, so they should be logged in -if Meteor.isClient - Tinytest.addAsync "logging - initialize test", (test, next) -> - Meteor.call "clearLogs", (err, res) -> - test.isFalse err - next() +// Client methods +// These run after the experiment client tests, so they should be logged in +if (Meteor.isClient) { + Tinytest.addAsync("logging - initialize test", (test, next) => Meteor.call("clearLogs", function(err, res) { + test.isFalse(err); + return next(); + })); - testAsyncMulti "logging - groupId and timestamp", [ - (test, expect) -> - TurkServer.log {foo: "bar"}, expect (err, res) -> - test.isFalse err + testAsyncMulti("logging - groupId and timestamp", [ + (test, expect) => TurkServer.log({foo: "bar"}, expect((err, res) => test.isFalse(err)) + ) , - (test, expect) -> - Meteor.call "getLogs", {foo: "bar"}, expect (err, res) -> - test.isFalse err - test.length res, 1 + (test, expect) => Meteor.call("getLogs", {foo: "bar"}, expect(function(err, res) { + test.isFalse(err); + test.length(res, 1); - logItem = res[0] + const logItem = res[0]; - test.isTrue logItem.foo - test.isTrue logItem._userId - test.isTrue logItem._groupId - test.isTrue logItem._timestamp - ] + test.isTrue(logItem.foo); + test.isTrue(logItem._userId); + test.isTrue(logItem._groupId); + return test.isTrue(logItem._timestamp); + }) + ) + ]); +} diff --git a/tests/timer_tests.js b/tests/timer_tests.js index b7f3250..c590e28 100644 --- a/tests/timer_tests.js +++ b/tests/timer_tests.js @@ -1,119 +1,141 @@ -testGroup = "timerTest" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS202: Simplify dynamic range loops + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const testGroup = "timerTest"; + +// use a before here because tests are async +const withCleanup = TestUtils.getCleanupWrapper({ + before() { + // Clear any timer information + Partitioner.bindGroup(testGroup, () => RoundTimers.remove({})); + // Clear any handlers + return TestUtils.clearRoundHandlers(); + } +}); + +Tinytest.addAsync("timers - expiration callback", withCleanup((test, next) => Partitioner.bindGroup(testGroup, function() { + const now = new Date; + + TurkServer.Timers.onRoundEnd(function(type) { + test.equal(type, TurkServer.Timers.ROUND_END_TIMEOUT); + // Cancel group binding + return Partitioner._currentGroup.withValue(null, next); + }); + + return TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); +})) +); + +Tinytest.addAsync("timers - start multiple rounds", withCleanup((test, next) => Partitioner.bindGroup(testGroup, function() { + const now = new Date; + + TurkServer.Timers.onRoundEnd(function(type) { + test.equal(type, TurkServer.Timers.ROUND_END_NEWROUND); + + return Partitioner._currentGroup.withValue(null, next); + }); + + TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); + return TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); +})) +); + +Tinytest.addAsync("timers - end and start new rounds", withCleanup((test, next) => Partitioner.bindGroup(testGroup, function() { + TurkServer.Timers.onRoundEnd(type => test.equal(type, TurkServer.Timers.ROUND_END_MANUAL)); + + const nRounds = 10; + + for (let i = 1, end = nRounds, asc = 1 <= end; asc ? i <= end : i >= end; asc ? i++ : i--) { + const now = new Date; + TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); + TurkServer.Timers.endCurrentRound(); + } + + // Make sure there are the right number of rounds + test.length(RoundTimers.find().fetch(), nRounds); + + return Partitioner._currentGroup.withValue(null, next); +})) +); + +Tinytest.addAsync("timers - early expiration", withCleanup((test, next) => Partitioner.bindGroup(testGroup, function() { + const now = new Date; + + let count = 0; + const types = {}; + + TurkServer.Timers.onRoundEnd(function(type) { + count++; + if (types[type] == null) { types[type] = 0; } + return types[type]++; + }); + + TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); + + TurkServer.Timers.endCurrentRound(); + + // round end callback should only have been called once + return Meteor.setTimeout(function() { + test.equal(count, 1); + test.equal(types[TurkServer.Timers.ROUND_END_MANUAL], 1); + // Cancel group binding + return Partitioner._currentGroup.withValue(null, next); + } + , 150); +})) +); + +Tinytest.addAsync("timers - robustness to multiple calls", withCleanup((test, next) => Partitioner.bindGroup(testGroup, function() { + + const now = new Date; + const end = new Date(now.getTime() + 300); + + let errors = 0; + + const testFunc = function() { + try { + return TurkServer.Timers.startNewRound(now, end); + } catch (e) { + // We should get at least one error here. + console.log(e); + return errors++; + } + }; + + // Make sure that running a bunch of these simultaneously doesn't bug out + for (let _ = 1; _ <= 10; _++) { Meteor.defer(testFunc); } + + return Meteor.setTimeout(function() { + // TODO: do something smarter with RoundTimers.find().fetch() + test.isTrue(errors > 0); + test.isTrue(errors < 10); + + return next(); + } + , 500); +})) +); -# use a before here because tests are async -withCleanup = TestUtils.getCleanupWrapper - before: -> - # Clear any timer information - Partitioner.bindGroup testGroup, -> - RoundTimers.remove {} - # Clear any handlers - TestUtils.clearRoundHandlers() +Tinytest.addAsync("timers - reschedule on server restart", withCleanup((test, next) => Partitioner.bindGroup(testGroup, function() { + const now = new Date; + + TurkServer.Timers.onRoundEnd(function() { + test.ok(); + // Cancel group binding + return Partitioner._currentGroup.withValue(null, next); + }); -Tinytest.addAsync "timers - expiration callback", withCleanup (test, next) -> - Partitioner.bindGroup testGroup, -> - now = new Date - - TurkServer.Timers.onRoundEnd (type) -> - test.equal(type, TurkServer.Timers.ROUND_END_TIMEOUT) - # Cancel group binding - Partitioner._currentGroup.withValue(null, next) - - TurkServer.Timers.startNewRound now, new Date(now.getTime() + 100) - -Tinytest.addAsync "timers - start multiple rounds", withCleanup (test, next) -> - Partitioner.bindGroup testGroup, -> - now = new Date - - TurkServer.Timers.onRoundEnd (type) -> - test.equal(type, TurkServer.Timers.ROUND_END_NEWROUND) - - Partitioner._currentGroup.withValue(null, next) - - TurkServer.Timers.startNewRound now, new Date(now.getTime() + 100) - TurkServer.Timers.startNewRound now, new Date(now.getTime() + 100) - -Tinytest.addAsync "timers - end and start new rounds", withCleanup (test, next) -> - Partitioner.bindGroup testGroup, -> - TurkServer.Timers.onRoundEnd (type) -> - test.equal(type, TurkServer.Timers.ROUND_END_MANUAL) - - nRounds = 10 - - for i in [1..nRounds] - now = new Date - TurkServer.Timers.startNewRound now, new Date(now.getTime() + 100) - TurkServer.Timers.endCurrentRound() - - # Make sure there are the right number of rounds - test.length RoundTimers.find().fetch(), nRounds - - Partitioner._currentGroup.withValue(null, next) - -Tinytest.addAsync "timers - early expiration", withCleanup (test, next) -> - Partitioner.bindGroup testGroup, -> - now = new Date - - count = 0 - types = {} - - TurkServer.Timers.onRoundEnd (type) -> - count++ - types[type] ?= 0 - types[type]++ - - TurkServer.Timers.startNewRound now, new Date(now.getTime() + 100) - - TurkServer.Timers.endCurrentRound() - - # round end callback should only have been called once - Meteor.setTimeout -> - test.equal(count, 1) - test.equal(types[TurkServer.Timers.ROUND_END_MANUAL], 1) - # Cancel group binding - Partitioner._currentGroup.withValue(null, next) - , 150 - -Tinytest.addAsync "timers - robustness to multiple calls", withCleanup (test, next) -> - Partitioner.bindGroup testGroup, -> - - now = new Date - end = new Date(now.getTime() + 300) - - errors = 0 - - testFunc = -> - try - TurkServer.Timers.startNewRound(now, end) - catch e - # We should get at least one error here. - console.log(e) - errors++ - - # Make sure that running a bunch of these simultaneously doesn't bug out - Meteor.defer(testFunc) for _ in [1..10] - - Meteor.setTimeout -> - # TODO: do something smarter with RoundTimers.find().fetch() - test.isTrue(errors > 0) - test.isTrue(errors < 10) - - next() - , 500 - -Tinytest.addAsync "timers - reschedule on server restart", withCleanup (test, next) -> - Partitioner.bindGroup testGroup, -> - now = new Date - - TurkServer.Timers.onRoundEnd -> - test.ok() - # Cancel group binding - Partitioner._currentGroup.withValue(null, next) - - TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)) - - # Prevent the normal timeout from being called - Meteor.clearTimeout(TestUtils.lastScheduledRound); - - # Pretend the server restarted - TestUtils.scheduleOutstandingRounds(); + TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); + + // Prevent the normal timeout from being called + Meteor.clearTimeout(TestUtils.lastScheduledRound); + + // Pretend the server restarted + return TestUtils.scheduleOutstandingRounds(); +})) +); diff --git a/tests/utils.js b/tests/utils.js index 78c681a..f3fc97f 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -1,53 +1,78 @@ -if Meteor.isClient - # Prevent router from complaining about missing path - Router.map -> - @route "/", - onBeforeAction: -> @render(null) +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +if (Meteor.isClient) { + // Prevent router from complaining about missing path + Router.map(function() { + return this.route("/", + {onBeforeAction() { return this.render(null); }}); + }); +} -if Meteor.isServer - # Clean up stuff that may have been leftover from other tests - Meteor.users.remove {} - Batches.remove {} - Experiments.remove {} - Assignments.remove {} - Treatments.remove {} +if (Meteor.isServer) { + // Clean up stuff that may have been leftover from other tests + Meteor.users.remove({}); + Batches.remove({}); + Experiments.remove({}); + Assignments.remove({}); + Treatments.remove({}); - # Stub out the mturk API + // Stub out the mturk API TestUtils.mturkAPI = { handler: null - } + }; - TurkServer.mturk = (op, params) -> - TestUtils.mturkAPI.op = op - TestUtils.mturkAPI.params = params - return TestUtils.mturkAPI.handler?(op, params) + TurkServer.mturk = function(op, params) { + TestUtils.mturkAPI.op = op; + TestUtils.mturkAPI.params = params; + return (typeof TestUtils.mturkAPI.handler === 'function' ? TestUtils.mturkAPI.handler(op, params) : undefined); + }; +} -# Get a wrapper that runs a before and after function wrapping some test function. -TestUtils.getCleanupWrapper = (settings) -> - before = settings.before - after = settings.after - # Take a function... - return (fn) -> - # Return a function that, when called, executes the hooks around the function. - return -> - next = arguments[1] - before?() +// Get a wrapper that runs a before and after function wrapping some test function. +TestUtils.getCleanupWrapper = function(settings) { + const { + before + } = settings; + const { + after + } = settings; + // Take a function... + return fn => // Return a function that, when called, executes the hooks around the function. + (function() { + const next = arguments[1]; + if (typeof before === 'function') { + before(); + } - unless next? - # Synchronous version - Tinytest.add - try - fn.apply(this, arguments) - catch error - throw error - finally - after?() - else - # Asynchronous version - Tinytest.addAsync - hookedNext = -> - after?() - next() - fn.call this, arguments[0], hookedNext + if (next == null) { + // Synchronous version - Tinytest.add + try { + return fn.apply(this, arguments); + } catch (error) { + throw error; + } + finally { + if (typeof after === 'function') { + after(); + } + } + } else { + // Asynchronous version - Tinytest.addAsync + const hookedNext = function() { + if (typeof after === 'function') { + after(); + } + return next(); + }; + return fn.call(this, arguments[0], hookedNext); + } + }); +}; -TestUtils.sleep = Meteor.wrapAsync((time, cb) -> Meteor.setTimeout (-> cb undefined), time) +TestUtils.sleep = Meteor.wrapAsync((time, cb) => Meteor.setTimeout((() => cb(undefined)), time)); -TestUtils.blockingCall = Meteor.wrapAsync(Meteor.call) +TestUtils.blockingCall = Meteor.wrapAsync(Meteor.call); From 166ec2ef2cefaef796acec1fe1534ea78e37dc8d Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Mon, 4 Nov 2019 00:28:32 -0500 Subject: [PATCH 4/7] decaffeinate: Run post-processing cleanups on admin.coffee and 31 other files --- admin/admin.js | 2 ++ admin/clientAdmin.js | 2 ++ admin/experimentAdmin.js | 2 ++ admin/lobbyAdmin.js | 2 ++ admin/mturkAdmin.js | 2 ++ admin/util.js | 2 ++ client/dialogs.js | 2 ++ client/helpers.js | 2 ++ client/lobby_client.js | 2 ++ client/logging_client.js | 2 ++ client/login.js | 2 ++ client/timers_client.js | 2 ++ client/ts_client.js | 2 ++ lib/common.js | 2 ++ lib/util.js | 2 ++ server/accounts_mturk.js | 2 ++ server/batches.js | 2 ++ server/connections.js | 2 ++ server/lobby_server.js | 2 ++ server/logging.js | 2 ++ server/turkserver.js | 2 ++ tests/admin_tests.js | 2 ++ tests/assigner_tests.js | 2 ++ tests/auth_tests.js | 2 ++ tests/connection_tests.js | 2 ++ tests/experiment_client_tests.js | 2 ++ tests/experiment_tests.js | 2 ++ tests/helper_tests.js | 2 ++ tests/lobby_tests.js | 2 ++ tests/logging_tests.js | 2 ++ tests/timer_tests.js | 2 ++ tests/utils.js | 2 ++ 32 files changed, 64 insertions(+) diff --git a/admin/admin.js b/admin/admin.js index c911e9b..074c54b 100644 --- a/admin/admin.js +++ b/admin/admin.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/admin/clientAdmin.js b/admin/clientAdmin.js index fe16a8f..af3dfbd 100644 --- a/admin/clientAdmin.js +++ b/admin/clientAdmin.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/admin/experimentAdmin.js b/admin/experimentAdmin.js index e3dd019..6d49ea3 100644 --- a/admin/experimentAdmin.js +++ b/admin/experimentAdmin.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/admin/lobbyAdmin.js b/admin/lobbyAdmin.js index 3255390..ca4f82e 100644 --- a/admin/lobbyAdmin.js +++ b/admin/lobbyAdmin.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/admin/mturkAdmin.js b/admin/mturkAdmin.js index bccb85c..4b518a6 100644 --- a/admin/mturkAdmin.js +++ b/admin/mturkAdmin.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/admin/util.js b/admin/util.js index 46354dc..bc58ad8 100644 --- a/admin/util.js +++ b/admin/util.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/client/dialogs.js b/client/dialogs.js index 1d40384..b7f5198 100644 --- a/client/dialogs.js +++ b/client/dialogs.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/client/helpers.js b/client/helpers.js index 09ced01..99eb886 100644 --- a/client/helpers.js +++ b/client/helpers.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/client/lobby_client.js b/client/lobby_client.js index ec75c05..86890e7 100644 --- a/client/lobby_client.js +++ b/client/lobby_client.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/client/logging_client.js b/client/logging_client.js index aec3c43..97056a5 100644 --- a/client/logging_client.js +++ b/client/logging_client.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/client/login.js b/client/login.js index c49c8a2..ddcf28d 100644 --- a/client/login.js +++ b/client/login.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/client/timers_client.js b/client/timers_client.js index 5a08b48..fdec453 100644 --- a/client/timers_client.js +++ b/client/timers_client.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/client/ts_client.js b/client/ts_client.js index 3a66d84..3562c0b 100644 --- a/client/ts_client.js +++ b/client/ts_client.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/lib/common.js b/lib/common.js index 0d42b2f..6b6fcac 100644 --- a/lib/common.js +++ b/lib/common.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/lib/util.js b/lib/util.js index 7c5542f..bfaa8bf 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/server/accounts_mturk.js b/server/accounts_mturk.js index cda9925..3f64dd4 100644 --- a/server/accounts_mturk.js +++ b/server/accounts_mturk.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS207: Consider shorter variations of null checks diff --git a/server/batches.js b/server/batches.js index a1369d8..a1e0ee3 100644 --- a/server/batches.js +++ b/server/batches.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/server/connections.js b/server/connections.js index 7793268..63c2c0e 100644 --- a/server/connections.js +++ b/server/connections.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/server/lobby_server.js b/server/lobby_server.js index 3af842f..1ffd9f7 100644 --- a/server/lobby_server.js +++ b/server/lobby_server.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/server/logging.js b/server/logging.js index 0d1b4cc..509bac8 100644 --- a/server/logging.js +++ b/server/logging.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/server/turkserver.js b/server/turkserver.js index 3bb0fc2..f81697e 100644 --- a/server/turkserver.js +++ b/server/turkserver.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/tests/admin_tests.js b/tests/admin_tests.js index 0ef8045..e3c748f 100644 --- a/tests/admin_tests.js +++ b/tests/admin_tests.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/tests/assigner_tests.js b/tests/assigner_tests.js index eb40100..271765b 100644 --- a/tests/assigner_tests.js +++ b/tests/assigner_tests.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/tests/auth_tests.js b/tests/auth_tests.js index 2fa07bc..4bbecb2 100644 --- a/tests/auth_tests.js +++ b/tests/auth_tests.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/tests/connection_tests.js b/tests/connection_tests.js index 74734ee..1f7e7b2 100644 --- a/tests/connection_tests.js +++ b/tests/connection_tests.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/tests/experiment_client_tests.js b/tests/experiment_client_tests.js index 3109ce6..9530dc1 100644 --- a/tests/experiment_client_tests.js +++ b/tests/experiment_client_tests.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/tests/experiment_tests.js b/tests/experiment_tests.js index 8379cc4..cb98546 100644 --- a/tests/experiment_tests.js +++ b/tests/experiment_tests.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/tests/helper_tests.js b/tests/helper_tests.js index 93a8ea4..99bc0cd 100644 --- a/tests/helper_tests.js +++ b/tests/helper_tests.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/tests/lobby_tests.js b/tests/lobby_tests.js index fa68329..2a536be 100644 --- a/tests/lobby_tests.js +++ b/tests/lobby_tests.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/tests/logging_tests.js b/tests/logging_tests.js index 3f39589..0ded518 100644 --- a/tests/logging_tests.js +++ b/tests/logging_tests.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/tests/timer_tests.js b/tests/timer_tests.js index c590e28..dcdee4f 100644 --- a/tests/timer_tests.js +++ b/tests/timer_tests.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/tests/utils.js b/tests/utils.js index f3fc97f..8609e71 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns From dd2b75db8866a1865a5f86866927e90c0bb2d30f Mon Sep 17 00:00:00 2001 From: Andrew Mao Date: Mon, 4 Nov 2019 00:38:19 -0500 Subject: [PATCH 5/7] finish decaffeination --- Contributing.md | 13 ++++----- package.js | 66 ++++++++++++++++++++++----------------------- tests/auth_tests.js | 9 +++---- 3 files changed, 42 insertions(+), 46 deletions(-) diff --git a/Contributing.md b/Contributing.md index 5be53d3..dee9217 100644 --- a/Contributing.md +++ b/Contributing.md @@ -1,11 +1,11 @@ ## Code -Initial parts of this codebase were written in Coffeescript. However, any -updates and refactoring should be done in ES6, which implements many useful -functions from Coffeescript but allows more people to read the code and +Initial parts of this codebase were written in Coffeescript. However, any +updates and refactoring should be done in ES6, which implements many useful +functions from Coffeescript but allows more people to read the code and contribute. Generally, follow AirBnb's [Javascript style guide](https://github.com/airbnb/javascript). - + More information to come. ## Testing @@ -20,11 +20,12 @@ Then run the tests: ``` cd turkserver -meteor test-packages ./ +meteor --release METEOR.VERSION test-packages ./ ``` +Where you should replace `METEOR.VERSION` with the `api.versionsFrom` specified in `package.json`. If you checked out the repository into an existing Meteor app, you can run `meteor test-packages turkserver` from your app instead. Browse to `http://localhost:3000` to run the tests. -You don't have to run the tests yourself; this project is set up for continuous integration on [Travis CI](https://travis-ci.org/TurkServer/turkserver-meteor), which runs these tests on each commit. +You don't have to run the tests yourself; this project is set up for continuous integration on [Travis CI](https://travis-ci.org/TurkServer/turkserver-meteor), which runs these tests on each commit. diff --git a/package.js b/package.js index 91c6a5c..c581000 100644 --- a/package.js +++ b/package.js @@ -33,7 +33,6 @@ Package.onUse(function (api) { 'jquery', 'random', 'underscore', - 'coffeescript', 'ecmascript', 'facts' ]); @@ -65,26 +64,26 @@ Package.onUse(function (api) { // Shared files api.addFiles([ 'lib/shared.js', - 'lib/common.coffee', - 'lib/util.coffee' + 'lib/common.js', + 'lib/util.js' ]); // Server files api.addFiles([ 'server/config.js', - 'server/turkserver.coffee', + 'server/turkserver.js', 'server/server_api.js', 'server/mturk.js', - 'server/lobby_server.coffee', - 'server/batches.coffee', + 'server/lobby_server.js', + 'server/batches.js', 'server/instance.js', - 'server/logging.coffee', + 'server/logging.js', 'server/assigners.js', 'server/assigners_extra.js', 'server/assignment.js', - 'server/connections.coffee', + 'server/connections.js', 'server/timers_server.js', - 'server/accounts_mturk.coffee' + 'server/accounts_mturk.js' ], 'server'); // Client @@ -93,32 +92,32 @@ Package.onUse(function (api) { 'client/login.html', 'client/client_api.js', 'client/ts_client.css', - 'client/ts_client.coffee', - 'client/login.coffee', - 'client/logging_client.coffee', - 'client/timers_client.coffee', - 'client/helpers.coffee', + 'client/ts_client.js', + 'client/login.js', + 'client/logging_client.js', + 'client/timers_client.js', + 'client/helpers.js', 'client/lobby_client.html', - 'client/lobby_client.coffee', - 'client/dialogs.coffee' + 'client/lobby_client.js', + 'client/dialogs.js' ], 'client'); // Admin api.addFiles([ 'admin/admin.css', 'admin/util.html', - 'admin/util.coffee', + 'admin/util.js', 'admin/clientAdmin.html', - 'admin/clientAdmin.coffee', + 'admin/clientAdmin.js', 'admin/mturkAdmin.html', - 'admin/mturkAdmin.coffee', + 'admin/mturkAdmin.js', 'admin/experimentAdmin.html', - 'admin/experimentAdmin.coffee', + 'admin/experimentAdmin.js', 'admin/lobbyAdmin.html', - 'admin/lobbyAdmin.coffee' + 'admin/lobbyAdmin.js' ], 'client'); - api.addFiles('admin/admin.coffee', 'server'); + api.addFiles('admin/admin.js', 'server'); api.export(['TurkServer']); @@ -137,7 +136,6 @@ Package.onTest(function (api) { 'accounts-password', 'check', 'deps', - 'coffeescript', 'mongo', 'random', 'ui', @@ -159,20 +157,20 @@ Package.onTest(function (api) { api.addFiles("tests/display_fix.css"); - api.addFiles('tests/utils.coffee'); // Deletes users so do it before insecure login + api.addFiles('tests/utils.js'); // Deletes users so do it before insecure login api.addFiles("tests/insecure_login.js"); - api.addFiles('tests/lobby_tests.coffee'); - api.addFiles('tests/admin_tests.coffee', 'server'); - api.addFiles('tests/auth_tests.coffee', 'server'); - api.addFiles('tests/connection_tests.coffee', 'server'); - api.addFiles('tests/experiment_tests.coffee', 'server'); - api.addFiles('tests/experiment_client_tests.coffee'); - api.addFiles('tests/timer_tests.coffee', 'server'); - api.addFiles('tests/logging_tests.coffee'); + api.addFiles('tests/lobby_tests.js'); + api.addFiles('tests/admin_tests.js', 'server'); + api.addFiles('tests/auth_tests.js', 'server'); + api.addFiles('tests/connection_tests.js', 'server'); + api.addFiles('tests/experiment_tests.js', 'server'); + api.addFiles('tests/experiment_client_tests.js'); + api.addFiles('tests/timer_tests.js', 'server'); + api.addFiles('tests/logging_tests.js'); // This goes after experiment tests, so we can be sure that assigning works - api.addFiles('tests/assigner_tests.coffee', 'server'); + api.addFiles('tests/assigner_tests.js', 'server'); // This runs after user is logged in, as it requires a userId - api.addFiles('tests/helper_tests.coffee'); + api.addFiles('tests/helper_tests.js'); }); diff --git a/tests/auth_tests.js b/tests/auth_tests.js index 4bbecb2..ae79824 100644 --- a/tests/auth_tests.js +++ b/tests/auth_tests.js @@ -47,12 +47,9 @@ HITs.upsert({HITId: hitId2}, const withCleanup = TestUtils.getCleanupWrapper({ before() { return Batches.update(authBatchId, { - $set: { active: true - }, - $unset: { allowReturns: null - } - } - ); + $set: { active: true }, + $unset: { allowReturns: null } + }); }, after() { // Only remove assignments created here to avoid side effects on server-client tests From 7163f4682ad5d48de28af6f194579a5abe1b1907 Mon Sep 17 00:00:00 2001 From: Andrew Mao Date: Mon, 4 Nov 2019 17:11:56 -0500 Subject: [PATCH 6/7] prettier all the files --- .prettierrc.yml | 1 + admin/admin.js | 257 +++--- admin/clientAdmin.js | 172 ++-- admin/experimentAdmin.js | 255 +++--- admin/lobbyAdmin.js | 22 +- admin/mturkAdmin.js | 358 ++++++--- admin/util.js | 80 +- client/client_api.js | 42 +- client/dialogs.js | 40 +- client/helpers.js | 46 +- client/lobby_client.js | 83 +- client/logging_client.js | 1 - client/login.js | 71 +- client/timers_client.js | 98 ++- client/ts_client.js | 46 +- format.sh | 2 + lib/common.js | 57 +- lib/util.js | 13 +- package.js | 201 +++-- server/accounts_mturk.js | 71 +- server/assigners.js | 26 +- server/assigners_extra.js | 479 +++++------ server/assignment.js | 178 +++-- server/batches.js | 49 +- server/config.js | 4 +- server/connections.js | 199 +++-- server/instance.js | 56 +- server/lobby_server.js | 68 +- server/logging.js | 29 +- server/mturk.js | 140 ++-- server/server_api.js | 10 +- server/timers_server.js | 48 +- server/turkserver.js | 117 +-- tests/admin_tests.js | 338 ++++---- tests/assigner_tests.js | 1283 ++++++++++++++++-------------- tests/auth_tests.js | 803 ++++++++++--------- tests/connection_tests.js | 425 +++++----- tests/experiment_client_tests.js | 231 +++--- tests/experiment_tests.js | 861 +++++++++++--------- tests/helper_tests.js | 26 +- tests/insecure_login.js | 26 +- tests/lobby_tests.js | 153 ++-- tests/logging_tests.js | 63 +- tests/timer_tests.js | 224 +++--- tests/utils.js | 76 +- 45 files changed, 4407 insertions(+), 3421 deletions(-) create mode 100644 .prettierrc.yml create mode 100755 format.sh diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000..7aaa141 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1 @@ +printWidth: 100 diff --git a/admin/admin.js b/admin/admin.js index 074c54b..e46da95 100644 --- a/admin/admin.js +++ b/admin/admin.js @@ -9,7 +9,7 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ // Server admin code -const isAdmin = userId => (userId != null) && __guard__(Meteor.users.findOne(userId), x => x.admin); +const isAdmin = userId => userId != null && __guard__(Meteor.users.findOne(userId), x => x.admin); // Only admin gets server facts Facts.setUserIdFilter(isAdmin); @@ -20,15 +20,11 @@ Facts.setUserIdFilter(isAdmin); // Publish all admin data for /turkserver Meteor.publish("tsAdmin", function() { - if (!isAdmin(this.userId)) { return []; } + if (!isAdmin(this.userId)) { + return []; + } - return [ - Batches.find(), - Treatments.find(), - Qualifications.find(), - HITTypes.find(), - HITs.find(), - ]; + return [Batches.find(), Treatments.find(), Qualifications.find(), HITTypes.find(), HITs.find()]; }); const userFindOptions = { @@ -41,7 +37,9 @@ const userFindOptions = { }; Meteor.publish("tsAdminUsers", function(groupId) { - if (!isAdmin(this.userId)) { return []; } + if (!isAdmin(this.userId)) { + return []; + } // When in a group, override whatever user publication the group sends with our fields // TODO Don't publish all users for /turkserver @@ -60,15 +58,19 @@ const offlineFindOptions = { // Helper publish function to get users for experiments that have ended. // Necessary to watch completed experiments. Meteor.publish("tsGroupUsers", function(groupId) { - if (!isAdmin(this.userId)) { return []; } + if (!isAdmin(this.userId)) { + return []; + } const exp = Experiments.findOne(groupId); - if (!exp) { return []; } + if (!exp) { + return []; + } const expUsers = exp.users || []; // This won't update if users changes, but it shouldn't after an experiment is completed // TODO Just return everything here; we don't know what the app subscription was using - return Meteor.users.find({ _id: {$in: expUsers}}, offlineFindOptions); + return Meteor.users.find({ _id: { $in: expUsers } }, offlineFindOptions); }); // Get a date that is `days` away from `date`, locked to day boundaries @@ -76,31 +78,31 @@ Meteor.publish("tsGroupUsers", function(groupId) { const getDateFloor = function(date, days) { const timestamp = date.valueOf(); const closestDay = timestamp - (timestamp % (24 * 3600 * 1000)); - return new Date(closestDay + (days * 24 * 3600 * 1000)); + return new Date(closestDay + days * 24 * 3600 * 1000); }; // Data for a single worker Meteor.publish("tsAdminWorkerData", function(workerId) { - if (!isAdmin(this.userId)) { return []; } + if (!isAdmin(this.userId)) { + return []; + } check(workerId, String); // TODO also return users here if they are not all published - return [ - Workers.find(workerId), - Assignments.find({workerId}) - ]; + return [Workers.find(workerId), Assignments.find({ workerId })]; }); Meteor.publish("tsAdminWorkers", function() { - if (!isAdmin(this.userId)) { return []; } - return [ - Workers.find(), - WorkerEmails.find() - ]; + if (!isAdmin(this.userId)) { + return []; + } + return [Workers.find(), WorkerEmails.find()]; }); Meteor.publish("tsAdminActiveAssignments", function(batchId) { - if (!isAdmin(this.userId)) { return []; } + if (!isAdmin(this.userId)) { + return []; + } check(batchId, String); // TODO this isn't fully indexed @@ -112,26 +114,33 @@ Meteor.publish("tsAdminActiveAssignments", function(batchId) { }); Meteor.publish("tsAdminCompletedAssignments", function(batchId, days, limit) { - if (!isAdmin(this.userId)) { return []; } + if (!isAdmin(this.userId)) { + return []; + } check(batchId, String); check(days, Number); check(limit, Number); - const threshold = getDateFloor(new Date, -days); + const threshold = getDateFloor(new Date(), -days); // effectively { status: "completed" } but there is an index on submitTime - return Assignments.find({ - batchId, - submitTime: { $gte: threshold } - }, { - sort: { submitTime: -1 }, - limit - }); + return Assignments.find( + { + batchId, + submitTime: { $gte: threshold } + }, + { + sort: { submitTime: -1 }, + limit + } + ); }); // Publish a single instance to the admin. Meteor.publish("tsAdminInstance", function(instance) { - if (!isAdmin(this.userId)) { return []; } + if (!isAdmin(this.userId)) { + return []; + } check(instance, String); return Experiments.find(instance); }); @@ -140,47 +149,63 @@ Meteor.publish("tsAdminInstance", function(instance) { // it's hard to do both endTime: null and endTime > some date while sorting by // endTime desc, because null sorts below any value. Meteor.publish("tsAdminBatchRunningExperiments", function(batchId) { - if (!isAdmin(this.userId)) { return []; } + if (!isAdmin(this.userId)) { + return []; + } check(batchId, String); - return Experiments.find({batchId, endTime: null}); + return Experiments.find({ batchId, endTime: null }); }); Meteor.publish("tsAdminBatchCompletedExperiments", function(batchId, days, limit) { - if (!isAdmin(this.userId)) { return []; } + if (!isAdmin(this.userId)) { + return []; + } check(batchId, String); check(days, Number); check(limit, Number); - const threshold = getDateFloor(new Date, -days); + const threshold = getDateFloor(new Date(), -days); - return Experiments.find({ - batchId, - endTime: { $gte: threshold } - }, { - sort: { endTime: -1 }, - limit - }); + return Experiments.find( + { + batchId, + endTime: { $gte: threshold } + }, + { + sort: { endTime: -1 }, + limit + } + ); }); Meteor.publish("tsGroupLogs", function(groupId, limit) { - if (!isAdmin(this.userId)) { return []; } + if (!isAdmin(this.userId)) { + return []; + } return [ Experiments.find(groupId), - Logs.find({_groupId: groupId}, { - sort: {_timestamp: -1}, - limit - }) + Logs.find( + { _groupId: groupId }, + { + sort: { _timestamp: -1 }, + limit + } + ) ]; }); // Get a HIT Type and make sure it is ready for use const getAndCheckHitType = function(hitTypeId) { - const hitType = HITTypes.findOne({HITTypeId: hitTypeId}); - if (!hitType.HITTypeId) { throw new Meteor.Error(403, "HITType not registered"); } + const hitType = HITTypes.findOne({ HITTypeId: hitTypeId }); + if (!hitType.HITTypeId) { + throw new Meteor.Error(403, "HITType not registered"); + } const batch = Batches.findOne(hitType.batchId); - if (!batch.active) { throw new Meteor.Error(403, "Batch not active; activate it first"); } + if (!batch.active) { + throw new Meteor.Error(403, "Batch not active; activate it first"); + } return hitType; }; @@ -217,7 +242,9 @@ Meteor.methods({ // Integer value is fine as array or not, but // Get the locale into its weird structure if (Array.isArray(qual.LocaleValue)) { - qual.LocaleValue = (Array.from(qual.LocaleValue).map((locale) => ({ Country: locale }))); + qual.LocaleValue = Array.from(qual.LocaleValue).map(locale => ({ + Country: locale + })); } else if (qual.LocaleValue) { qual.LocaleValue = { Country: qual.LocaleValue }; } @@ -234,8 +261,7 @@ Meteor.methods({ throw new Meteor.Error(500, e.toString()); } - HITTypes.update(hitType_id, - {$set: {HITTypeId: hitTypeId}}); + HITTypes.update(hitType_id, { $set: { HITTypeId: hitTypeId } }); }, "ts-admin-create-hit"(hitTypeId, params) { @@ -244,8 +270,7 @@ Meteor.methods({ const hitType = getAndCheckHitType(hitTypeId); params.HITTypeId = hitType.HITTypeId; - params.Question = - ` + params.Question = ` ${TurkServer.config.mturk.externalUrl}?batchId=${hitType.batchId} ${TurkServer.config.mturk.frameHeight} \ @@ -266,33 +291,34 @@ Meteor.methods({ this.unblock(); // Immediately refresh HIT data after creation Meteor.call("ts-admin-refresh-hit", hitId); - }, "ts-admin-refresh-hit"(HITId) { TurkServer.checkAdmin(); - if (!HITId) { throw new Meteor.Error(400, "HIT ID not specified"); } + if (!HITId) { + throw new Meteor.Error(400, "HIT ID not specified"); + } try { - const hitData = TurkServer.mturk("GetHIT", {HITId}); - HITs.update({HITId}, {$set: hitData}); + const hitData = TurkServer.mturk("GetHIT", { HITId }); + HITs.update({ HITId }, { $set: hitData }); } catch (e) { throw new Meteor.Error(500, e.toString()); } - }, "ts-admin-expire-hit"(HITId) { TurkServer.checkAdmin(); - if (!HITId) { throw new Meteor.Error(400, "HIT ID not specified"); } + if (!HITId) { + throw new Meteor.Error(400, "HIT ID not specified"); + } try { - const hitData = TurkServer.mturk("ForceExpireHIT", {HITId}); + const hitData = TurkServer.mturk("ForceExpireHIT", { HITId }); this.unblock(); // If successful, refresh the HIT Meteor.call("ts-admin-refresh-hit", HITId); } catch (e) { throw new Meteor.Error(500, e.toString()); } - }, "ts-admin-change-hittype"(params) { @@ -308,14 +334,13 @@ Meteor.methods({ } catch (e) { throw new Meteor.Error(500, e.toString()); } - }, "ts-admin-extend-hit"(params) { TurkServer.checkAdmin(); check(params.HITId, String); - const hit = HITs.findOne({HITId: params.HITId}); + const hit = HITs.findOne({ HITId: params.HITId }); getAndCheckHitType(hit.HITTypeId); @@ -327,7 +352,6 @@ Meteor.methods({ } catch (e) { throw new Meteor.Error(500, e.toString()); } - }, "ts-admin-lobby-event"(batchId, event) { @@ -335,7 +359,9 @@ Meteor.methods({ check(batchId, String); const batch = TurkServer.Batch.getBatch(batchId); - if (batch == null) { throw new Meteor.Error(500, `Batch ${batchId} does not exist`); } + if (batch == null) { + throw new Meteor.Error(500, `Batch ${batchId} does not exist`); + } const emitter = batch.lobby.events; emitter.emit.apply(emitter, Array.prototype.slice.call(arguments, 1)); // Event and any other arguments }, @@ -350,7 +376,9 @@ Meteor.methods({ recipients = __guard__(WorkerEmails.findOne(copyFromId), x => x.recipients); } - if (recipients == null) { recipients = []; } + if (recipients == null) { + recipients = []; + } return WorkerEmails.insert({ subject, message, recipients }); }, @@ -360,17 +388,19 @@ Meteor.methods({ check(emailId, String); const email = WorkerEmails.findOne(emailId); - if (email.sentTime != null) { throw new Error(403, "Message already sent"); } + if (email.sentTime != null) { + throw new Error(403, "Message already sent"); + } - const { - recipients - } = email; + const { recipients } = email; check(email.subject, String); check(email.message, String); check(recipients, Array); - if (recipients.length === 0) { throw new Error(403, "No recipients on e-mail"); } + if (recipients.length === 0) { + throw new Error(403, "No recipients on e-mail"); + } let count = 0; @@ -394,14 +424,17 @@ Meteor.methods({ Meteor._debug(count + " workers notified"); // Record which workers got the e-mail in case something breaks - Workers.update({_id: {$in: chunk}}, { - $push: {emailsReceived: emailId} - }, {multi: true}); + Workers.update( + { _id: { $in: chunk } }, + { + $push: { emailsReceived: emailId } + }, + { multi: true } + ); } // Record date that this was sent - WorkerEmails.update(emailId, - {$set: {sentTime: new Date}}); + WorkerEmails.update(emailId, { $set: { sentTime: new Date() } }); return `${count} workers notified.`; }, @@ -422,7 +455,8 @@ Meteor.methods({ return WorkerEmails.insert({ subject: email.subject, message: email.message, - recipients: []}); + recipients: [] + }); }, "ts-admin-delete-message"(emailId) { @@ -430,7 +464,9 @@ Meteor.methods({ check(emailId, String); const email = WorkerEmails.findOne(emailId); - if (email.sentTime) { throw new Meteor.Error(403, "Email has already been sent"); } + if (email.sentTime) { + throw new Meteor.Error(403, "Email has already been sent"); + } WorkerEmails.remove(emailId); }, @@ -439,12 +475,14 @@ Meteor.methods({ TurkServer.checkAdmin(); // Find all users that are state: experiment but don't have an active assignment // This shouldn't have to be used in most cases - Meteor.users.find({"turkserver.state": "experiment"}).map(function(user) { - if (TurkServer.Assignment.getCurrentUserAssignment(user._id) != null) { return; } - return Meteor.users.update(user._id, - {$unset: {"turkserver.state": null}}); + Meteor.users.find({ "turkserver.state": "experiment" }).map(function(user) { + if (TurkServer.Assignment.getCurrentUserAssignment(user._id) != null) { + return; + } + return Meteor.users.update(user._id, { + $unset: { "turkserver.state": null } + }); }); - }, "ts-admin-cancel-assignments"(batchId) { @@ -452,17 +490,19 @@ Meteor.methods({ check(batchId, String); let count = 0; - Assignments.find({batchId, status: "assigned"}).map(function(asst) { + Assignments.find({ batchId, status: "assigned" }).map(function(asst) { let userGroup; - const user = Meteor.users.findOne({workerId: asst.workerId}); - if (user.status != null ? user.status.online : undefined) { return; } + const user = Meteor.users.findOne({ workerId: asst.workerId }); + if (user.status != null ? user.status.online : undefined) { + return; + } const tsAsst = TurkServer.Assignment.getAssignment(asst._id); tsAsst.setReturned(); // if they were disconnected in the middle of an experiment, // and the experiment was either never torndown, // or torndown with returnToLobby = false - if (( userGroup = Partitioner.getUserGroup(user._id) ) != null) { + if ((userGroup = Partitioner.getUserGroup(user._id)) != null) { tsAsst._leaveInstance(userGroup); Partitioner.clearUserGroup(user._id); } @@ -497,7 +537,9 @@ Meteor.methods({ } }); - if (err != null) { throw err; } + if (err != null) { + throw err; + } }, "ts-admin-refresh-assignment"(asstId) { @@ -562,11 +604,11 @@ Meteor.methods({ Assignments.find({ batchId, mturkStatus: "Approved", - bonusPayment: {$gt: 0}, - bonusPaid: {$exists: false} + bonusPayment: { $gt: 0 }, + bonusPaid: { $exists: false } }).forEach(function(asst) { result.numPaid += 1; - return result.amt += asst.bonusPayment; + return (result.amt += asst.bonusPayment); }); return result; @@ -580,10 +622,9 @@ Meteor.methods({ Assignments.find({ batchId, mturkStatus: "Approved", - bonusPayment: {$gt: 0}, - bonusPaid: {$exists: false} + bonusPayment: { $gt: 0 }, + bonusPaid: { $exists: false } }).forEach(asst => TurkServer.Assignment.getAssignment(asst._id).payBonus(msg)); - }, "ts-admin-unset-bonus"(asstId) { @@ -600,7 +641,9 @@ Meteor.methods({ check(reason, String); // Protect against possible typos in payment amount. - if (amount > 10.00) { throw new Meteor.Error(403, `You probably didn't mean to pay ${amount}`); } + if (amount > 10.0) { + throw new Meteor.Error(403, `You probably didn't mean to pay ${amount}`); + } const asst = TurkServer.Assignment.getAssignment(asstId); try { @@ -623,7 +666,7 @@ Meteor.methods({ check(batchId, String); let count = 0; - Experiments.find({batchId, endTime: {$exists: false} }).map(function(instance) { + Experiments.find({ batchId, endTime: { $exists: false } }).map(function(instance) { TurkServer.Instance.getInstance(instance._id).teardown(); return count++; }); @@ -640,7 +683,7 @@ Meteor.startup(function() { return; } - const adminUser = Meteor.users.findOne({username: "admin"}); + const adminUser = Meteor.users.findOne({ username: "admin" }); if (!adminUser) { Accounts.createUser({ username: "admin", @@ -648,9 +691,7 @@ Meteor.startup(function() { }); Meteor._debug("Created Turkserver admin user from Meteor.settings."); - return Meteor.users.update({username: "admin"}, - {$set: {admin: true}}); - + return Meteor.users.update({ username: "admin" }, { $set: { admin: true } }); } else { // Make sure password matches that of settings file // Don't change password unless necessary, which pitches login tokens @@ -661,5 +702,5 @@ Meteor.startup(function() { }); function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== "undefined" && value !== null ? transform(value) : undefined; +} diff --git a/admin/clientAdmin.js b/admin/clientAdmin.js index af3dfbd..0f0a865 100644 --- a/admin/clientAdmin.js +++ b/admin/clientAdmin.js @@ -16,7 +16,6 @@ TurkServer.adminSettings = { // This controller handles the behavior of all admin templates class TSAdminController extends RouteController { static initClass() { - this.prototype.layoutTemplate = "tsAdminLayout"; } onBeforeAction() { @@ -35,11 +34,13 @@ class TSAdminController extends RouteController { // Using subscriptions here is safe as long as everything else below uses waitOn subscriptions() { - if (!TurkServer.isAdmin()) { return []; } + if (!TurkServer.isAdmin()) { + return []; + } // Subscribe to admin data if we are an admin user, and in the admin interface // Re-subscribes should be a no-op; no arguments - const subs = [ Meteor.subscribe("tsAdmin") ]; + const subs = [Meteor.subscribe("tsAdmin")]; // Subscribe to user data and resubscribe when group changes // Only subscribe if in admin interface, or assigned to a group @@ -54,28 +55,28 @@ class TSAdminController extends RouteController { } TSAdminController.initClass(); -const logSubErrors = - {onError(e) { return console.log(e); }}; +const logSubErrors = { + onError(e) { + return console.log(e); + } +}; Router.map(function() { this.route("tsOverview", { path: "/turkserver", controller: TSAdminController, template: "tsAdminOverview" - } - ); + }); this.route("tsMturk", { path: "turkserver/mturk", controller: TSAdminController, template: "tsAdminMTurk" - } - ); + }); this.route("tsHits", { path: "turkserver/hits", controller: TSAdminController, template: "tsAdminHits" - } - ); + }); // No sub needed - done with autocomplete this.route("tsWorkers", { path: "turkserver/workers/:workerId?", @@ -83,22 +84,24 @@ Router.map(function() { template: "tsAdminWorkers", waitOn() { let workerId; - if ((workerId = this.params.workerId) == null) { return; } + if ((workerId = this.params.workerId) == null) { + return; + } return Meteor.subscribe("tsAdminWorkerData", workerId); }, data() { - return {workerId: this.params.workerId}; + return { workerId: this.params.workerId }; } - } - ); + }); this.route("tsPanel", { path: "turkserver/panel", controller: TSAdminController, template: "tsAdminPanel", - waitOn() { return Meteor.subscribe("tsAdminWorkers"); } - } - ); + waitOn() { + return Meteor.subscribe("tsAdminWorkers"); + } + }); this.route("tsActiveAssignments", { path: "turkserver/assignments/active", @@ -106,11 +109,12 @@ Router.map(function() { template: "tsAdminActiveAssignments", waitOn() { let batchId; - if ((batchId = Session.get("_tsViewingBatchId")) == null) { return; } + if ((batchId = Session.get("_tsViewingBatchId")) == null) { + return; + } return Meteor.subscribe("tsAdminActiveAssignments", batchId); } - } - ); + }); this.route("tsCompletedAssignments", { path: "turkserver/assignments/completed/:days?/:limit?", @@ -118,7 +122,9 @@ Router.map(function() { template: "tsAdminCompletedAssignments", waitOn() { let batchId; - if ((batchId = Session.get("_tsViewingBatchId")) == null) { return; } + if ((batchId = Session.get("_tsViewingBatchId")) == null) { + return; + } const days = parseInt(this.params.days) || TurkServer.adminSettings.defaultDaysThreshold; const limit = parseInt(this.params.limit) || TurkServer.adminSettings.defaultLimit; return Meteor.subscribe("tsAdminCompletedAssignments", batchId, days, limit); @@ -129,27 +135,26 @@ Router.map(function() { limit: this.params.limit || TurkServer.adminSettings.defaultLimit }; } - } - ); + }); this.route("tsConnections", { path: "turkserver/connections", controller: TSAdminController, template: "tsAdminConnections" - } - ); + }); this.route("tsLobby", { path: "turkserver/lobby", controller: TSAdminController, template: "tsAdminLobby", waitOn() { let batchId; - if ((batchId = Session.get("_tsViewingBatchId")) == null) { return; } + if ((batchId = Session.get("_tsViewingBatchId")) == null) { + return; + } // Same sub as normal lobby clients return Meteor.subscribe("lobby", batchId); } - } - ); + }); this.route("tsExperiments", { path: "turkserver/experiments/:days?/:limit?", @@ -157,7 +162,9 @@ Router.map(function() { template: "tsAdminExperiments", waitOn() { let batchId; - if ((batchId = Session.get("_tsViewingBatchId")) == null) { return; } + if ((batchId = Session.get("_tsViewingBatchId")) == null) { + return; + } const days = parseInt(this.params.days) || TurkServer.adminSettings.defaultDaysThreshold; const limit = parseInt(this.params.limit) || TurkServer.adminSettings.defaultLimit; return [ @@ -171,36 +178,39 @@ Router.map(function() { limit: this.params.limit || TurkServer.adminSettings.defaultLimit }; } - } - ); + }); this.route("tsLogs", { path: "turkserver/logs/:groupId/:count", controller: TSAdminController, template: "tsAdminLogs", - waitOn() { return Meteor.subscribe("tsGroupLogs", this.params.groupId, parseInt(this.params.count)); }, + waitOn() { + return Meteor.subscribe("tsGroupLogs", this.params.groupId, parseInt(this.params.count)); + }, data() { return { instance: this.params.groupId, count: this.params.count }; } - } - ); + }); return this.route("tsManage", { path: "turkserver/manage", controller: TSAdminController, template: "tsAdminManage" - } - ); + }); }); // Extra admin user subscription for after experiment ended Deps.autorun(function() { let group; - if (!TurkServer.isAdmin()) { return; } - if ((group = Partitioner.group()) == null) { return; } + if (!TurkServer.isAdmin()) { + return; + } + if ((group = Partitioner.group()) == null) { + return; + } return Meteor.subscribe("tsGroupUsers", group); }); @@ -211,15 +221,20 @@ const pillPopoverEvents = { "mouseenter .ts-instance-pill-container"(e) { const container = $(e.target); - container.popover({ - html: true, - placement: "auto right", - trigger: "manual", - container, - // TODO: Dynamic popover content would be very helpful here. - // https://github.com/meteor/meteor/issues/2010#issuecomment-40532280 - content: Blaze.toHTMLWithData(Template.tsAdminAssignmentInstanceInfo, Blaze.getData(e.target)) - }).popover("show"); + container + .popover({ + html: true, + placement: "auto right", + trigger: "manual", + container, + // TODO: Dynamic popover content would be very helpful here. + // https://github.com/meteor/meteor/issues/2010#issuecomment-40532280 + content: Blaze.toHTMLWithData( + Template.tsAdminAssignmentInstanceInfo, + Blaze.getData(e.target) + ) + }) + .popover("show"); return container.one("mouseleave", () => container.popover("destroy")); }, @@ -232,14 +247,16 @@ const pillPopoverEvents = { "mouseenter .ts-user-pill-container"(e) { const container = $(e.target); - container.popover({ - html: true, - placement: "auto right", - trigger: "manual", - container, - // TODO: ditto - content: Blaze.toHTMLWithData(Template.tsUserPillPopover, Blaze.getData(e.target)) - }).popover("show"); + container + .popover({ + html: true, + placement: "auto right", + trigger: "manual", + container, + // TODO: ditto + content: Blaze.toHTMLWithData(Template.tsUserPillPopover, Blaze.getData(e.target)) + }) + .popover("show"); return container.one("mouseleave", () => container.popover("destroy")); } @@ -257,7 +274,9 @@ Template.turkserverPulldown.events(pillPopoverEvents); Template.turkserverPulldown.helpers({ admin: TurkServer.isAdmin, - currentExperiment() { return Experiments.findOne(); } + currentExperiment() { + return Experiments.findOne(); + } }); Template.tsAdminLogin.events = { @@ -265,38 +284,51 @@ Template.tsAdminLogin.events = { e.preventDefault(); const password = $(tp.find("input")).val(); return Meteor.loginWithPassword("admin", password, function(err) { - if (err != null) { return bootbox.alert("Unable to login: " + err.reason); } + if (err != null) { + return bootbox.alert("Unable to login: " + err.reason); + } }); } }; Template.tsAdminLayout.events(pillPopoverEvents); -const onlineUsers = () => Meteor.users.find({ - admin: {$exists: false}, - "status.online": true -}); +const onlineUsers = () => + Meteor.users.find({ + admin: { $exists: false }, + "status.online": true + }); Template.tsAdminOverview.events = { "click .-ts-account-balance"() { return Meteor.call("ts-admin-account-balance", function(err, res) { - if (err) { return bootbox.alert(err.reason); } else { return bootbox.alert(`

$${res}

`); } + if (err) { + return bootbox.alert(err.reason); + } else { + return bootbox.alert(`

$${res}

`); + } }); } }; Template.tsAdminOverview.helpers({ - onlineUserCount() { return onlineUsers().count(); }}); + onlineUserCount() { + return onlineUsers().count(); + } +}); // All non-admin users who are online, sorted by most recent login Template.tsAdminConnections.helpers({ users() { - return Meteor.users.find({ - admin: {$exists: false}, - "turkserver.state": {$exists: true} - }, { - sort: { "status.lastLogin.date" : -1 } - }); + return Meteor.users.find( + { + admin: { $exists: false }, + "turkserver.state": { $exists: true } + }, + { + sort: { "status.lastLogin.date": -1 } + } + ); } }); diff --git a/admin/experimentAdmin.js b/admin/experimentAdmin.js index 6d49ea3..d39d00d 100644 --- a/admin/experimentAdmin.js +++ b/admin/experimentAdmin.js @@ -14,70 +14,86 @@ Template.tsAdminExperiments.events({ e.preventDefault(); return Router.go("tsExperiments", { - days: parseInt(t.find("input[name=filter_days]").value) || + days: + parseInt(t.find("input[name=filter_days]").value) || TurkServer.adminSettings.defaultDaysThreshold, - limit: parseInt(t.find("input[name=filter_limit]").value) || - TurkServer.adminSettings.defaultLimit - } - ); + limit: + parseInt(t.find("input[name=filter_limit]").value) || TurkServer.adminSettings.defaultLimit + }); }, "click .-ts-stop-experiment"() { const expId = this._id; - return bootbox.confirm("This will end the experiment immediately. Are you sure?", function(res) { - if (res) { return Meteor.call("ts-admin-stop-experiment", expId); } + return bootbox.confirm("This will end the experiment immediately. Are you sure?", function( + res + ) { + if (res) { + return Meteor.call("ts-admin-stop-experiment", expId); + } }); } }); Template.tsAdminExperiments.helpers({ - numExperiments() { return Experiments.find().count(); }}); + numExperiments() { + return Experiments.find().count(); + } +}); -const numUsers = function() { return (this.users != null ? this.users.length : undefined); }; +const numUsers = function() { + return this.users != null ? this.users.length : undefined; +}; Template.tsAdminExperimentMaintenance.events({ "click .-ts-stop-all-experiments"(e) { - return bootbox.confirm("This will end all experiments in progress. Are you sure?", function(res) { - if (!res) { return; } - return TurkServer.callWithModal("ts-admin-stop-all-experiments", Session.get("_tsViewingBatchId")); + return bootbox.confirm("This will end all experiments in progress. Are you sure?", function( + res + ) { + if (!res) { + return; + } + return TurkServer.callWithModal( + "ts-admin-stop-all-experiments", + Session.get("_tsViewingBatchId") + ); }); } }); Template.tsAdminExperimentTimeline.helpers({ experiments() { - return Experiments.find({startTime: {$exists: true}}, { - sort: {startTime: 1}, - fields: {startTime: 1, endTime: 1} - }); + return Experiments.find( + { startTime: { $exists: true } }, + { + sort: { startTime: 1 }, + fields: { startTime: 1, endTime: 1 } + } + ); } }); Template.tsAdminExperimentTimeline.rendered = function() { - this.lastUpdate = new ReactiveVar(new Date); + this.lastUpdate = new ReactiveVar(new Date()); const svg = d3.select(this.find("svg")); const $svg = this.$("svg"); - const margin = - {bottom: 30}; + const margin = { bottom: 30 }; const chartHeight = $svg.height() - margin.bottom; - const x = d3.scale.linear() - .range([0, $svg.width()]); + const x = d3.scale.linear().range([0, $svg.width()]); - const y = d3.scale.ordinal() - .rangeBands([0, $svg.height() - margin.bottom], 0.2); + const y = d3.scale.ordinal().rangeBands([0, $svg.height() - margin.bottom], 0.2); - const xAxis = d3.svg.axis() + const xAxis = d3.svg + .axis() .scale(x) .orient("bottom") .ticks(5) // Dates are long - .tickFormat( date => new Date(date).toLocaleString()); + .tickFormat(date => new Date(date).toLocaleString()); - const svgX = svg.select("g.x.axis") - .attr("transform", `translate(0,${chartHeight})`); + const svgX = svg.select("g.x.axis").attr("transform", `translate(0,${chartHeight})`); const svgXgrid = svg.select("g.x.grid"); @@ -88,10 +104,10 @@ Template.tsAdminExperimentTimeline.rendered = function() { svgX.call(xAxis); // Update x grid - const grid = svgXgrid.selectAll("line.grid") - .data(x.ticks(10)); // More gridlines than above + const grid = svgXgrid.selectAll("line.grid").data(x.ticks(10)); // More gridlines than above - grid.enter() + grid + .enter() .append("line") .attr("class", "grid"); @@ -108,16 +124,21 @@ Template.tsAdminExperimentTimeline.rendered = function() { // Update bar positions; need to guard against missing values upon load return chart.selectAll(".bar").attr({ - x(e) { return (e && x(e.startTime)) || 0; }, + x(e) { + return (e && x(e.startTime)) || 0; + }, width(e) { - return (e && Math.max( x(e.endTime || now) - x(e.startTime), 0 )) || 0; + return (e && Math.max(x(e.endTime || now) - x(e.startTime), 0)) || 0; + }, + y(e) { + return (e && y(e._id)) || 0; }, - y(e) { return (e && y(e._id)) || 0; }, height: y.rangeBand() }); }; - const zoom = d3.behavior.zoom() + const zoom = d3.behavior + .zoom() .scaleExtent([1, 100]) .on("zoom", redraw); @@ -134,14 +155,18 @@ Template.tsAdminExperimentTimeline.rendered = function() { // guards below since some bars may not have data bound // compute new domains - const minStart = d3.min(exps, e => e != null ? e.startTime : undefined) || TimeSync.serverTime(null, 2000); + const minStart = + d3.min(exps, e => (e != null ? e.startTime : undefined)) || TimeSync.serverTime(null, 2000); // a running experiment hasn't ended yet :) - const maxEnd = d3.max(exps, e => (e != null ? e.endTime : undefined) || TimeSync.serverTime(null, 2000)); + const maxEnd = d3.max( + exps, + e => (e != null ? e.endTime : undefined) || TimeSync.serverTime(null, 2000) + ); // However, we cannot use Deps.currentComputation.firstRun here as data may not // be ready on first run. - x.domain( [minStart, maxEnd] ); - y.domain( exps.map( e => e && e._id) ); + x.domain([minStart, maxEnd]); + y.domain(exps.map(e => e && e._id)); // Set zoom **after** x axis has been initialized zoom.x(x); @@ -159,15 +184,12 @@ Template.tsAdminExperimentTimeline.events({ Template.tsAdminExperimentTimelineBar.onRendered(function() { d3.select(this.firstNode).datum(this.data); // Trigger re-draw on parent, guard against first render - return __guard__(this.parent().lastUpdate, x => x.set(new Date)); + return __guard__(this.parent().lastUpdate, x => x.set(new Date())); }); Template.tsAdminActiveExperiments.helpers({ experiments() { - return Experiments.find( - {endTime: {$exists: false}} - , - {sort: { startTime: -1 }}); + return Experiments.find({ endTime: { $exists: false } }, { sort: { startTime: -1 } }); }, numUsers @@ -175,10 +197,7 @@ Template.tsAdminActiveExperiments.helpers({ Template.tsAdminCompletedExperiments.helpers({ experiments() { - return Experiments.find( - {endTime: {$exists: true}} - , - {sort: { startTime: -1 }}); + return Experiments.find({ endTime: { $exists: true } }, { sort: { startTime: -1 } }); }, duration() { return TurkServer.Util.duration(this.endTime - this.startTime); @@ -187,31 +206,44 @@ Template.tsAdminCompletedExperiments.helpers({ }); Template.tsAdminExpButtons.helpers({ - dataRoute: __guard__(__guard__(Meteor.settings != null ? Meteor.settings.public : undefined, x1 => x1.turkserver), x => x.dataRoute)}); + dataRoute: __guard__( + __guard__(Meteor.settings != null ? Meteor.settings.public : undefined, x1 => x1.turkserver), + x => x.dataRoute + ) +}); Template.tsAdminLogs.helpers({ - experiment() { return Experiments.findOne(this.instance); }, - logEntries() { return Logs.find({}, {sort: {_timestamp: -1}}); }, - entryData() { return _.omit(this, "_id", "_userId", "_groupId", "_timestamp"); } + experiment() { + return Experiments.findOne(this.instance); + }, + logEntries() { + return Logs.find({}, { sort: { _timestamp: -1 } }); + }, + entryData() { + return _.omit(this, "_id", "_userId", "_groupId", "_timestamp"); + } }); Template.tsAdminLogs.events({ "submit form.ts-admin-log-filter"(e, t) { e.preventDefault(); const count = parseInt(t.find("input[name=count]").value); - if (!count) { return; } + if (!count) { + return; + } return Router.go("tsLogs", { groupId: this.instance, count - } - ); + }); } }); Template.tsAdminTreatments.helpers({ treatments, - zeroTreatments() { return Treatments.find().count() === 0; } + zeroTreatments() { + return Treatments.find().count() === 0; + } }); Template.tsAdminTreatments.events = { @@ -221,7 +253,9 @@ Template.tsAdminTreatments.events = { "click .-ts-delete-treatment"() { return Meteor.call("ts-delete-treatment", this._id, function(err, res) { - if (err) { return bootbox.alert(err.message); } + if (err) { + return bootbox.alert(err.message); + } }); } }; @@ -238,9 +272,11 @@ Template.tsAdminNewTreatment.events = { return; } - return Treatments.insert( - {name} - , function(e) { if (e) { return bootbox.alert(e.message); } }); + return Treatments.insert({ name }, function(e) { + if (e) { + return bootbox.alert(e.message); + } + }); } }; @@ -252,44 +288,46 @@ Template.tsAdminTreatmentConfig.helpers({ Template.tsAdminConfigureBatch.events = { "click .-ts-activate-batch"() { - return Batches.update(this._id, { $set: { - active: true - } - } - ); + return Batches.update(this._id, { + $set: { + active: true + } + }); }, "click .-ts-deactivate-batch"() { - return Batches.update(this._id, { $set: { - active: false - } - } - ); + return Batches.update(this._id, { + $set: { + active: false + } + }); }, "change input[name=allowReturns]"(e) { - return Batches.update(this._id, { $set: { - allowReturns: e.target.checked - } - } - ); + return Batches.update(this._id, { + $set: { + allowReturns: e.target.checked + } + }); } }; Template.tsAdminConfigureBatch.helpers({ - selectedBatch() { return Batches.findOne(Session.get("_tsSelectedBatchId")); }}); + selectedBatch() { + return Batches.findOne(Session.get("_tsSelectedBatchId")); + } +}); Template.tsAdminBatchEditDesc.rendered = function() { - const container = this.$('div.editable'); + const container = this.$("div.editable"); const grabValue = () => $.trim(container.text()); // Always get reactively updated value container.editable({ value: grabValue, display() {}, // Never set text; have Meteor update to preserve reactivity success: (response, newValue) => { - Batches.update(this.data._id, - {$set: { desc: newValue }}); + Batches.update(this.data._id, { $set: { desc: newValue } }); // Thinks it knows the value, but it actually doesn't - grab a fresh value each time - Meteor.defer(() => container.data('editableContainer').formOptions.value = grabValue); + Meteor.defer(() => (container.data("editableContainer").formOptions.value = grabValue)); } }); // The value of this function matters }; @@ -297,21 +335,26 @@ Template.tsAdminBatchEditDesc.rendered = function() { Template.tsAdminBatchEditTreatments.events = { "click .-ts-remove-batch-treatment"(e, tmpl) { const treatmentName = "" + (this.name || this); // In case the treatment is gone - return Batches.update(Session.get("_tsSelectedBatchId"), - {$pull: { treatments: treatmentName }}); + return Batches.update(Session.get("_tsSelectedBatchId"), { + $pull: { treatments: treatmentName } + }); }, "click .-ts-add-batch-treatment"(e, tmpl) { e.preventDefault(); const treatment = Blaze.getData(tmpl.find(":selected")); - if (treatment == null) { return; } - return Batches.update(this._id, - {$addToSet: { treatments: treatment.name }}); + if (treatment == null) { + return; + } + return Batches.update(this._id, { + $addToSet: { treatments: treatment.name } + }); } }; Template.tsAdminBatchEditTreatments.helpers({ - allTreatments: treatments}); + allTreatments: treatments +}); Template.tsAdminBatchList.events = { "click tbody > tr"(e) { @@ -320,10 +363,18 @@ Template.tsAdminBatchList.events = { }; Template.tsAdminBatchList.helpers({ - batches() { return Batches.find(); }, - zeroBatches() { return Batches.find().count() === 0; }, + batches() { + return Batches.find(); + }, + zeroBatches() { + return Batches.find().count() === 0; + }, selectedClass() { - if (Session.equals("_tsSelectedBatchId", this._id)) { return "info"; } else { return ""; } + if (Session.equals("_tsSelectedBatchId", this._id)) { + return "info"; + } else { + return ""; + } } }); @@ -333,21 +384,29 @@ Template.tsAdminAddBatch.events = { const el = tmpl.find("input"); const name = el.value; - if (name === "") { return; } + if (name === "") { + return; + } el.value = ""; // Default batch settings - return Batches.insert({ - name, - grouping: "groupSize", - groupVal: 1, - lobby: true - } - , function(e) { if (e) { return bootbox.alert(e.message); } }); + return Batches.insert( + { + name, + grouping: "groupSize", + groupVal: 1, + lobby: true + }, + function(e) { + if (e) { + return bootbox.alert(e.message); + } + } + ); } }; function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== "undefined" && value !== null ? transform(value) : undefined; +} diff --git a/admin/lobbyAdmin.js b/admin/lobbyAdmin.js index ca4f82e..46d50c9 100644 --- a/admin/lobbyAdmin.js +++ b/admin/lobbyAdmin.js @@ -6,19 +6,31 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ Template.tsAdminLobby.helpers({ - lobbyUsers() { return LobbyStatus.find(); }}); + lobbyUsers() { + return LobbyStatus.find(); + } +}); Template.tsAdminLobbyHeader.events = { "submit form"(e, t) { e.preventDefault(); const event = t.$("input[name=lobby-event]").val(); - return Meteor.call("ts-admin-lobby-event", Session.get("_tsViewingBatchId"), event, function(err, res) { - if (err) { return bootbox.alert(err); } + return Meteor.call("ts-admin-lobby-event", Session.get("_tsViewingBatchId"), event, function( + err, + res + ) { + if (err) { + return bootbox.alert(err); + } }); } }; Template.tsAdminLobbyHeader.helpers({ - count() { return LobbyStatus.find().count(); }, - readyCount() { return LobbyStatus.find({status: true}).count(); } + count() { + return LobbyStatus.find().count(); + }, + readyCount() { + return LobbyStatus.find({ status: true }).count(); + } }); diff --git a/admin/mturkAdmin.js b/admin/mturkAdmin.js index 4b518a6..07ad904 100644 --- a/admin/mturkAdmin.js +++ b/admin/mturkAdmin.js @@ -12,25 +12,40 @@ const quals = () => Qualifications.find(); const hitTypes = () => HITTypes.find(); Template.tsAdminMTurk.helpers({ - selectedHITType() { return HITTypes.findOne(Session.get("_tsSelectedHITType")); }}); + selectedHITType() { + return HITTypes.findOne(Session.get("_tsSelectedHITType")); + } +}); -Template.tsAdminMTurk.events = - {"click .-ts-new-hittype"() { return Session.set("_tsSelectedHITType", undefined); }}; +Template.tsAdminMTurk.events = { + "click .-ts-new-hittype"() { + return Session.set("_tsSelectedHITType", undefined); + } +}; -Template.tsAdminHitTypes.events = - {"click tr"() { return Session.set("_tsSelectedHITType", this._id); }}; +Template.tsAdminHitTypes.events = { + "click tr"() { + return Session.set("_tsSelectedHITType", this._id); + } +}; Template.tsAdminHitTypes.helpers({ hitTypes, selectedClass() { - if (Session.equals("_tsSelectedHITType", this._id)) { return "info"; } else { return ""; } + if (Session.equals("_tsSelectedHITType", this._id)) { + return "info"; + } else { + return ""; + } } }); Template.tsAdminViewHitType.events = { "click .-ts-register-hittype"() { return Meteor.call("ts-admin-register-hittype", this._id, function(err, res) { - if (err) { return bootbox.alert(err.reason); } + if (err) { + return bootbox.alert(err.reason); + } }); }, "click .-ts-delete-hittype"() { @@ -39,9 +54,15 @@ Template.tsAdminViewHitType.events = { }; Template.tsAdminViewHitType.helpers({ - batchName() { return __guard__(Batches.findOne(this.batchId), x => x.name) || "(none)"; }, - renderReward() { return this.Reward.toFixed(2); }, - qualName() { return __guard__(Qualifications.findOne(""+this), x => x.name); } + batchName() { + return __guard__(Batches.findOne(this.batchId), x => x.name) || "(none)"; + }, + renderReward() { + return this.Reward.toFixed(2); + }, + qualName() { + return __guard__(Qualifications.findOne("" + this), x => x.name); + } }); Template.tsAdminNewHitType.events = { @@ -65,7 +86,9 @@ Template.tsAdminNewHitType.events = { Template.tsAdminNewHitType.helpers({ quals, - batches() { return Batches.find(); } + batches() { + return Batches.find(); + } }); Template.tsAdminQuals.events = { @@ -92,12 +115,12 @@ Template.tsAdminNewQual.events = { const name = tmpl.find("input[name=name]").value; let type = tmpl.find("input[name=type]").value; const comp = tmpl.find("select[name=comp]").value; - const { - value - } = tmpl.find("input[name=value]"); + const { value } = tmpl.find("input[name=value]"); const preview = tmpl.find("input[name=preview]").checked; - if (!name || !type || !comp) { return; } + if (!name || !type || !comp) { + return; + } const qual = { name, @@ -108,11 +131,15 @@ Template.tsAdminNewQual.events = { try { switch (comp) { - case "Exists": case "DoesNotExist": - if (!!value) { throw new Error("No value should be specified for Exists or DoesNotExist"); } + case "Exists": + case "DoesNotExist": + if (!!value) { + throw new Error("No value should be specified for Exists or DoesNotExist"); + } break; - case "In": case "NotIn": + case "In": + case "NotIn": // Parse value as a comma-separated array var vals = []; type = null; @@ -121,9 +148,11 @@ Template.tsAdminNewQual.events = { // TODO we don't check for the validity of the type here for (let v of Array.from(value.split(/[\s,]+/))) { var newType, numV; - if (!v) { continue; } + if (!v) { + continue; + } - if (numV = parseInt(v)) { + if ((numV = parseInt(v))) { vals.push(numV); newType = "Integer"; } else { @@ -131,11 +160,15 @@ Template.tsAdminNewQual.events = { newType = "String"; } - if ((type != null) && (newType !== type)) { throw new Error("Must be all Integers or Locales"); } + if (type != null && newType !== type) { + throw new Error("Must be all Integers or Locales"); + } type = newType; } - if (type == null) { throw new Error("Must specify at least one value for In or NotIn"); } + if (type == null) { + throw new Error("Must specify at least one value for In or NotIn"); + } if (type === "Integer") { qual.IntegerValue = vals; @@ -144,7 +177,8 @@ Template.tsAdminNewQual.events = { } break; - default: // Things with values + default: + // Things with values if (!!value) { if (parseInt(value)) { qual.IntegerValue = value; @@ -163,13 +197,21 @@ Template.tsAdminNewQual.events = { }; Template.tsAdminHits.events = { - "click tr"() { return Session.set("_tsSelectedHIT", this._id); }, - "click .-ts-new-hit"() { return Session.set("_tsSelectedHIT", undefined); } + "click tr"() { + return Session.set("_tsSelectedHIT", this._id); + }, + "click .-ts-new-hit"() { + return Session.set("_tsSelectedHIT", undefined); + } }; Template.tsAdminHits.helpers({ - hits() { return HITs.find({}, {sort: {CreationTime: -1}}); }, - selectedHIT() { return HITs.findOne(Session.get("_tsSelectedHIT")); } + hits() { + return HITs.find({}, { sort: { CreationTime: -1 } }); + }, + selectedHIT() { + return HITs.findOne(Session.get("_tsSelectedHIT")); + } }); Template.tsAdminViewHit.events = { @@ -184,9 +226,7 @@ Template.tsAdminViewHit.events = { "submit .-ts-change-hittype"(e, tmpl) { e.preventDefault(); const htId = tmpl.find("select[name=hittype]").value; - const { - HITTypeId - } = HITTypes.findOne(htId); + const { HITTypeId } = HITTypes.findOne(htId); if (!HITTypeId) { bootbox.alert("Register that HIT Type first"); return; @@ -219,7 +259,8 @@ Template.tsAdminViewHit.events = { }; Template.tsAdminViewHit.helpers({ - hitTypes}); + hitTypes +}); Template.tsAdminNewHit.events = { "submit form"(e, tmpl) { @@ -233,8 +274,8 @@ Template.tsAdminNewHit.events = { } const params = { - MaxAssignments:parseInt(tmpl.find("input[name=maxAssts]").value), - LifetimeInSeconds:parseInt(tmpl.find("input[name=lifetime]").value) + MaxAssignments: parseInt(tmpl.find("input[name=maxAssts]").value), + LifetimeInSeconds: parseInt(tmpl.find("input[name=lifetime]").value) }; return TurkServer.callWithModal("ts-admin-create-hit", hitTypeId, params); @@ -242,7 +283,8 @@ Template.tsAdminNewHit.events = { }; Template.tsAdminNewHit.helpers({ - hitTypes}); + hitTypes +}); Template.tsAdminWorkers.helpers({ settings: { @@ -266,26 +308,32 @@ Template.tsAdminWorkers.helpers({ ] }, - workerData() { return Workers.findOne(this.workerId); }, + workerData() { + return Workers.findOne(this.workerId); + }, workerActiveAssts() { - return Assignments.find({ - workerId: this.workerId, - status: { $ne: "completed" } - }, { - sort: { acceptTime: -1 - } - }); + return Assignments.find( + { + workerId: this.workerId, + status: { $ne: "completed" } + }, + { + sort: { acceptTime: -1 } + } + ); }, workerCompletedAssts() { - return Assignments.find({ - workerId: this.workerId, - status: "completed" - }, { - sort: { submitTime: -1 - } - }); + return Assignments.find( + { + workerId: this.workerId, + status: "completed" + }, + { + sort: { submitTime: -1 } + } + ); }, numCompletedAssts() { @@ -296,10 +344,11 @@ Template.tsAdminWorkers.helpers({ } }); - Template.tsAdminWorkers.events({ "autocompleteselect input"(e, t, user) { - if (user.workerId != null) { return Router.go("tsWorkers", {workerId: user.workerId}); } + if (user.workerId != null) { + return Router.go("tsWorkers", { workerId: user.workerId }); + } } }); @@ -312,35 +361,38 @@ Template.tsAdminPanel.rendered = function() { bottom: 30 }; - const x = d3.scale.linear() - .range([0, $svg.width() - margin.left]); + const x = d3.scale.linear().range([0, $svg.width() - margin.left]); - const y = d3.scale.ordinal() + const y = d3.scale + .ordinal() // Data was originally stored in GMT -5 so just display that - .domain(Array.from(TurkServer.Util._defaultTimeSlots()).map((m) => m.zone(300).format("HH ZZ"))) + .domain(Array.from(TurkServer.Util._defaultTimeSlots()).map(m => m.zone(300).format("HH ZZ"))) .rangeBands([0, $svg.height() - margin.bottom], 0.2); - const xAxis = d3.svg.axis() + const xAxis = d3.svg + .axis() .scale(x) .orient("bottom"); - const yAxis = d3.svg.axis() + const yAxis = d3.svg + .axis() .scale(y) .orient("left"); // Draw axes - const chart = svg.append("g") - .attr("transform", "translate(" + margin.left + ",0)"); + const chart = svg.append("g").attr("transform", "translate(" + margin.left + ",0)"); - chart.append("g") + chart + .append("g") .attr("class", "x axis") .attr("transform", "translate(0," + ($svg.height() - margin.bottom) + ")") .call(xAxis); - chart.append("g") + chart + .append("g") .attr("class", "y axis") .call(yAxis) - .append("text") + .append("text") .attr("transform", "rotate(-90)") .attr("y", -80) .attr("dy", ".71em") @@ -351,7 +403,9 @@ Template.tsAdminPanel.rendered = function() { let newData = false; const redraw = function() { - if (!newData) { return; } + if (!newData) { + return; + } newData = false; const entries = d3.entries(data); @@ -360,38 +414,45 @@ Template.tsAdminPanel.rendered = function() { x.domain([0, d3.max(entries, d => d.value)]); chart.select("g.x.axis").call(xAxis); - const bars = chart.selectAll(".bar") - .data(entries, d => d.key); + const bars = chart.selectAll(".bar").data(entries, d => d.key); // Add any new bars in the enter selection - bars.enter() + bars + .enter() .append("rect") .attr("class", "bar") .attr("y", d => y(d.key)) .attr("height", y.rangeBand()); // Update widths in the update selection, including entered nodes - return bars.attr("data-value", d => d.value) + return bars + .attr("data-value", d => d.value) .transition() .attr("width", d => x(d.value)); }; // Aggregate the worker times into the current timezone - return this.handle = Workers.find().observeChanges({ + return (this.handle = Workers.find().observeChanges({ added(id, fields) { // Only use data from workers who agreed to be contacted - if (!fields.contact || (fields.available == null)) { return; } + if (!fields.contact || fields.available == null) { + return; + } for (let time of Array.from(fields.available.times)) { // normalize into buckets - if (!time) { continue; } // Ignore invalid (empty) entries - if (data[time] == null) { data[time] = 0; } + if (!time) { + continue; + } // Ignore invalid (empty) entries + if (data[time] == null) { + data[time] = 0; + } data[time] += 1; } newData = true; return Meteor.defer(redraw); } - }); + })); }; Template.tsAdminPanel.destroyed = function() { @@ -399,30 +460,41 @@ Template.tsAdminPanel.destroyed = function() { }; Template.tsAdminPanel.helpers({ - workerContact() { return Workers.find({contact: true}).count(); }, - workerTotal() { return Workers.find().count(); } + workerContact() { + return Workers.find({ contact: true }).count(); + }, + workerTotal() { + return Workers.find().count(); + } }); const recipientsHelper = function(recipients) { - if (recipients.length === 1) { - return recipients; - } else { - return recipients.length; - } - }; + if (recipients.length === 1) { + return recipients; + } else { + return recipients.length; + } +}; Template.tsAdminEmail.helpers({ - messages() { return WorkerEmails.find({}, {sort: {sentTime: -1}}); }, + messages() { + return WorkerEmails.find({}, { sort: { sentTime: -1 } }); + }, recipientsHelper }); Template.tsAdminEmail.events({ - "click tr"() { return Session.set("_tsSelectedEmailId", this._id); }}); + "click tr"() { + return Session.set("_tsSelectedEmailId", this._id); + } +}); Template.tsAdminEmailMessage.helpers({ selectedMessage() { const emailId = Session.get("_tsSelectedEmailId"); - if (emailId != null) { return WorkerEmails.findOne(emailId); } + if (emailId != null) { + return WorkerEmails.findOne(emailId); + } }, recipientsHelper }); @@ -447,10 +519,13 @@ Template.tsAdminEmailMessage.events({ Template.tsAdminNewEmail.helpers({ messages() { - return WorkerEmails.find({}, { - fields: {subject: 1}, - sort: {sentTime: -1} - }); + return WorkerEmails.find( + {}, + { + fields: { subject: 1 }, + sort: { sentTime: -1 } + } + ); } }); @@ -472,17 +547,24 @@ Template.tsAdminNewEmail.events({ } } - return TurkServer.callWithModal("ts-admin-create-message", subject, message, copyFromId, res => // Display the new message - Session.set("_tsSelectedEmailId", res)); + return TurkServer.callWithModal("ts-admin-create-message", subject, message, copyFromId, ( + res // Display the new message + ) => Session.set("_tsSelectedEmailId", res)); } }); Template.tsAdminAssignmentMaintenance.events({ "click .-ts-cancel-assignments"() { - const message = "This will cancel all assignments of users are disconnected. You should only do this if these users will definitely not return to their work. Continue? "; + const message = + "This will cancel all assignments of users are disconnected. You should only do this if these users will definitely not return to their work. Continue? "; return bootbox.confirm(message, function(res) { - if (!res) { return; } - return TurkServer.callWithModal("ts-admin-cancel-assignments", Session.get("_tsViewingBatchId")); + if (!res) { + return; + } + return TurkServer.callWithModal( + "ts-admin-cancel-assignments", + Session.get("_tsViewingBatchId") + ); }); } }); @@ -492,8 +574,9 @@ const numAssignments = () => Assignments.find().count(); Template.tsAdminActiveAssignments.helpers({ numAssignments, activeAssts() { - return Assignments.find({}, { sort: {acceptTime: -1} }); - }}); + return Assignments.find({}, { sort: { acceptTime: -1 } }); + } +}); const checkBatch = function(batchId) { if (batchId == null) { @@ -506,13 +589,17 @@ const checkBatch = function(batchId) { Template.tsAdminCompletedMaintenance.events({ "click .-ts-refresh-assignments"() { const batchId = Session.get("_tsViewingBatchId"); - if (!checkBatch(batchId)) { return; } + if (!checkBatch(batchId)) { + return; + } return TurkServer.callWithModal("ts-admin-refresh-assignments", batchId); }, "click .-ts-approve-all"() { const batchId = Session.get("_tsViewingBatchId"); - if (!checkBatch(batchId)) { return; } + if (!checkBatch(batchId)) { + return; + } return TurkServer.callWithModal("ts-admin-count-submitted", batchId, function(count) { if (count === 0) { @@ -520,16 +607,23 @@ Template.tsAdminCompletedMaintenance.events({ return; } - return bootbox.prompt(`${count} assignments will be approved. Enter a (possibly blank) message to send to each worker.`, function(res) { - if (res == null) { return; } - return TurkServer.callWithModal("ts-admin-approve-all", batchId, res); - }); + return bootbox.prompt( + `${count} assignments will be approved. Enter a (possibly blank) message to send to each worker.`, + function(res) { + if (res == null) { + return; + } + return TurkServer.callWithModal("ts-admin-approve-all", batchId, res); + } + ); }); }, "click .-ts-pay-bonuses"() { const batchId = Session.get("_tsViewingBatchId"); - if (!checkBatch(batchId)) { return; } + if (!checkBatch(batchId)) { + return; + } return TurkServer.callWithModal("ts-admin-count-unpaid-bonuses", batchId, function(data) { if (data.numPaid === 0) { @@ -537,10 +631,15 @@ Template.tsAdminCompletedMaintenance.events({ return; } - return bootbox.prompt(`${data.numPaid} workers will be paid, for a total of $${data.amt}. Enter a message to send to each worker.`, function(res) { - if (!res) { return; } - return TurkServer.callWithModal("ts-admin-pay-bonuses", batchId, res); - }); + return bootbox.prompt( + `${data.numPaid} workers will be paid, for a total of $${data.amt}. Enter a message to send to each worker.`, + function(res) { + if (!res) { + return; + } + return TurkServer.callWithModal("ts-admin-pay-bonuses", batchId, res); + } + ); }); } }); @@ -550,20 +649,21 @@ Template.tsAdminCompletedAssignments.events({ e.preventDefault(); return Router.go("tsCompletedAssignments", { - days: parseInt(t.find("input[name=filter_days]").value) || + days: + parseInt(t.find("input[name=filter_days]").value) || TurkServer.adminSettings.defaultDaysThreshold, - limit: parseInt(t.find("input[name=filter_limit]").value) || - TurkServer.adminSettings.defaultLimit - } - ); + limit: + parseInt(t.find("input[name=filter_limit]").value) || TurkServer.adminSettings.defaultLimit + }); } }); Template.tsAdminCompletedAssignments.helpers({ numAssignments, completedAssts() { - return Assignments.find({}, { sort: {submitTime: -1} }); - }}); + return Assignments.find({}, { sort: { submitTime: -1 } }); + } +}); Template.tsAdminCompletedAssignmentsTable.events({ "click .ts-admin-refresh-assignment"() { @@ -572,15 +672,23 @@ Template.tsAdminCompletedAssignmentsTable.events({ "click .ts-admin-approve-assignment"() { const _asstId = this._id; - return bootbox.prompt("Approve assignment: enter an optional message to send to the worker.", res => TurkServer.callWithModal("ts-admin-approve-assignment", _asstId, res)); + return bootbox.prompt( + "Approve assignment: enter an optional message to send to the worker.", + res => TurkServer.callWithModal("ts-admin-approve-assignment", _asstId, res) + ); }, "click .ts-admin-reject-assignment"() { const _asstId = this._id; - return bootbox.prompt("1 worker's assignment will be rejected. Enter a message to send to the worker.", function(res) { - if (!res) { return; } - return TurkServer.callWithModal("ts-admin-reject-assignment", _asstId, res); - }); + return bootbox.prompt( + "1 worker's assignment will be rejected. Enter a message to send to the worker.", + function(res) { + if (!res) { + return; + } + return TurkServer.callWithModal("ts-admin-reject-assignment", _asstId, res); + } + ); }, "click .ts-admin-unset-bonus"() { @@ -595,10 +703,14 @@ Template.tsAdminCompletedAssignmentsTable.events({ Template.tsAdminCompletedAssignmentRow.helpers({ labelStatus() { switch (this.mturkStatus) { - case "Submitted": return "label-warning"; - case "Approved": return "label-primary"; - case "Rejected": return "label-danger"; - default: return "label-default"; + case "Submitted": + return "label-warning"; + case "Approved": + return "label-primary"; + case "Rejected": + return "label-danger"; + default: + return "label-default"; } }, submitted() { @@ -607,5 +719,5 @@ Template.tsAdminCompletedAssignmentRow.helpers({ }); function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== "undefined" && value !== null ? transform(value) : undefined; +} diff --git a/admin/util.js b/admin/util.js index bc58ad8..3bc3c93 100644 --- a/admin/util.js +++ b/admin/util.js @@ -7,7 +7,9 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -if (TurkServer.Util == null) { TurkServer.Util = {}; } +if (TurkServer.Util == null) { + TurkServer.Util = {}; +} TurkServer.Util.duration = function(millis) { const diff = moment.utc(millis); @@ -16,11 +18,15 @@ TurkServer.Util.duration = function(millis) { return (days !== 0 ? days + "d " : "") + time; }; -TurkServer.Util.timeSince = timestamp => TurkServer.Util.duration(TimeSync.serverTime() - timestamp); -TurkServer.Util.timeUntil = timestamp => TurkServer.Util.duration(timestamp - TimeSync.serverTime()); +TurkServer.Util.timeSince = timestamp => + TurkServer.Util.duration(TimeSync.serverTime() - timestamp); +TurkServer.Util.timeUntil = timestamp => + TurkServer.Util.duration(timestamp - TimeSync.serverTime()); TurkServer.callWithModal = function(...args1) { - let adjustedLength = Math.max(args1.length, 1), args = args1.slice(0, adjustedLength - 1), callback = args1[adjustedLength - 1]; + let adjustedLength = Math.max(args1.length, 1), + args = args1.slice(0, adjustedLength - 1), + callback = args1[adjustedLength - 1]; const dialog = bootbox.dialog({ closeButton: false, message: "

Working...

" @@ -41,7 +47,7 @@ TurkServer.callWithModal = function(...args1) { } // If callback is given, calls it with data, otherwise just alert - if ((res != null) && (callback != null)) { + if (res != null && callback != null) { return callback(res); } else if (res != null) { return bootbox.alert(res); @@ -53,7 +59,9 @@ TurkServer.callWithModal = function(...args1) { UI.registerHelper("_tsViewingBatch", () => Batches.findOne(Session.get("_tsViewingBatchId"))); -UI.registerHelper("_tsLookupTreatment", function() { return Treatments.findOne({name: ""+this}); }); +UI.registerHelper("_tsLookupTreatment", function() { + return Treatments.findOne({ name: "" + this }); +}); UI.registerHelper("_tsRenderTime", timestamp => new Date(timestamp).toLocaleString()); UI.registerHelper("_tsRenderTimeMillis", function(timestamp) { @@ -70,7 +78,7 @@ UI.registerHelper("_tsRenderISOTime", function(isoString) { }); // https://github.com/kvz/phpjs/blob/master/functions/strings/nl2br.js -const nl2br = str => (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1
$2'); +const nl2br = str => (str + "").replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, "$1
$2"); UI.registerHelper("_tsnl2br", nl2br); @@ -83,10 +91,18 @@ Template.tsBatchSelector.events = { }; Template.tsBatchSelector.helpers({ - batches() { return Batches.find({}, {sort: {name: 1}}); }, - noBatchSelection() { return !Session.get("_tsViewingBatchId"); }, - selected() { return Session.equals("_tsViewingBatchId", this._id); }, - viewingBatchId() { return Session.get("_tsViewingBatchId"); } + batches() { + return Batches.find({}, { sort: { name: 1 } }); + }, + noBatchSelection() { + return !Session.get("_tsViewingBatchId"); + }, + selected() { + return Session.equals("_tsViewingBatchId", this._id); + }, + viewingBatchId() { + return Session.get("_tsViewingBatchId"); + } }); Template.tsAdminInstance.rendered = function() { @@ -95,7 +111,10 @@ Template.tsAdminInstance.rendered = function() { }; Template.tsAdminInstance.helpers({ - instance() { return Experiments.findOne(this+""); }}); + instance() { + return Experiments.findOne(this + ""); + } +}); Template.tsAdminPayBonus.events({ "submit form"(e, t) { @@ -103,7 +122,9 @@ Template.tsAdminPayBonus.events({ const amount = parseFloat(t.find("input[name=amount]").value); const reason = t.find("textarea[name=reason]").value; - $(t.firstNode).closest(".bootbox.modal").modal('hide'); + $(t.firstNode) + .closest(".bootbox.modal") + .modal("hide"); return TurkServer.callWithModal("ts-admin-pay-bonus", this._id, amount, reason); } @@ -118,7 +139,9 @@ Template.tsAdminEmailWorker.events({ const emailId = WorkerEmails.insert({ subject, message, recipients }); - $(t.firstNode).closest(".bootbox.modal").modal('hide'); + $(t.firstNode) + .closest(".bootbox.modal") + .modal("hide"); return TurkServer.callWithModal("ts-admin-send-message", emailId); } @@ -126,9 +149,12 @@ Template.tsAdminEmailWorker.events({ const userLabelClass = function() { switch (false) { - case !(this.status != null ? this.status.idle : undefined): return "label-warning"; - case !(this.status != null ? this.status.online : undefined): return "label-success"; - default: return "label-default"; + case !(this.status != null ? this.status.idle : undefined): + return "label-warning"; + case !(this.status != null ? this.status.online : undefined): + return "label-success"; + default: + return "label-default"; } }; @@ -150,9 +176,12 @@ Template.tsAdminWorkerItem.helpers({ Template.tsUserPill.helpers({ user() { switch (false) { - case !this.userId: return Meteor.users.findOne(this.userId); - case !this.workerId: return Meteor.users.findOne({workerId: this.workerId}); - default: return this; + case !this.userId: + return Meteor.users.findOne(this.userId); + case !this.workerId: + return Meteor.users.findOne({ workerId: this.workerId }); + default: + return this; } }, // Object was already passed in labelClass: userLabelClass, @@ -170,16 +199,19 @@ Template.tsDescList.helpers({ const result = []; for (let key in this) { const value = this[key]; - result.push({key, value}); + result.push({ key, value }); } return result; }, // Special rules for rendering description lists value() { switch (false) { - case this.value !== false: return "false"; - case !_.isObject(this.value): return JSON.stringify(this.value); - default: return nl2br(this.value); + case this.value !== false: + return "false"; + case !_.isObject(this.value): + return JSON.stringify(this.value); + default: + return nl2br(this.value); } } }); diff --git a/client/client_api.js b/client/client_api.js index b8d7be9..cb2b614 100644 --- a/client/client_api.js +++ b/client/client_api.js @@ -6,9 +6,12 @@ */ TurkServer.treatment = function(name) { if (name != null) { - return Treatments.findOne({name}, { - fields: { _id: 0 } - }); + return Treatments.findOne( + { name }, + { + fields: { _id: 0 } + } + ); } // Merge all treatments into one document @@ -40,7 +43,7 @@ TurkServer.inExperiment = function() { * @returns {Boolean} true if the user is in a completed experiment */ TurkServer.instanceEnded = function() { - if ( !TurkServer.inExperiment() ) return false; + if (!TurkServer.inExperiment()) return false; const currentExp = Experiments.findOne(); return currentExp && currentExp.endTime != null; @@ -74,20 +77,20 @@ TurkServer.currentPayment = function() { * startTime, and endTime. */ TurkServer.currentRound = function() { - let activeRound = RoundTimers.findOne({ended: false}); + let activeRound = RoundTimers.findOne({ ended: false }); // TODO this polls every second, which can be quite inefficient. Could be improved. - if( activeRound && activeRound.startTime <= TimeSync.serverTime() ) { + if (activeRound && activeRound.startTime <= TimeSync.serverTime()) { return activeRound; } // Return the round before this one, if any - if( activeRound != null ) { - return RoundTimers.findOne({index: activeRound.index - 1}); + if (activeRound != null) { + return RoundTimers.findOne({ index: activeRound.index - 1 }); } // If no active round and no round scheduled, return the highest one - return RoundTimers.findOne({}, { sort: {index: -1} } ); + return RoundTimers.findOne({}, { sort: { index: -1 } }); }; // Called to start the monitor with given settings when in experiment @@ -98,15 +101,15 @@ function safeStartMonitor(threshold, idleOnBlur) { // is not synced. As soon as it succeeds, we are done. // See https://github.com/mizzao/meteor-user-status/blob/master/monitor.coffee const settings = { threshold, idleOnBlur }; - - Tracker.autorun((c) => { + + Tracker.autorun(c => { try { UserStatus.startMonitor(settings); c.stop(); console.log("Idle monitor started with ", settings); } catch (e) {} }); -}; +} function stopMonitor() { if (Deps.nonreactive(UserStatus.isMonitoring)) { @@ -115,7 +118,7 @@ function stopMonitor() { } // This Tracker Computation starts and stops idle monitoring as the user -// enters/exits an experiment +// enters/exits an experiment let idleComp = null; /** @@ -133,10 +136,10 @@ TurkServer.disableIdleMonitor = function() { * @summary Start idle monitoring on the client with specific settings, * automatically activating and deactivating as the user enters experiment * instances. - * + * * See {@link https://github.com/mizzao/meteor-user-status} for detailed * meanings of the parameters. - * + * * @locus Client * @param threshold Time of inaction before a user is considered inactive. * @param idleOnBlur Whether to count window blurs as inactivity. @@ -144,9 +147,10 @@ TurkServer.disableIdleMonitor = function() { TurkServer.enableIdleMonitor = function(threshold, idleOnBlur) { // If monitor is already started, stop it before trying new settings TurkServer.disableIdleMonitor(); - + idleComp = Deps.autorun(() => { - if (TurkServer.inExperiment()) { // This is reactive + if (TurkServer.inExperiment()) { + // This is reactive safeStartMonitor(threshold, idleOnBlur); } else { stopMonitor(); @@ -159,5 +163,7 @@ TurkServer.enableIdleMonitor = function(threshold, idleOnBlur) { */ // Run a function some time after Meteor.startup TurkServer._delayedStartup = function(func, delay) { - Meteor.startup(function() { Meteor.setTimeout(func, delay) }); + Meteor.startup(function() { + Meteor.setTimeout(func, delay); + }); }; diff --git a/client/dialogs.js b/client/dialogs.js index b7f5198..6d5f785 100644 --- a/client/dialogs.js +++ b/client/dialogs.js @@ -26,28 +26,28 @@ TurkServer._delayedStartup(function() { return Deps.autorun(function() { const status = Meteor.status(); - if (status.connected && (disconnectDialog != null)) { + if (status.connected && disconnectDialog != null) { disconnectDialog.modal("hide"); disconnectDialog = null; return; } - if (!status.connected && (disconnectDialog === null)) { + if (!status.connected && disconnectDialog === null) { disconnectDialog = bootbox.dialog({ closeButton: false, - message: - `

You have been disconnected from the server. + message: `

You have been disconnected from the server. Please check your Internet connection.

` }); return; } }); -} -, disconnectWarningDelay); +}, disconnectWarningDelay); TurkServer._displayModal = function(template, data, options) { // minimum options to get message to show - if (options == null) { options = { message: " " }; } + if (options == null) { + options = { message: " " }; + } const dialog = bootbox.dialog(options); // Take out the thing that bootbox rendered dialog.find(".bootbox-body").remove(); @@ -74,7 +74,10 @@ TurkServer.ensureUsername = function() { } // TODO: stop the username dialog popping up during the subscription process - const username = __guard__(Meteor.users.findOne(userId, {fields: {username: 1}}), x => x.username); + const username = __guard__( + Meteor.users.findOne(userId, { fields: { username: 1 } }), + x => x.username + ); if (username && usernameDialog) { usernameDialog.modal("hide"); @@ -82,8 +85,8 @@ TurkServer.ensureUsername = function() { return; } - if (!username && (usernameDialog === null)) { - usernameDialog = bootbox.dialog({message: " "}).html(''); + if (!username && usernameDialog === null) { + usernameDialog = bootbox.dialog({ message: " " }).html(""); Blaze.render(Template.tsRequestUsername, usernameDialog[0]); return; } @@ -91,21 +94,28 @@ TurkServer.ensureUsername = function() { }; Template.tsRequestUsername.events = { - "focus input"() { return Session.set("_tsUsernameError", undefined); }, + "focus input"() { + return Session.set("_tsUsernameError", undefined); + }, "submit form"(e, tmpl) { e.preventDefault(); const input = tmpl.find("input[name=username]"); input.blur(); const username = input.value; return Meteor.call("ts-set-username", username, function(err, res) { - if (err) { return Session.set("_tsUsernameError", err.reason); } + if (err) { + return Session.set("_tsUsernameError", err.reason); + } }); } }; Template.tsRequestUsername.helpers({ - usernameError() { return Session.get("_tsUsernameError"); }}); + usernameError() { + return Session.get("_tsUsernameError"); + } +}); function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== "undefined" && value !== null ? transform(value) : undefined; +} diff --git a/client/helpers.js b/client/helpers.js index 99eb886..2e8b260 100644 --- a/client/helpers.js +++ b/client/helpers.js @@ -10,41 +10,55 @@ UI.registerHelper("_tsDebug", function() { return console.log(this, arguments); }); -if (TurkServer.Util == null) { TurkServer.Util = {}; } +if (TurkServer.Util == null) { + TurkServer.Util = {}; +} TurkServer.Util._defaultTimeSlots = function() { // Default time selections: 9AM EST to 11PM EST - const m = moment.utc({hours: 9 + 5}).local(); - return ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14].map((x) => m.clone().add(x, 'hours'))); + const m = moment.utc({ hours: 9 + 5 }).local(); + return [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14].map(x => m.clone().add(x, "hours")); }; // Submit as soon as this template appears on the page. -Template.mturkSubmit.rendered = function() { return this.find("form").submit(); }; +Template.mturkSubmit.rendered = function() { + return this.find("form").submit(); +}; Template.tsTimePicker.helpers({ - zone() { return moment().format("Z"); }}); + zone() { + return moment().format("Z"); + } +}); Template.tsTimeOptions.helpers({ - momentList: TurkServer.Util._defaultTimeSlots}); + momentList: TurkServer.Util._defaultTimeSlots +}); Template.tsTimeOptions.helpers({ // Store all values in GMT-5 - valueFormatted() { return this.zone(300).format('HH ZZ'); }, + valueFormatted() { + return this.zone(300).format("HH ZZ"); + }, // Display values in user's timezone - displayFormatted() { return this.local().format('hA [UTC]Z'); } + displayFormatted() { + return this.local().format("hA [UTC]Z"); + } }); /* Submits the exit survey data to the server and submits the HIT if successful */ -TurkServer.submitExitSurvey = (results, panel) => Meteor.call("ts-submit-exitdata", results, panel, function(err, res) { - if (err) { bootbox.alert(err); } +TurkServer.submitExitSurvey = (results, panel) => + Meteor.call("ts-submit-exitdata", results, panel, function(err, res) { + if (err) { + bootbox.alert(err); + } - if (res) { - return TurkServer.submitHIT(); - } -}); - // TODO: log the user out here? Maybe doesn't matter because resume login will be disabled + if (res) { + return TurkServer.submitHIT(); + } + }); +// TODO: log the user out here? Maybe doesn't matter because resume login will be disabled TurkServer.submitHIT = () => Blaze.render(Template.mturkSubmit, document.body); - diff --git a/client/lobby_client.js b/client/lobby_client.js index 86890e7..4fe0fb4 100644 --- a/client/lobby_client.js +++ b/client/lobby_client.js @@ -13,7 +13,12 @@ As defined below, autoLobby is default true unless explicitly set to false TODO document this setting */ -if (__guard__(__guard__(Meteor.settings != null ? Meteor.settings.public : undefined, x1 => x1.turkserver), x => x.autoLobby) !== false) { +if ( + __guard__( + __guard__(Meteor.settings != null ? Meteor.settings.public : undefined, x1 => x1.turkserver), + x => x.autoLobby + ) !== false +) { Router.map(function() { return this.route("lobby", { template: "tsBasicLobby", @@ -27,44 +32,66 @@ if (__guard__(__guard__(Meteor.settings != null ? Meteor.settings.public : undef return this.next(); } } - } - ); + }); }); // We need to defer this because iron router can throw errors if a route is // hit before the page is fully loaded - Meteor.startup(() => Meteor.defer(() => // Subscribe to lobby if we are in it (auto unsubscribe if we aren't) - Deps.autorun(function() { - if (typeof Package !== 'undefined' && Package !== null ? Package.tinytest : undefined) { return; } // Don't change routes when being tested - if (TurkServer.inLobby()) { - Meteor.subscribe("lobby", __guard__(TurkServer.batch(), x2 => x2._id)); - return Router.go("/lobby"); - } - }))); + Meteor.startup(() => + Meteor.defer(() => + // Subscribe to lobby if we are in it (auto unsubscribe if we aren't) + Deps.autorun(function() { + if (typeof Package !== "undefined" && Package !== null ? Package.tinytest : undefined) { + return; + } // Don't change routes when being tested + if (TurkServer.inLobby()) { + Meteor.subscribe("lobby", __guard__(TurkServer.batch(), x2 => x2._id)); + return Router.go("/lobby"); + } + }) + ) + ); } Meteor.methods({ - "toggleStatus"() { + toggleStatus() { let existing; const userId = Meteor.userId(); - if (userId) { existing = LobbyStatus.findOne(userId); } - if (!userId || !existing) { return; } - - return LobbyStatus.update(userId, - {$set: { status: !existing.status }}); - }}); + if (userId) { + existing = LobbyStatus.findOne(userId); + } + if (!userId || !existing) { + return; + } + + return LobbyStatus.update(userId, { $set: { status: !existing.status } }); + } +}); Template.tsBasicLobby.helpers({ - count() { return LobbyStatus.find().count(); }, - lobbyInfo() { return LobbyStatus.find(); }, - identifier() { return __guard__(Meteor.users.findOne(this._id), x2 => x2.username) || "unnamed user"; } + count() { + return LobbyStatus.find().count(); + }, + lobbyInfo() { + return LobbyStatus.find(); + }, + identifier() { + return __guard__(Meteor.users.findOne(this._id), x2 => x2.username) || "unnamed user"; + } }); Template.tsLobby.helpers({ - lobbyInfo() { return LobbyStatus.find(); }, - identifier() { return __guard__(Meteor.users.findOne(this._id), x2 => x2.username) || this._id; }, + lobbyInfo() { + return LobbyStatus.find(); + }, + identifier() { + return __guard__(Meteor.users.findOne(this._id), x2 => x2.username) || this._id; + }, readyEnabled() { - return (LobbyStatus.find().count() >= TSConfig.findOne("lobbyThreshold").value) && (this._id === Meteor.userId()); + return ( + LobbyStatus.find().count() >= TSConfig.findOne("lobbyThreshold").value && + this._id === Meteor.userId() + ); } }); @@ -73,11 +100,13 @@ Template.tsLobby.events = { ev.preventDefault(); return Meteor.call("toggleStatus", function(err, res) { - if (err) { return bootbox.alert(err.reason); } + if (err) { + return bootbox.alert(err.reason); + } }); } }; function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== "undefined" && value !== null ? transform(value) : undefined; +} diff --git a/client/logging_client.js b/client/logging_client.js index 97056a5..3abc015 100644 --- a/client/logging_client.js +++ b/client/logging_client.js @@ -6,4 +6,3 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ TurkServer.log = (doc, callback) => Meteor.call("ts-log", doc, callback); - diff --git a/client/login.js b/client/login.js index ddcf28d..5f2ba44 100644 --- a/client/login.js +++ b/client/login.js @@ -25,7 +25,7 @@ const getURLParams = function() { const params = getURLParams(); -const hitIsViewing = params.assignmentId && (params.assignmentId === "ASSIGNMENT_ID_NOT_AVAILABLE"); +const hitIsViewing = params.assignmentId && params.assignmentId === "ASSIGNMENT_ID_NOT_AVAILABLE"; // UI helpers for login UI.registerHelper("hitParams", params); @@ -33,12 +33,14 @@ UI.registerHelper("hitIsViewing", hitIsViewing); // Subscribe to the currently viewed batch if in the preview page // TODO: allow for reading meta properties later as well -if (hitIsViewing && (params.batchId != null)) { +if (hitIsViewing && params.batchId != null) { Meteor.subscribe("tsLoginBatches", params.batchId); } const loginCallback = function(err) { - if (!err) { return; } + if (!err) { + return; + } console.log(err); if (err.reason === ErrMsg.alreadyCompleted) { // submit the HIT @@ -46,23 +48,26 @@ const loginCallback = function(err) { } else { // Make sure to display this after client fully loads; otherwise error may // not appear. (However, log out immediately as below.) - Meteor.startup(() => bootbox.dialog({ - closeButton: false, - message: "

Unable to login:

" + err.message - })); + Meteor.startup(() => + bootbox.dialog({ + closeButton: false, + message: "

Unable to login:

" + err.message + }) + ); // TODO: make this a bit more robust // Log us out even if the resume token logged us in; copied from // https://github.com/meteor/meteor/blob/devel/packages/accounts-base/accounts_client.js#L195 Accounts.connection.setUserId(null); - return Accounts.connection.onReconnect = null; + return (Accounts.connection.onReconnect = null); } }; -const mturkLogin = args => Accounts.callLoginMethod({ - methodArguments: [ args ], - userCallback: loginCallback -}); +const mturkLogin = args => + Accounts.callLoginMethod({ + methodArguments: [args], + userCallback: loginCallback + }); let loginDialog = null; @@ -70,7 +75,9 @@ Template.tsTestingLogin.events = { "submit form"(e, tmpl) { e.preventDefault(); const batchId = tmpl.find("select[name=batch]").value; - if (!batchId) { return; } + if (!batchId) { + return; + } console.log("Trying login with testing credentials"); // Save parameters (including generated stuff) and login const loginParams = _.extend(this, { @@ -82,15 +89,15 @@ Template.tsTestingLogin.events = { mturkLogin(loginParams); if (loginDialog != null) { - loginDialog.modal('hide'); + loginDialog.modal("hide"); } - return loginDialog = null; + return (loginDialog = null); } }; // Subscribe to the list of batches only when this dialog is open Template.tsTestingLogin.rendered = function() { - return this.subHandle = Meteor.subscribe("tsLoginBatches"); + return (this.subHandle = Meteor.subscribe("tsLoginBatches")); }; Template.tsTestingLogin.destroyed = function() { @@ -98,16 +105,27 @@ Template.tsTestingLogin.destroyed = function() { }; Template.tsTestingLogin.helpers({ - batches() { return Batches.find(); }}); + batches() { + return Batches.find(); + } +}); const testLogin = function() { // FIXME hack: never run this if we are live - if (hitIsViewing) { return; } - if ((window.location.protocol === "https:") || (window !== window.parent)) { return; } + if (hitIsViewing) { + return; + } + if (window.location.protocol === "https:" || window !== window.parent) { + return; + } // Don't try logging in if we are logged in or already have parameters - if (Meteor.userId() || Session.get("_loginParams")) { return; } + if (Meteor.userId() || Session.get("_loginParams")) { + return; + } // Don't show this if we are trying to get at the admin interface - if (__guard__(__guard__(Router.current(), x1 => x1.url), x => x.indexOf("/turkserver")) === 0) { return; } + if (__guard__(__guard__(Router.current(), x1 => x1.url), x => x.indexOf("/turkserver")) === 0) { + return; + } const str = Random.id(); const data = { @@ -117,10 +135,9 @@ const testLogin = function() { }; loginDialog = TurkServer._displayModal(Template.tsTestingLogin, data, { - title: 'Select batch', + title: "Select batch", message: " " }); - }; // Remember our previous hit parameters unless they have been replaced @@ -132,7 +149,7 @@ if (params.hitId && params.assignmentId && params.workerId) { workerId: params.workerId, batchId: params.batchId, // TODO: hack to allow testing logins - test: (params.test != null) || (params.workerId.indexOf("_Worker") >= 0) + test: params.test != null || params.workerId.indexOf("_Worker") >= 0 }); Meteor._debug("Captured login params"); } @@ -141,7 +158,6 @@ if (params.hitId && params.assignmentId && params.workerId) { const loginParams = Session.get("_loginParams"); if (loginParams) { - Meteor._debug("Logging in with captured or stored parameters"); mturkLogin(loginParams); } else { @@ -162,7 +178,6 @@ TurkServer.testingLogin = function() { return mturkLogin(Session.get("_loginParams")); }; - function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== "undefined" && value !== null ? transform(value) : undefined; +} diff --git a/client/timers_client.js b/client/timers_client.js index fdec453..7ee1696 100644 --- a/client/timers_client.js +++ b/client/timers_client.js @@ -24,15 +24,22 @@ static initClass() { _currentAssignmentInstance = function() { let group; - if (TurkServer.isAdmin()) { return; } - if ((group = TurkServer.group()) == null) { return; } - return _.find(__guard__(Assignments.findOne(), x => x.instances), inst => inst.id === group); + if (TurkServer.isAdmin()) { + return; + } + if ((group = TurkServer.group()) == null) { + return; + } + return _.find( + __guard__(Assignments.findOne(), x => x.instances), + inst => inst.id === group + ); }; - + _joinedTime = (instance, serverTime) => Math.max(0, serverTime - instance.joinTime); - + _idleTime = function(instance, serverTime) { - let idleMillis = (instance.idleTime || 0); + let idleMillis = instance.idleTime || 0; // If we're idle, add the time since we went idle // TODO add a test for this part if (instance.lastIdle != null) { @@ -40,7 +47,7 @@ } return idleMillis; }; - + _disconnectedTime = function(instance, serverTime) { let discMillis = instance.disconnectedTime || 0; if (instance.lastDisconnect != null) { @@ -53,8 +60,12 @@ // Milliseconds elapsed since experiment start static elapsedTime() { let exp; - if ((exp = Experiments.findOne()) == null) { return; } - if (exp.startTime == null) { return; } + if ((exp = Experiments.findOne()) == null) { + return; + } + if (exp.startTime == null) { + return; + } return Math.max(0, TimeSync.serverTime() - exp.startTime); } @@ -63,15 +74,21 @@ // Milliseconds elapsed since this user joined the experiment instance // This is slightly different than the above static joinedTime(instance) { - if ((instance != null ? instance : (instance = _currentAssignmentInstance())) == null) { return; } + if ((instance != null ? instance : (instance = _currentAssignmentInstance())) == null) { + return; + } const serverTime = instance.leaveTime || TimeSync.serverTime(); return _joinedTime(instance, serverTime); } static remainingTime() { let exp; - if ((exp = Experiments.findOne()) == null) { return; } - if (exp.endTime == null) { return; } + if ((exp = Experiments.findOne()) == null) { + return; + } + if (exp.endTime == null) { + return; + } return Math.max(0, exp.endTime - TimeSync.serverTime()); } @@ -81,54 +98,78 @@ // Milliseconds this user has been idle in the experiment static idleTime(instance) { - if ((instance != null ? instance : (instance = _currentAssignmentInstance())) == null) { return; } + if ((instance != null ? instance : (instance = _currentAssignmentInstance())) == null) { + return; + } const serverTime = instance.leaveTime || TimeSync.serverTime(); return _idleTime(instance, serverTime); } // Milliseconds this user has been disconnected in the experiment static disconnectedTime(instance) { - if ((instance != null ? instance : (instance = _currentAssignmentInstance())) == null) { return; } + if ((instance != null ? instance : (instance = _currentAssignmentInstance())) == null) { + return; + } const serverTime = instance.leaveTime || TimeSync.serverTime(); return _disconnectedTime(instance, serverTime); } static activeTime(instance) { - if ((instance != null ? instance : (instance = _currentAssignmentInstance())) == null) { return; } + if ((instance != null ? instance : (instance = _currentAssignmentInstance())) == null) { + return; + } // Compute this using helper functions to avoid thrashing const serverTime = instance.leaveTime || TimeSync.serverTime(); - return _joinedTime(instance, serverTime) - _idleTime(instance, serverTime) - _disconnectedTime(instance, serverTime); + return ( + _joinedTime(instance, serverTime) - + _idleTime(instance, serverTime) - + _disconnectedTime(instance, serverTime) + ); } // Milliseconds elapsed since round start static roundElapsedTime() { let round; - if ((round = TurkServer.currentRound()) == null) { return; } - if (round.startTime == null) { return; } + if ((round = TurkServer.currentRound()) == null) { + return; + } + if (round.startTime == null) { + return; + } return Math.max(0, TimeSync.serverTime() - round.startTime); } // Milliseconds until end of round static roundRemainingTime() { let round; - if ((round = TurkServer.currentRound()) == null) { return; } - if (round.endTime == null) { return; } + if ((round = TurkServer.currentRound()) == null) { + return; + } + if (round.endTime == null) { + return; + } return Math.max(0, round.endTime - TimeSync.serverTime()); } // Milliseconds until start of next round, if any static breakRemainingTime() { let nextRound, round; - if ((round = TurkServer.currentRound()) == null) { return; } + if ((round = TurkServer.currentRound()) == null) { + return; + } const now = Date.now(); - if ((round.startTime <= now) && (round.endTime >= now)) { + if (round.startTime <= now && round.endTime >= now) { // if we are not at a break, return 0 return 0; } // if we are at a break, we already set next round to be active. - if ((nextRound = RoundTimers.findOne({index: round.index + 1})) == null) { return; } - if (nextRound.startTime == null) { return; } + if ((nextRound = RoundTimers.findOne({ index: round.index + 1 })) == null) { + return; + } + if (nextRound.startTime == null) { + return; + } return Math.max(0, nextRound.startTime - TimeSync.serverTime()); } }); @@ -140,14 +181,15 @@ for (var key of Object.keys(TurkServer.Timers || {})) { // camelCase the helper name var helperName = "ts" + key.charAt(0).toUpperCase() + key.slice(1); - (function() { // Bind the function to the current value inside the closure + (function() { + // Bind the function to the current value inside the closure const func = TurkServer.Timers[key]; return UI.registerHelper(helperName, function() { return TurkServer.Util.formatMillis(func.apply(this, arguments)); - }); + }); })(); } function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== "undefined" && value !== null ? transform(value) : undefined; +} diff --git a/client/ts_client.js b/client/ts_client.js index 3562c0b..a173417 100644 --- a/client/ts_client.js +++ b/client/ts_client.js @@ -13,7 +13,7 @@ this.TSConfig = new Mongo.Collection("ts.config"); TurkServer.batch = function() { let batchId; - if ((batchId = __guard__(Session.get('_loginParams'), x => x.batchId)) != null) { + if ((batchId = __guard__(Session.get("_loginParams"), x => x.batchId)) != null) { return Batches.findOne(batchId); } else { return Batches.findOne(); @@ -27,17 +27,26 @@ TurkServer.batch = function() { // TODO perhaps make a better version of this reactivity Deps.autorun(function() { const userId = Meteor.userId(); - if (!userId) { return; } - const turkserver = __guard__(Meteor.users.findOne({ - _id: userId, - "turkserver.state": { $exists: true } + if (!userId) { + return; } - , { fields: { - "turkserver.state" : 1 + const turkserver = __guard__( + Meteor.users.findOne( + { + _id: userId, + "turkserver.state": { $exists: true } + }, + { + fields: { + "turkserver.state": 1 + } + } + ), + x => x.turkserver + ); + if (!turkserver) { + return; } -} - ), x => x.turkserver); - if (!turkserver) { return; } return Session.set("turkserver.state", turkserver.state); }); @@ -46,18 +55,21 @@ Deps.autorun(() => Meteor.subscribe("tsCurrentExperiment", Partitioner.group())) // Reactive join on treatments for assignments and experiments Deps.autorun(function() { - const exp = Experiments.findOne({}, {fields: {treatments: 1}}); - if (!exp || (exp.treatments == null)) { return; } + const exp = Experiments.findOne({}, { fields: { treatments: 1 } }); + if (!exp || exp.treatments == null) { + return; + } return Meteor.subscribe("tsTreatments", exp.treatments); }); Deps.autorun(function() { - const asst = Assignments.findOne({}, {fields: {treatments: 1}}); - if (!asst || (asst.treatments == null)) { return; } + const asst = Assignments.findOne({}, { fields: { treatments: 1 } }); + if (!asst || asst.treatments == null) { + return; + } return Meteor.subscribe("tsTreatments", asst.treatments); }); - function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== "undefined" && value !== null ? transform(value) : undefined; +} diff --git a/format.sh b/format.sh new file mode 100755 index 0000000..4b8ec09 --- /dev/null +++ b/format.sh @@ -0,0 +1,2 @@ +#!/bin/sh +prettier --config .prettierrc.yml --write '**/*.js' diff --git a/lib/common.js b/lib/common.js index 6b6fcac..f279851 100644 --- a/lib/common.js +++ b/lib/common.js @@ -17,7 +17,7 @@ if (Meteor.isServer) { } TurkServer.partitionCollection(RoundTimers, { - index: {index: 1}, + index: { index: 1 }, indexOptions: { unique: 1, dropDups: true, @@ -37,8 +37,10 @@ const ErrMsg = { // authentication unexpectedBatch: "This HIT is not recognized.", batchInactive: "This task is currently not accepting new assignments.", - batchLimit: "You've attempted or completed the maximum number of HITs allowed in this group. Please return this assignment.", - simultaneousLimit: "You are already connected through another HIT, or you previously returned a HIT from this group. If you still have the HIT open, please complete that one first.", + batchLimit: + "You've attempted or completed the maximum number of HITs allowed in this group. Please return this assignment.", + simultaneousLimit: + "You are already connected through another HIT, or you previously returned a HIT from this group. If you still have the HIT open, please complete that one first.", alreadyCompleted: "You have already completed this HIT.", // operations authErr: "You are not logged in", @@ -67,36 +69,55 @@ Meteor.methods({ // Check if a particular user is an admin. // If no user is specified, attempts to check the current user. TurkServer.isAdmin = function(userId) { - if (userId == null) { userId = Meteor.userId(); } - if (!userId) { return false; } - return __guard__(Meteor.users.findOne({ - _id: userId, - "admin": { $exists: true } + if (userId == null) { + userId = Meteor.userId(); } - , { fields: { - "admin" : 1 + if (!userId) { + return false; } -} - ), x => x.admin) || false; + return ( + __guard__( + Meteor.users.findOne( + { + _id: userId, + admin: { $exists: true } + }, + { + fields: { + admin: 1 + } + } + ), + x => x.admin + ) || false + ); }; TurkServer.checkNotAdmin = function() { if (Meteor.isClient) { // Don't register reactive dependencies on userId for a client check - if (Deps.nonreactive(() => TurkServer.isAdmin())) { throw new Meteor.Error(403, ErrMsg.adminErr); } + if (Deps.nonreactive(() => TurkServer.isAdmin())) { + throw new Meteor.Error(403, ErrMsg.adminErr); + } } else { - if (TurkServer.isAdmin()) { throw new Meteor.Error(403, ErrMsg.adminErr); } + if (TurkServer.isAdmin()) { + throw new Meteor.Error(403, ErrMsg.adminErr); + } } }; TurkServer.checkAdmin = function() { if (Meteor.isClient) { - if (!Deps.nonreactive(() => TurkServer.isAdmin())) { throw new Meteor.Error(403, ErrMsg.notAdminErr); } + if (!Deps.nonreactive(() => TurkServer.isAdmin())) { + throw new Meteor.Error(403, ErrMsg.notAdminErr); + } } else { - if (!TurkServer.isAdmin()) { throw new Meteor.Error(403, ErrMsg.notAdminErr); } + if (!TurkServer.isAdmin()) { + throw new Meteor.Error(403, ErrMsg.notAdminErr); + } } }; function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== "undefined" && value !== null ? transform(value) : undefined; +} diff --git a/lib/util.js b/lib/util.js index bfaa8bf..3750df8 100644 --- a/lib/util.js +++ b/lib/util.js @@ -10,11 +10,15 @@ Server/client util files */ -if (TurkServer.Util == null) { TurkServer.Util = {}; } +if (TurkServer.Util == null) { + TurkServer.Util = {}; +} TurkServer.Util.formatMillis = function(millis) { - if (millis == null) { return; } // Can be 0 in which case we should render it - const negative = (millis < 0); + if (millis == null) { + return; + } // Can be 0 in which case we should render it + const negative = millis < 0; const diff = moment.utc(Math.abs(millis)); const time = diff.format("H:mm:ss"); const days = +diff.format("DDD") - 1; @@ -22,8 +26,7 @@ TurkServer.Util.formatMillis = function(millis) { }; TurkServer._mergeTreatments = function(arr) { - const fields = - {treatments: []}; + const fields = { treatments: [] }; arr.forEach(function(treatment) { fields.treatments.push(treatment.name); return _.extend(fields, _.omit(treatment, "_id", "name")); diff --git a/package.js b/package.js index c581000..952b94f 100644 --- a/package.js +++ b/package.js @@ -11,30 +11,25 @@ Npm.depends({ deepmerge: "0.2.7" // For merging config parameters }); -Package.onUse(function (api) { +Package.onUse(function(api) { api.versionsFrom("1.4.4.6"); // Client-only deps - api.use([ - 'session', - 'ui', - 'templating', - 'reactive-var' - ], 'client'); + api.use(["session", "ui", "templating", "reactive-var"], "client"); // Client & Server deps api.use([ - 'accounts-base', - 'accounts-ui', - 'accounts-password', // for the admin user - 'check', - 'deps', - 'ejson', - 'jquery', - 'random', - 'underscore', - 'ecmascript', - 'facts' + "accounts-base", + "accounts-ui", + "accounts-password", // for the admin user + "check", + "deps", + "ejson", + "jquery", + "random", + "underscore", + "ecmascript", + "facts" ]); api.use(["ddp", "mongo"]); // For pub/sub and collections @@ -53,124 +48,126 @@ Package.onUse(function (api) { api.use("d3js:d3@3.5.5"); api.use("mizzao:autocomplete@0.5.1"); - api.use('natestrauser:x-editable-bootstrap@1.5.2_1'); + api.use("natestrauser:x-editable-bootstrap@1.5.2_1"); // Dev packages - may be locally installed with submodule api.use("matb33:collection-hooks@0.7.15"); api.use("mizzao:partitioner@0.5.9"); - api.use('mizzao:timesync@0.3.3'); + api.use("mizzao:timesync@0.3.3"); api.use("mizzao:user-status@0.6.5"); // Shared files - api.addFiles([ - 'lib/shared.js', - 'lib/common.js', - 'lib/util.js' - ]); + api.addFiles(["lib/shared.js", "lib/common.js", "lib/util.js"]); // Server files - api.addFiles([ - 'server/config.js', - 'server/turkserver.js', - 'server/server_api.js', - 'server/mturk.js', - 'server/lobby_server.js', - 'server/batches.js', - 'server/instance.js', - 'server/logging.js', - 'server/assigners.js', - 'server/assigners_extra.js', - 'server/assignment.js', - 'server/connections.js', - 'server/timers_server.js', - 'server/accounts_mturk.js' - ], 'server'); + api.addFiles( + [ + "server/config.js", + "server/turkserver.js", + "server/server_api.js", + "server/mturk.js", + "server/lobby_server.js", + "server/batches.js", + "server/instance.js", + "server/logging.js", + "server/assigners.js", + "server/assigners_extra.js", + "server/assignment.js", + "server/connections.js", + "server/timers_server.js", + "server/accounts_mturk.js" + ], + "server" + ); // Client - api.addFiles([ - 'client/templates.html', - 'client/login.html', - 'client/client_api.js', - 'client/ts_client.css', - 'client/ts_client.js', - 'client/login.js', - 'client/logging_client.js', - 'client/timers_client.js', - 'client/helpers.js', - 'client/lobby_client.html', - 'client/lobby_client.js', - 'client/dialogs.js' - ], 'client'); + api.addFiles( + [ + "client/templates.html", + "client/login.html", + "client/client_api.js", + "client/ts_client.css", + "client/ts_client.js", + "client/login.js", + "client/logging_client.js", + "client/timers_client.js", + "client/helpers.js", + "client/lobby_client.html", + "client/lobby_client.js", + "client/dialogs.js" + ], + "client" + ); // Admin - api.addFiles([ - 'admin/admin.css', - 'admin/util.html', - 'admin/util.js', - 'admin/clientAdmin.html', - 'admin/clientAdmin.js', - 'admin/mturkAdmin.html', - 'admin/mturkAdmin.js', - 'admin/experimentAdmin.html', - 'admin/experimentAdmin.js', - 'admin/lobbyAdmin.html', - 'admin/lobbyAdmin.js' - ], 'client'); - - api.addFiles('admin/admin.js', 'server'); - - api.export(['TurkServer']); + api.addFiles( + [ + "admin/admin.css", + "admin/util.html", + "admin/util.js", + "admin/clientAdmin.html", + "admin/clientAdmin.js", + "admin/mturkAdmin.html", + "admin/mturkAdmin.js", + "admin/experimentAdmin.html", + "admin/experimentAdmin.js", + "admin/lobbyAdmin.html", + "admin/lobbyAdmin.js" + ], + "client" + ); + + api.addFiles("admin/admin.js", "server"); + + api.export(["TurkServer"]); /* Exported collections for legacy purposes TODO Direct access to these should be deprecated in the future */ - api.export(['Batches', 'Treatments', 'Experiments', 'LobbyStatus', 'Logs', 'RoundTimers']); + api.export(["Batches", "Treatments", "Experiments", "LobbyStatus", "Logs", "RoundTimers"]); - api.export(['ErrMsg', 'TestUtils'], { testOnly: true }); + api.export(["ErrMsg", "TestUtils"], { testOnly: true }); }); -Package.onTest(function (api) { +Package.onTest(function(api) { api.use([ - 'accounts-base', - 'accounts-password', - 'check', - 'deps', - 'mongo', - 'random', - 'ui', - 'underscore' + "accounts-base", + "accounts-password", + "check", + "deps", + "mongo", + "random", + "ui", + "underscore" ]); - api.use([ - 'tinytest', - 'test-helpers' - ]); + api.use(["tinytest", "test-helpers"]); - api.use('session', 'client'); + api.use("session", "client"); - api.use('iron:router'); // Needed so we can un-configure the router - api.use('mizzao:partitioner'); - api.use('mizzao:timesync'); + api.use("iron:router"); // Needed so we can un-configure the router + api.use("mizzao:partitioner"); + api.use("mizzao:timesync"); api.use("mizzao:turkserver"); // This package! api.addFiles("tests/display_fix.css"); - api.addFiles('tests/utils.js'); // Deletes users so do it before insecure login + api.addFiles("tests/utils.js"); // Deletes users so do it before insecure login api.addFiles("tests/insecure_login.js"); - api.addFiles('tests/lobby_tests.js'); - api.addFiles('tests/admin_tests.js', 'server'); - api.addFiles('tests/auth_tests.js', 'server'); - api.addFiles('tests/connection_tests.js', 'server'); - api.addFiles('tests/experiment_tests.js', 'server'); - api.addFiles('tests/experiment_client_tests.js'); - api.addFiles('tests/timer_tests.js', 'server'); - api.addFiles('tests/logging_tests.js'); + api.addFiles("tests/lobby_tests.js"); + api.addFiles("tests/admin_tests.js", "server"); + api.addFiles("tests/auth_tests.js", "server"); + api.addFiles("tests/connection_tests.js", "server"); + api.addFiles("tests/experiment_tests.js", "server"); + api.addFiles("tests/experiment_client_tests.js"); + api.addFiles("tests/timer_tests.js", "server"); + api.addFiles("tests/logging_tests.js"); // This goes after experiment tests, so we can be sure that assigning works - api.addFiles('tests/assigner_tests.js', 'server'); + api.addFiles("tests/assigner_tests.js", "server"); // This runs after user is logged in, as it requires a userId - api.addFiles('tests/helper_tests.js'); + api.addFiles("tests/helper_tests.js"); }); diff --git a/server/accounts_mturk.js b/server/accounts_mturk.js index 3f64dd4..6c44fef 100644 --- a/server/accounts_mturk.js +++ b/server/accounts_mturk.js @@ -11,23 +11,28 @@ for users who are not currently assigned to a HIT. */ Accounts.validateLoginAttempt(function(info) { - if (info.user != null ? info.user.admin : undefined) { return true; } // Always allow admin to login + if (info.user != null ? info.user.admin : undefined) { + return true; + } // Always allow admin to login // If resuming, is the worker currently assigned to a HIT? // TODO add a test for this if (info.methodArguments[0].resume != null) { - if (!(info.user != null ? info.user.workerId : undefined) || !Assignments.findOne({ - workerId: info.user.workerId, - status: "assigned" - })) { + if ( + !(info.user != null ? info.user.workerId : undefined) || + !Assignments.findOne({ + workerId: info.user.workerId, + status: "assigned" + }) + ) { throw new Meteor.Error(403, "Your HIT session has expired."); } } // TODO Does the worker have this open in another window? If so, reject the login. // This is a bit fail-prone due to leaking sessions across HCR, so take it out. -// if info.user? and UserStatus.connections.findOne(userId: info.user._id) -// throw new Meteor.Error(403, "You already have this open in another window. Complete it there.") + // if info.user? and UserStatus.connections.findOne(userId: info.user._id) + // throw new Meteor.Error(403, "You already have this open in another window. Complete it there.") return true; }); @@ -42,19 +47,25 @@ const authenticateWorker = function(loginRequest) { // check if batchId is correct except for testing logins if (!loginRequest.test && !TurkServer.config.hits.acceptUnknownHits) { const hit = HITs.findOne({ - HITId: hitId}); + HITId: hitId + }); const hitType = HITTypes.findOne({ - HITTypeId: hit.HITTypeId}); - if (batchId !== hitType.batchId) { throw new Meteor.Error(403, ErrMsg.unexpectedBatch); } + HITTypeId: hit.HITTypeId + }); + if (batchId !== hitType.batchId) { + throw new Meteor.Error(403, ErrMsg.unexpectedBatch); + } } // Has this worker already completed the HIT? - if (Assignments.findOne({ - hitId, - assignmentId, - workerId, - status: "completed" - })) { + if ( + Assignments.findOne({ + hitId, + assignmentId, + workerId, + status: "completed" + }) + ) { // makes the client auto-submit with this error throw new Meteor.Error(403, ErrMsg.alreadyCompleted); } @@ -84,15 +95,17 @@ const authenticateWorker = function(loginRequest) { const batch = Batches.findOne(batchId); // Only active batches accept new HITs - if ((batchId != null) && !(batch != null ? batch.active : undefined)) { + if (batchId != null && !(batch != null ? batch.active : undefined)) { throw new Meteor.Error(403, ErrMsg.batchInactive); } // Limits - simultaneously accepted HITs - if (Assignments.find({ - workerId, - status: { $nin: [ "completed", "returned" ] } - }).count() >= TurkServer.config.experiment.limit.simultaneous) { + if ( + Assignments.find({ + workerId, + status: { $nin: ["completed", "returned"] } + }).count() >= TurkServer.config.experiment.limit.simultaneous + ) { throw new Meteor.Error(403, ErrMsg.simultaneousLimit); } @@ -102,7 +115,9 @@ const authenticateWorker = function(loginRequest) { batchId }; - if (batch.allowReturns) { predicate.status = { $ne: "returned" }; } + if (batch.allowReturns) { + predicate.status = { $ne: "returned" }; + } if (Assignments.find(predicate).count() >= TurkServer.config.experiment.limit.batch) { throw new Meteor.Error(403, ErrMsg.batchLimit); @@ -123,18 +138,20 @@ const authenticateWorker = function(loginRequest) { Accounts.registerLoginHandler("mturk", function(loginRequest) { // Don't handle unless we have an mturk login let userId; - if (!loginRequest.hitId || !loginRequest.assignmentId || !loginRequest.workerId) { return; } + if (!loginRequest.hitId || !loginRequest.assignmentId || !loginRequest.workerId) { + return; + } // At some point this became processed as part of a method call // (DDP._CurrentInvocation.get() is defined), so we need the direct or this // would fail with a partitioner error. const user = Meteor.users.direct.findOne({ - workerId: loginRequest.workerId}); + workerId: loginRequest.workerId + }); if (!user) { // Use the provided method of creating users - userId = Accounts.insertUserDoc({}, - {workerId: loginRequest.workerId}); + userId = Accounts.insertUserDoc({}, { workerId: loginRequest.workerId }); } else { userId = user._id; } @@ -150,7 +167,7 @@ Accounts.registerLoginHandler("mturk", function(loginRequest) { // So we'll need to aggressively prune logins when a HIT is submitted, instead. return { - userId, + userId }; }); diff --git a/server/assigners.js b/server/assigners.js index 05dae1e..bf2410f 100644 --- a/server/assigners.js +++ b/server/assigners.js @@ -7,7 +7,6 @@ * @instancename assigner */ class Assigner { - /** * @summary Initialize this assigner for a particular batch. This should set up the assigner's internal state, including reconstructing state after a server restart. * @param {String} batch The {@link TurkServer.Batch} object to initialize this assigner on. @@ -30,7 +29,7 @@ class Assigner { */ assignToNewInstance(assts, treatments) { this.lobby.pluckUsers(_.pluck(assts, "userId")); - + const instance = this.batch.createInstance(treatments); for (let asst of assts) { instance.addAssignment(asst); @@ -43,9 +42,7 @@ class Assigner { * @summary Function that is called when a user enters the lobby, either from the initial entry or after returning from a world. * @param asst The user assignment {@link TurkServer.Assignment} (session) that just entered the lobby. */ - userJoined(asst) { - - } + userJoined(asst) {} /** * @summary Function that is called when the status of a user in the lobby changes (such as the user changing from not ready to ready.) @@ -53,17 +50,13 @@ class Assigner { * changed status. * @param newStatus */ - userStatusChanged(asst, newStatus) { - - } + userStatusChanged(asst, newStatus) {} /** * @summary Function that is called when a user disconnects from the lobby. This is only triggered by users losing connectivity, not from being assigned to a new instance). * @param asst The user assignment {@link TurkServer.Assignment} that departed. */ - userLeft(asst) { - - } + userLeft(asst) {} } TurkServer.Assigner = Assigner; @@ -83,13 +76,12 @@ TurkServer.Assigners = {}; * @alias TestAssigner */ TurkServer.Assigners.TestAssigner = class extends TurkServer.Assigner { - initialize(batch) { super.initialize(batch); - const exp = Experiments.findOne({batchId: this.batch.batchId}); + const exp = Experiments.findOne({ batchId: this.batch.batchId }); // Take any experiment from this batch, creating it if it doesn't exist - if ( exp != null) { + if (exp != null) { this.instance = TurkServer.Instance.getInstance(exp._id); } else { // TODO: refactor once batch treatments are separated from instance @@ -108,7 +100,6 @@ TurkServer.Assigners.TestAssigner = class extends TurkServer.Assigner { this.lobby.pluckUsers([asst.userId]); } } - }; /** @@ -119,7 +110,6 @@ TurkServer.Assigners.TestAssigner = class extends TurkServer.Assigner { * @alias SimpleAssigner */ TurkServer.Assigners.SimpleAssigner = class extends TurkServer.Assigner { - userJoined(asst) { if (asst.getInstances().length > 0) { this.lobby.pluckUsers([asst.userId]); @@ -129,7 +119,6 @@ TurkServer.Assigners.SimpleAssigner = class extends TurkServer.Assigner { this.assignToNewInstance([asst], treatments); } } - }; /************************************************************************ @@ -169,7 +158,7 @@ TurkServer.Assigners.RoundRobinAssigner = class extends TurkServer.Assigner { this.instanceIds = instanceIds; // Create instances if they don't exist - for( let instanceId of this.instanceIds ) { + for (let instanceId of this.instanceIds) { let instance; try { @@ -215,5 +204,4 @@ TurkServer.Assigners.SequentialAssigner = class extends TurkServer.Assigner { this.lobby.pluckUsers([asst.userId]); this.instance.addAssignment(asst); } - }; diff --git a/server/assigners_extra.js b/server/assigners_extra.js index fbe74af..3470015 100644 --- a/server/assigners_extra.js +++ b/server/assigners_extra.js @@ -9,7 +9,6 @@ * An event on the lobby is used to trigger the group. */ TurkServer.Assigners.TutorialGroupAssigner = class extends TurkServer.Assigner { - constructor(tutorialTreatments, groupTreatments, autoAssign = false) { super(); @@ -27,13 +26,16 @@ TurkServer.Assigners.TutorialGroupAssigner = class extends TurkServer.Assigner { super.initialize(batch); // if experiment was already created, and in progress store it - const exp = Experiments.findOne({ - batchId: this.batch.batchId, - treatments: { $all: this.groupTreatments }, - endTime: { $exists: false } - }, { - sort: { startTime: -1 } - }); + const exp = Experiments.findOne( + { + batchId: this.batch.batchId, + treatments: { $all: this.groupTreatments }, + endTime: { $exists: false } + }, + { + sort: { startTime: -1 } + } + ); if (exp != null) { this.instance = TurkServer.Instance.getInstance(exp._id); @@ -66,7 +68,7 @@ TurkServer.Assigners.TutorialGroupAssigner = class extends TurkServer.Assigner { return asst.getInstances().length === 1; }); - for( let asst of assts ) { + for (let asst of assts) { this.lobby.pluckUsers([asst.userId]); this.instance.addAssignment(asst); } @@ -85,7 +87,6 @@ TurkServer.Assigners.TutorialGroupAssigner = class extends TurkServer.Assigner { this.lobby.pluckUsers([asst.userId]); this.instance.addAssignment(asst); } - } }; @@ -109,266 +110,283 @@ function ensureGroupTreatments(sizeArray) { This was created for executing the crisis mapping experiment. */ -TurkServer.Assigners.TutorialRandomizedGroupAssigner = - class TutorialRandomizedGroupAssigner extends TurkServer.Assigner { - - static generateConfig(sizeArray, otherTreatments) { - ensureGroupTreatments(sizeArray); - - const config = []; +TurkServer.Assigners.TutorialRandomizedGroupAssigner = class TutorialRandomizedGroupAssigner extends TurkServer.Assigner { + static generateConfig(sizeArray, otherTreatments) { + ensureGroupTreatments(sizeArray); - for (let size of sizeArray) { - config.push({ - size: size, - treatments: ["group_" + size].concat(otherTreatments) - }); - } + const config = []; - // Create a buffer group for everyone else + for (let size of sizeArray) { config.push({ - treatments: otherTreatments + size: size, + treatments: ["group_" + size].concat(otherTreatments) }); - - return config; } - constructor(tutorialTreatments, groupTreatments, groupArray) { - super(); - this.tutorialTreatments = tutorialTreatments; - this.groupTreatments = groupTreatments; - this.groupArray = groupArray; - } + // Create a buffer group for everyone else + config.push({ + treatments: otherTreatments + }); - initialize(batch) { - super.initialize(batch); + return config; + } - this.configure(); - this.lobby.events.on("setup-instances", this.setup.bind(this)); - this.lobby.events.on("configure", this.configure.bind(this)); - this.lobby.events.on("auto-assign", this.assignAll.bind(this)); - } + constructor(tutorialTreatments, groupTreatments, groupArray) { + super(); + this.tutorialTreatments = tutorialTreatments; + this.groupTreatments = groupTreatments; + this.groupArray = groupArray; + } - // If pre-allocated instances don't exist, create and initialize them - setup(lookBackHours = 6) { - console.log("Creating new set of instances for randomized groups"); + initialize(batch) { + super.initialize(batch); - const existing = Experiments.find({ - batchId: this.batch.batchId, - treatments: { $nin: this.tutorialTreatments }, - $or: [ - { startTime: { $gte: new Date(Date.now() - lookBackHours * 3600 * 1000) } }, - { startTime: null } - ] - }).fetch(); + this.configure(); + this.lobby.events.on("setup-instances", this.setup.bind(this)); + this.lobby.events.on("configure", this.configure.bind(this)); + this.lobby.events.on("auto-assign", this.assignAll.bind(this)); + } - // Reuse buffer instance if it already exists - if (existing.length > 0 && _.any(existing, (exp) => exp.startTime != null )) { - console.log("Not creating new instances as recently started ones already exist"); - return; - } + // If pre-allocated instances don't exist, create and initialize them + setup(lookBackHours = 6) { + console.log("Creating new set of instances for randomized groups"); - this.groupConfig = TutorialRandomizedGroupAssigner.generateConfig(this.groupArray, this.groupTreatments); + const existing = Experiments.find({ + batchId: this.batch.batchId, + treatments: { $nin: this.tutorialTreatments }, + $or: [ + { + startTime: { + $gte: new Date(Date.now() - lookBackHours * 3600 * 1000) + } + }, + { startTime: null } + ] + }).fetch(); - if (existing.length === this.groupConfig.length) { - console.log("Not creating new instances as we already have the expected number"); - return; - } + // Reuse buffer instance if it already exists + if (existing.length > 0 && _.any(existing, exp => exp.startTime != null)) { + console.log("Not creating new instances as recently started ones already exist"); + return; + } - // Some existing instances exist. Count how many are available to reuse - const reusable = {}; + this.groupConfig = TutorialRandomizedGroupAssigner.generateConfig( + this.groupArray, + this.groupTreatments + ); - for ( let exp of existing ) { - let key; + if (existing.length === this.groupConfig.length) { + console.log("Not creating new instances as we already have the expected number"); + return; + } - if (exp.treatments[0].indexOf("group_") >= 0) { - key = parseInt(exp.treatments[0].substring(6)); - } else { - key = "buffer"; - } - console.log("Will reuse one existing instance with " + exp.treatments); + // Some existing instances exist. Count how many are available to reuse + const reusable = {}; - if (exp.endTime != null) { - Experiments.update(exp._id, { - $unset: { endTime: null } - }); - console.log("Reset an unused terminated instance: " + exp._id); - } + for (let exp of existing) { + let key; - if (reusable[key] == null) { reusable[key] = 0; } - reusable[key]++; + if (exp.treatments[0].indexOf("group_") >= 0) { + key = parseInt(exp.treatments[0].substring(6)); + } else { + key = "buffer"; } + console.log("Will reuse one existing instance with " + exp.treatments); - // create and setup instances - for (let config of this.groupConfig ) { - // Skip creating reusable instances - let key = config.size || "buffer"; - if ((reusable[key] != null) && (reusable[key] > 0)) { - console.log("Skipping creating one group of " + key); - reusable[key]--; - continue; - } - - const instance = this.batch.createInstance(config.treatments); - instance.setup(); + if (exp.endTime != null) { + Experiments.update(exp._id, { + $unset: { endTime: null } + }); + console.log("Reset an unused terminated instance: " + exp._id); } - // Configure randomization with these groups - this.configure(undefined, lookBackHours); + if (reusable[key] == null) { + reusable[key] = 0; + } + reusable[key]++; } - // TODO remove the restriction that groupArray has to be passed in sorted - configure(groupArray, lookBackHours = 6) { - if (groupArray != null) { - this.groupArray = groupArray; - console.log("Configuring randomized group assigner with", this.groupArray); - } else { - console.log("Initialization of randomized group assigner with", this.groupArray); + // create and setup instances + for (let config of this.groupConfig) { + // Skip creating reusable instances + let key = config.size || "buffer"; + if (reusable[key] != null && reusable[key] > 0) { + console.log("Skipping creating one group of " + key); + reusable[key]--; + continue; } - this.groupConfig = TutorialRandomizedGroupAssigner.generateConfig(this.groupArray, this.groupTreatments); + const instance = this.batch.createInstance(config.treatments); + instance.setup(); + } + + // Configure randomization with these groups + this.configure(undefined, lookBackHours); + } + + // TODO remove the restriction that groupArray has to be passed in sorted + configure(groupArray, lookBackHours = 6) { + if (groupArray != null) { + this.groupArray = groupArray; + console.log("Configuring randomized group assigner with", this.groupArray); + } else { + console.log("Initialization of randomized group assigner with", this.groupArray); + } + + this.groupConfig = TutorialRandomizedGroupAssigner.generateConfig( + this.groupArray, + this.groupTreatments + ); - // Check if existing created instances exist - const existing = Experiments.find({ + // Check if existing created instances exist + const existing = Experiments.find( + { batchId: this.batch.batchId, treatments: { $nin: this.tutorialTreatments }, $or: [ - { startTime: { $gte: new Date(Date.now() - lookBackHours * 3600 * 1000) } }, + { + startTime: { + $gte: new Date(Date.now() - lookBackHours * 3600 * 1000) + } + }, { startTime: null } ] - }, { + }, + { transform: function(exp) { exp.treatmentData = TurkServer.Instance.getInstance(exp._id).treatment(); return exp; } - }).fetch(); - - if (existing.length < this.groupConfig.length) { - console.log("Not setting up randomization: " + existing.length + " existing groups"); - return; } + ).fetch(); - // Sort existing experiments by smallest groups first for matching purposes. - // The buffer group goes to the end. - existing.sort(function(a, b) { - if (a.treatmentData.groupSize == null) { - // b comes first - return 1; - } else if (b.treatmentData.groupSize == null) { - // a comes first - return -1; - } else { - return a.treatmentData.groupSize - b.treatmentData.groupSize; - } - }); + if (existing.length < this.groupConfig.length) { + console.log("Not setting up randomization: " + existing.length + " existing groups"); + return; + } - const availableSlots = []; + // Sort existing experiments by smallest groups first for matching purposes. + // The buffer group goes to the end. + existing.sort(function(a, b) { + if (a.treatmentData.groupSize == null) { + // b comes first + return 1; + } else if (b.treatmentData.groupSize == null) { + // a comes first + return -1; + } else { + return a.treatmentData.groupSize - b.treatmentData.groupSize; + } + }); - // Compute remaining slots on existing groups - for( let exp of existing ) { - const filled = exp.users && exp.users.length || 0; + const availableSlots = []; - if (exp.treatmentData.groupSize == null) { - console.log(`${exp._id} (buffer) has ${filled} users`); - this.bufferInstanceId = exp._id; - continue; - } + // Compute remaining slots on existing groups + for (let exp of existing) { + const filled = (exp.users && exp.users.length) || 0; - const target = exp.treatmentData.groupSize; - // In case some bug overfilled it - const remaining = Math.max(0, target - filled); + if (exp.treatmentData.groupSize == null) { + console.log(`${exp._id} (buffer) has ${filled} users`); + this.bufferInstanceId = exp._id; + continue; + } - console.log(`${exp._id} has ${remaining} slots left (${filled}/${target})`); + const target = exp.treatmentData.groupSize; + // In case some bug overfilled it + const remaining = Math.max(0, target - filled); - for (let x = 0; x < remaining; x++) { - availableSlots.push(exp._id); - } + console.log(`${exp._id} has ${remaining} slots left (${filled}/${target})`); - if (filled > 0) { - this.autoAssign = true; - } + for (let x = 0; x < remaining; x++) { + availableSlots.push(exp._id); } - if (this.autoAssign) { - console.log("Enabled auto-assign as instances currently have users"); + if (filled > 0) { + this.autoAssign = true; } - - this.instanceSlots = _.shuffle(availableSlots); - this.instanceSlotIndex = 0; - - console.log(this.instanceSlots.length + " randomization slots remaining"); } - userJoined(asst) { - const instances = asst.getInstances(); - if (instances.length === 0) { - // This function automatically removes users from the lobby - this.assignToNewInstance([asst], this.tutorialTreatments); - } else if (instances.length === 2) { - this.lobby.pluckUsers([asst.userId]); - asst.showExitSurvey(); - } else if (this.autoAssign) { - // Put me in, coach! - this.assignNext(asst); - } - // Otherwise, wait for auto-assignment event + if (this.autoAssign) { + console.log("Enabled auto-assign as instances currently have users"); } - // Randomly assign all users in the lobby who have done the tutorial - assignAll() { - if (this.instanceSlots == null) { - console.log("Can't auto-assign as we haven't been set up yet"); - return; - } - - const currentAssignments = this.lobby.getAssignments(); + this.instanceSlots = _.shuffle(availableSlots); + this.instanceSlotIndex = 0; - // Auto assign future users that join after this point - // We can't put this before getting current assignments, - // or some people might get double assigned, with - // "already in a group" errors. - // TODO this should be theoretically right after grabbing LobbyStatus but - // before populating assignments. - this.autoAssign = true; + console.log(this.instanceSlots.length + " randomization slots remaining"); + } - const assts = _.filter(currentAssignments, function(asst) { - return asst.getInstances().length === 1; - }); + userJoined(asst) { + const instances = asst.getInstances(); + if (instances.length === 0) { + // This function automatically removes users from the lobby + this.assignToNewInstance([asst], this.tutorialTreatments); + } else if (instances.length === 2) { + this.lobby.pluckUsers([asst.userId]); + asst.showExitSurvey(); + } else if (this.autoAssign) { + // Put me in, coach! + this.assignNext(asst); + } + // Otherwise, wait for auto-assignment event + } - for( let asst of assts ) { - this.assignNext(asst); - } + // Randomly assign all users in the lobby who have done the tutorial + assignAll() { + if (this.instanceSlots == null) { + console.log("Can't auto-assign as we haven't been set up yet"); + return; } - assignNext(asst) { - if (this.instanceSlotIndex >= this.instanceSlots.length) { - const bufferInstance = TurkServer.Instance.getInstance(this.bufferInstanceId); + const currentAssignments = this.lobby.getAssignments(); - if (bufferInstance.isEnded()) { - console.log("Not assigning " + asst.asstId + " as buffer has ended"); - return; - } + // Auto assign future users that join after this point + // We can't put this before getting current assignments, + // or some people might get double assigned, with + // "already in a group" errors. + // TODO this should be theoretically right after grabbing LobbyStatus but + // before populating assignments. + this.autoAssign = true; - this.lobby.pluckUsers([asst.userId]); - bufferInstance.addAssignment(asst); - return; - } + const assts = _.filter(currentAssignments, function(asst) { + return asst.getInstances().length === 1; + }); - const nextInstId = this.instanceSlots[this.instanceSlotIndex]; - this.instanceSlotIndex++; + for (let asst of assts) { + this.assignNext(asst); + } + } - const instance = TurkServer.Instance.getInstance(nextInstId); + assignNext(asst) { + if (this.instanceSlotIndex >= this.instanceSlots.length) { + const bufferInstance = TurkServer.Instance.getInstance(this.bufferInstanceId); - if (instance.isEnded()) { - console.log("Skipping assignment to slot for ended instance " + instance.groupId); - // Recursively try to assign to the next slot - this.assignNext(asst); + if (bufferInstance.isEnded()) { + console.log("Not assigning " + asst.asstId + " as buffer has ended"); return; } this.lobby.pluckUsers([asst.userId]); - instance.addAssignment(asst); + bufferInstance.addAssignment(asst); + return; } - }; + + const nextInstId = this.instanceSlots[this.instanceSlotIndex]; + this.instanceSlotIndex++; + + const instance = TurkServer.Instance.getInstance(nextInstId); + + if (instance.isEnded()) { + console.log("Skipping assignment to slot for ended instance " + instance.groupId); + // Recursively try to assign to the next slot + this.assignNext(asst); + return; + } + + this.lobby.pluckUsers([asst.userId]); + instance.addAssignment(asst); + } +}; /* Assign people to a tutorial treatment and then sequentially to different sized @@ -380,7 +398,6 @@ TurkServer.Assigners.TutorialRandomizedGroupAssigner = After the last group is filled, there is no more assignment. */ TurkServer.Assigners.TutorialMultiGroupAssigner = class TutorialMultiGroupAssigner extends TurkServer.Assigner { - static generateConfig(sizeArray, otherTreatments) { ensureGroupTreatments(sizeArray); @@ -421,7 +438,6 @@ TurkServer.Assigners.TutorialMultiGroupAssigner = class TutorialMultiGroupAssign } configure(groupArray, lookBackHours = 6) { - if (groupArray) { this.groupArray = groupArray; this.stopped = false; @@ -430,7 +446,10 @@ TurkServer.Assigners.TutorialMultiGroupAssigner = class TutorialMultiGroupAssign console.log("Initial setup of multi-group assigner with", this.groupArray); } - this.groupConfig = TutorialMultiGroupAssigner.generateConfig(this.groupArray, this.groupTreatments); + this.groupConfig = TutorialMultiGroupAssigner.generateConfig( + this.groupArray, + this.groupTreatments + ); // If we resurrected in the middle of a server restart, pick up where we // left off. @@ -445,26 +464,33 @@ TurkServer.Assigners.TutorialMultiGroupAssigner = class TutorialMultiGroupAssign this.currentGroup = -1; // i.e. before the start of the array this.currentFilled = 0; - const existing = Experiments.find({ - batchId: this.batch.batchId, - treatments: { $nin: this.tutorialTreatments }, - startTime: { $gte: new Date(Date.now() - lookBackHours * 3600 * 1000) } - }, { - sort: { startTime: 1 } - }).fetch(); + const existing = Experiments.find( + { + batchId: this.batch.batchId, + treatments: { $nin: this.tutorialTreatments }, + startTime: { $gte: new Date(Date.now() - lookBackHours * 3600 * 1000) } + }, + { + sort: { startTime: 1 } + } + ).fetch(); const results = []; - for( let i = 0; i < existing.length; i++ ) { + for (let i = 0; i < existing.length; i++) { let exp = existing[i]; - let count = exp.users && exp.users.length || 0; + let count = (exp.users && exp.users.length) || 0; let target = this.groupConfig[i].size; if (count === target) { console.log("Group of size " + target + " already filled in " + exp._id); this.currentGroup = i; this.currentFilled = count; - results.push(this.currentInstance = TurkServer.Instance.getInstance(exp._id)); - } else if (count > target || i !== existing.length - 1 || !_.isEqual(exp.treatments, this.groupConfig[i].treatments)) { + results.push((this.currentInstance = TurkServer.Instance.getInstance(exp._id))); + } else if ( + count > target || + i !== existing.length - 1 || + !_.isEqual(exp.treatments, this.groupConfig[i].treatments) + ) { // Group sizes either don't match or this isn't the last one console.log("Unable to match with existing groups, starting over"); this.currentInstance = null; @@ -475,7 +501,15 @@ TurkServer.Assigners.TutorialMultiGroupAssigner = class TutorialMultiGroupAssign this.currentGroup = i; this.currentFilled = count; this.currentInstance = TurkServer.Instance.getInstance(exp._id); - console.log("Initializing multi-group assigner to group " + this.currentGroup + " (" + this.currentFilled + "/" + target + ")"); + console.log( + "Initializing multi-group assigner to group " + + this.currentGroup + + " (" + + this.currentFilled + + "/" + + target + + ")" + ); break; // We set the counter to the last assigned group. } } @@ -506,8 +540,7 @@ TurkServer.Assigners.TutorialMultiGroupAssigner = class TutorialMultiGroupAssign if (this.stopped) return; // Check if the last group has already been stopped. - if (this.currentGroup === this.groupConfig.length - 1 && - this.currentInstance .isEnded()) { + if (this.currentGroup === this.groupConfig.length - 1 && this.currentInstance.isEnded()) { this.stopped = true; console.log("Final group has finished, stopping automatic multi-group assignment"); return; @@ -520,7 +553,7 @@ TurkServer.Assigners.TutorialMultiGroupAssigner = class TutorialMultiGroupAssign } // It's imperative we do not do any yielding operations while updating counters - if ((this.currentInstance == null) || this.currentGroupFilled()) { + if (this.currentInstance == null || this.currentGroupFilled()) { const newGroup = this.currentGroup + 1; const treatments = this.groupConfig[newGroup].treatments; @@ -529,7 +562,9 @@ TurkServer.Assigners.TutorialMultiGroupAssigner = class TutorialMultiGroupAssign // Update group counters only once, if we are the first fiber to arrive here if (this.currentGroup === newGroup) { // New group already created. Try again on the next tick - Meteor.defer(() => { this.assignNext(asst); }); + Meteor.defer(() => { + this.assignNext(asst); + }); return; } else { // First to return from create instance. Put the user in this instance. diff --git a/server/assignment.js b/server/assignment.js index 6579b39..65fe291 100644 --- a/server/assignment.js +++ b/server/assignment.js @@ -6,11 +6,11 @@ const _userAssignments = {}; // XXX the first query might be a little slow due to not having an index, // but it will run quickly for subsequent live updates. -Assignments.find({status: "assigned"}, {fields: {workerId: 1}}).observe({ - removed: function(asstDoc) { - const user = Meteor.users.findOne({workerId: asstDoc.workerId}); - if( user != null ) delete _userAssignments[user._id]; - } +Assignments.find({ status: "assigned" }, { fields: { workerId: 1 } }).observe({ + removed: function(asstDoc) { + const user = Meteor.users.findOne({ workerId: asstDoc.workerId }); + if (user != null) delete _userAssignments[user._id]; + } }); /** @@ -22,10 +22,9 @@ Assignments.find({status: "assigned"}, {fields: {workerId: 1}}).observe({ * @instancename assignment */ class Assignment { - static createAssignment(data) { const asstId = Assignments.insert(data); - return _assignments[asstId] = new Assignment(asstId, data); + return (_assignments[asstId] = new Assignment(asstId, data)); } /** @@ -38,7 +37,7 @@ class Assignment { check(asstId, String); let asst = _assignments[asstId]; - if( asst != null ) return asst; + if (asst != null) return asst; const data = Assignments.findOne(asstId); if (data == null) throw new Error(`Assignment ${asstId} doesn't exist`); @@ -63,11 +62,11 @@ class Assignment { // Check for cached assignment let asst = _userAssignments[userId]; - if (asst != null ) return asst; + if (asst != null) return asst; let user = Meteor.users.findOne(userId); - if( user == null || user.workerId == null ) return; + if (user == null || user.workerId == null) return; let asstData = Assignments.findOne({ workerId: user.workerId, @@ -76,7 +75,7 @@ class Assignment { if (asstData != null) { // Cache assignment and return - return _userAssignments[userId] = Assignment.getAssignment(asstData._id); + return (_userAssignments[userId] = Assignment.getAssignment(asstData._id)); } // return null } @@ -89,8 +88,7 @@ class Assignment { let userId = null; try { userId = Meteor.userId(); - } - catch (e) { + } catch (e) { // We aren't in a method, so Meteor throws this error: // "Error: Meteor.userId can only be invoked in method calls. Use this.userId in publish functions." // Note that isn't just publish functions, but any server code not @@ -105,16 +103,11 @@ class Assignment { constructor(asstId, props) { check(asstId, String); - if ( _assignments[asstId] != null ) { + if (_assignments[asstId] != null) { throw new Error(`Assignment ${asstId} already exists; use getAssignment`); } - let { - batchId, - hitId, - assignmentId, - workerId - } = props || Assignments.findOne(asstId); + let { batchId, hitId, assignmentId, workerId } = props || Assignments.findOne(asstId); check(batchId, String); check(hitId, String); @@ -171,12 +164,11 @@ class Assignment { */ if (_.isArray(names)) { Assignments.update(this.asstId, { - $addToSet: { treatments: { $each: names } } + $addToSet: { treatments: { $each: names } } }); - } - else { + } else { Assignments.update(this.asstId, { - $addToSet: { treatments: names } + $addToSet: { treatments: names } }); } } @@ -260,7 +252,7 @@ class Assignment { * @returns {Number} The current bonus payment. */ getPayment() { - return Assignments.findOne(this.asstId).bonusPayment || 0; + return Assignments.findOne(this.asstId).bonusPayment || 0; } /** @@ -279,8 +271,7 @@ class Assignment { bonusPayment: amount } }; - } - else { + } else { modifier = { $unset: { bonusPayment: null @@ -288,10 +279,13 @@ class Assignment { }; } - const update = Assignments.update({ - _id: this.asstId, - bonusPaid: null - }, modifier); + const update = Assignments.update( + { + _id: this.asstId, + bonusPaid: null + }, + modifier + ); if (update === 0) { throw new Error("Can't modify a bonus that was already paid"); @@ -305,14 +299,17 @@ class Assignment { addPayment(amount) { check(amount, Number); - const update = Assignments.update({ - _id: this.asstId, - bonusPaid: null - }, { - $inc: { - bonusPayment: amount + const update = Assignments.update( + { + _id: this.asstId, + bonusPaid: null + }, + { + $inc: { + bonusPayment: amount + } } - }); + ); if (update === 0) { throw new Error("Can't modify a bonus that was already paid"); @@ -326,12 +323,12 @@ class Assignment { refreshStatus() { // Since MTurk AssignmentIds may be re-used, it's important we only query // for completed assignments. - if ( !this.isCompleted() ) { + if (!this.isCompleted()) { throw new Error("Assignment not completed"); } // Just a warning for running in testing mode. - if ( this.assignmentId.endsWith("_Asst") ) { + if (this.assignmentId.endsWith("_Asst")) { throw new Meteor.Error(403, "This is a fake test assignment that does not exist on MTurk."); } @@ -345,18 +342,18 @@ class Assignment { // XXX this is a bit hacky and will break if the exact error message changes // Moreover, it will remove assignment records if run *LONG AFTER* // MTurk no longer has kept track of an assignment. - if ( e.toString().indexOf("does not exist") >= 0 ) { - Meteor._debug(`${this.asstId} seems to have been returned on MTurk.`); + if (e.toString().indexOf("does not exist") >= 0) { + Meteor._debug(`${this.asstId} seems to have been returned on MTurk.`); this.setReturned(); return; } - - throw new Meteor.Error(500, e.toString()); + + throw new Meteor.Error(500, e.toString()); } - + // XXX Just in case, check that it's actually the same worker here, // and not a reassignment to someone else. - if ( this.workerId !== asstData.WorkerId ) { + if (this.workerId !== asstData.WorkerId) { throw new Error("Worker ID doesn't match"); } @@ -370,7 +367,7 @@ class Assignment { } _checkSubmittedStatus() { - if ( !this.isCompleted() ) { + if (!this.isCompleted()) { throw new Error("Assignment not completed"); } @@ -506,7 +503,7 @@ class Assignment { // Is the worker reconnecting to an exit survey? let user = Meteor.users.findOne(this.userId); let state = user && user.turkserver && user.turkserver.state; - if ( state === "exitsurvey") { + if (state === "exitsurvey") { Meteor._debug(`${this.userId} is reconnecting to the exit survey`); } @@ -545,11 +542,11 @@ class Assignment { } _leaveInstance(instanceId) { - var exp = Experiments.findOne({_id: instanceId}); + var exp = Experiments.findOne({ _id: instanceId }); // if experiment has ended, use the end time as the user's leave time // else, if experiment is ongoing, use current time - const leaveTime = exp.endTime || new Date; + const leaveTime = exp.endTime || new Date(); const updateObj = { $set: { @@ -559,18 +556,21 @@ class Assignment { let discTime, idleTime; // If in disconnected state, compute total disconnected time - if ( (discTime = this._getLastDisconnect(instanceId)) != null) { + if ((discTime = this._getLastDisconnect(instanceId)) != null) { addResetDisconnectedUpdateFields(updateObj, leaveTime.getTime() - discTime); } // If in idle state, compute total idle time - if ( (idleTime = this._getLastIdle(instanceId)) != null) { + if ((idleTime = this._getLastIdle(instanceId)) != null) { addResetIdleUpdateFields(updateObj, leaveTime.getTime() - idleTime); } - Assignments.update({ - _id: this.asstId, - "instances.id": instanceId - }, updateObj); + Assignments.update( + { + _id: this.asstId, + "instances.id": instanceId + }, + updateObj + ); } // Handle a disconnection by this user @@ -588,14 +588,17 @@ class Assignment { // If we are idle, add the total idle time to the running amount; // A new idle session will start when the user reconnects let idleTime; - if (( idleTime = this._getLastIdle(instanceId)) != null ) { + if ((idleTime = this._getLastIdle(instanceId)) != null) { addResetIdleUpdateFields(updateObj, now.getTime() - idleTime); } - Assignments.update({ - _id: this.asstId, - "instances.id": instanceId - }, updateObj); + Assignments.update( + { + _id: this.asstId, + "instances.id": instanceId + }, + updateObj + ); } // Handle a reconnection by a user, if they were assigned prior to the reconnection @@ -608,51 +611,62 @@ class Assignment { }; let discTime; - if ( (discTime = this._getLastDisconnect(instanceId)) != null ) { + if ((discTime = this._getLastDisconnect(instanceId)) != null) { addResetDisconnectedUpdateFields(updateObj, Date.now() - discTime); } - Assignments.update({ - _id: this.asstId, - "instances.id": instanceId - }, updateObj); + Assignments.update( + { + _id: this.asstId, + "instances.id": instanceId + }, + updateObj + ); } _isIdle(instanceId, timestamp) { // TODO: ignore this update if user is disconnected - Assignments.update({ - _id: this.asstId, - "instances.id": instanceId - }, { - $set: { - "instances.$.lastIdle": timestamp + Assignments.update( + { + _id: this.asstId, + "instances.id": instanceId + }, + { + $set: { + "instances.$.lastIdle": timestamp + } } - }); + ); } _isActive(instanceId, timestamp) { const idleTime = this._getLastIdle(instanceId); - if ( !idleTime ) return; + if (!idleTime) return; - Assignments.update({ - _id: this.asstId, - "instances.id": instanceId - }, addResetIdleUpdateFields({}, timestamp - idleTime)); + Assignments.update( + { + _id: this.asstId, + "instances.id": instanceId + }, + addResetIdleUpdateFields({}, timestamp - idleTime) + ); } // Helper functions // TODO test that these are grabbing the right numbers _getLastDisconnect(instanceId) { const instances = this.getInstances(); - const instanceData = _.find(instances, - (inst) => { return inst.id === instanceId }); + const instanceData = _.find(instances, inst => { + return inst.id === instanceId; + }); return instanceData && instanceData.lastDisconnect; } _getLastIdle(instanceId) { const instances = this.getInstances(); - const instanceData = _.find(instances, - (inst) => { return inst.id === instanceId }); + const instanceData = _.find(instances, inst => { + return inst.id === instanceId; + }); return instanceData && instanceData.lastIdle; } } diff --git a/server/batches.js b/server/batches.js index a1e0ee3..0b302ce 100644 --- a/server/batches.js +++ b/server/batches.js @@ -20,28 +20,38 @@ if ((batch = _batches[batchId]) != null) { return batch; } else { - if (Batches.findOne(batchId) == null) { throw new Error("Batch does not exist"); } + if (Batches.findOne(batchId) == null) { + throw new Error("Batch does not exist"); + } // Return this if another Fiber created it while we yielded - return _batches[batchId] != null ? _batches[batchId] : (_batches[batchId] = new Batch(batchId)); + return _batches[batchId] != null + ? _batches[batchId] + : (_batches[batchId] = new Batch(batchId)); } } static getBatchByName(batchName) { check(batchName, String); - const batch = Batches.findOne({name: batchName}); - if (!batch) { throw new Error("Batch does not exist"); } + const batch = Batches.findOne({ name: batchName }); + if (!batch) { + throw new Error("Batch does not exist"); + } return this.getBatch(batch._id); } static currentBatch() { let userId; - if ((userId = Meteor.userId()) == null) { return; } + if ((userId = Meteor.userId()) == null) { + return; + } return TurkServer.Assignment.getCurrentUserAssignment(userId).getBatch(); } constructor(batchId) { this.batchId = batchId; - if (_batches[this.batchId] != null) { throw new Error("Batch already exists; use getBatch"); } + if (_batches[this.batchId] != null) { + throw new Error("Batch already exists; use getBatch"); + } this.lobby = new TurkServer.Lobby(this.batchId); } @@ -58,16 +68,23 @@ // need to go through getInstance. const instance = TurkServer.Instance.getInstance(groupId); - instance.bindOperation(() => TurkServer.log({ - _meta: "created"})); + instance.bindOperation(() => + TurkServer.log({ + _meta: "created" + }) + ); return instance; } - getTreatments() { return Batches.findOne(this.batchId).treatments; } + getTreatments() { + return Batches.findOne(this.batchId).treatments; + } setAssigner(assigner) { - if (this.assigner != null) { throw new Error("Assigner already set for this batch"); } + if (this.assigner != null) { + throw new Error("Assigner already set for this batch"); + } this.assigner = assigner; return assigner.initialize(this); } @@ -77,11 +94,15 @@ })(); TurkServer.ensureBatchExists = function(props) { - if (props.name == null) { throw new Error("Batch must have a name"); } - return Batches.upsert({name: props.name}, props); + if (props.name == null) { + throw new Error("Batch must have a name"); + } + return Batches.upsert({ name: props.name }, props); }; TurkServer.ensureTreatmentExists = function(props) { - if (props.name == null) { throw new Error("Treatment must have a name"); } - return Treatments.upsert({name: props.name}, props); + if (props.name == null) { + throw new Error("Treatment must have a name"); + } + return Treatments.upsert({ name: props.name }, props); }; diff --git a/server/config.js b/server/config.js index ea56c3c..7d712c2 100644 --- a/server/config.js +++ b/server/config.js @@ -1,5 +1,5 @@ -const os = Npm.require('os'); -const merge = Npm.require('deepmerge'); +const os = Npm.require("os"); +const merge = Npm.require("deepmerge"); // Client-side default settings, for reference const defaultPublicSettings = { diff --git a/server/connections.js b/server/connections.js index 63c2c0e..d66ede9 100644 --- a/server/connections.js +++ b/server/connections.js @@ -8,12 +8,16 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const attemptCallbacks = (callbacks, context, errMsg) => Array.from(callbacks).map((cb) => - (() => { try { - return cb.call(context); - } catch (e) { - return Meteor._debug(errMsg, e); - } })()); +const attemptCallbacks = (callbacks, context, errMsg) => + Array.from(callbacks).map(cb => + (() => { + try { + return cb.call(context); + } catch (e) { + return Meteor._debug(errMsg, e); + } + })() + ); const connectCallbacks = []; const disconnectCallbacks = []; @@ -28,7 +32,9 @@ TurkServer.onActive = func => activeCallbacks.push(func); // When getting user records in a session callback, we have to check if admin const getUserNonAdmin = function(userId) { const user = Meteor.users.findOne(userId); - if ((user == null) || (user != null ? user.admin : undefined)) { return; } + if (user == null || (user != null ? user.admin : undefined)) { + return; + } return user; }; @@ -39,12 +45,16 @@ const getUserNonAdmin = function(userId) { user.group takes a moment to be propagated. */ const sessionReconnect = function(doc) { - if (getUserNonAdmin(doc.userId) == null) { return; } + if (getUserNonAdmin(doc.userId) == null) { + return; + } const asst = TurkServer.Assignment.getCurrentUserAssignment(doc.userId); // TODO possible debug message, but probably caught below. - if (asst == null) { return; } + if (asst == null) { + return; + } // Save IP address and UA; multiple connections from different IPs/browsers // are recorded for diagnostic purposes. @@ -52,7 +62,8 @@ const sessionReconnect = function(doc) { $addToSet: { ipAddr: doc.ipAddr, userAgent: doc.userAgent - }}); + } + }); }; const userReconnect = function(user) { @@ -67,28 +78,32 @@ const userReconnect = function(user) { // Ensure user is in a valid state; add to lobby if not const state = user.turkserver != null ? user.turkserver.state : undefined; - if ((state === "lobby") || (state == null)) { + if (state === "lobby" || state == null) { asst._enterLobby(); return; } // We only call the group operations below if the user was in a group at the // time of connection - if ((groupId = Partitioner.getUserGroup(user._id)) == null) { return; } + if ((groupId = Partitioner.getUserGroup(user._id)) == null) { + return; + } asst._reconnected(groupId); - return TurkServer.Instance.getInstance(groupId).bindOperation(function() { - TurkServer.log({ - _userId: user._id, - _meta: "connected" - }); + return TurkServer.Instance.getInstance(groupId).bindOperation( + function() { + TurkServer.log({ + _userId: user._id, + _meta: "connected" + }); - attemptCallbacks(connectCallbacks, this, "Exception in user connect callback"); - } - , { + attemptCallbacks(connectCallbacks, this, "Exception in user connect callback"); + }, + { userId: user._id, event: "connected" - }); + } + ); }; const userDisconnect = function(user) { @@ -97,26 +112,32 @@ const userDisconnect = function(user) { // If they are disconnecting after completing an assignment, there will be no // current assignment. - if (asst == null) { return; } + if (asst == null) { + return; + } // If user was in lobby, remove them asst._removeFromLobby(); - if ((groupId = Partitioner.getUserGroup(user._id)) == null) { return; } + if ((groupId = Partitioner.getUserGroup(user._id)) == null) { + return; + } asst._disconnected(groupId); - return TurkServer.Instance.getInstance(groupId).bindOperation(function() { - TurkServer.log({ - _userId: user._id, - _meta: "disconnected" - }); + return TurkServer.Instance.getInstance(groupId).bindOperation( + function() { + TurkServer.log({ + _userId: user._id, + _meta: "disconnected" + }); - attemptCallbacks(disconnectCallbacks, this, "Exception in user disconnect callback"); - } - , { + attemptCallbacks(disconnectCallbacks, this, "Exception in user disconnect callback"); + }, + { userId: user._id, event: "disconnected" - }); + } + ); }; /* @@ -125,50 +146,60 @@ const userDisconnect = function(user) { const userIdle = function(user) { let groupId; - if ((groupId = Partitioner.getUserGroup(user._id)) == null) { return; } + if ((groupId = Partitioner.getUserGroup(user._id)) == null) { + return; + } const asst = TurkServer.Assignment.getCurrentUserAssignment(user._id); asst._isIdle(groupId, user.status.lastActivity); - return TurkServer.Instance.getInstance(groupId).bindOperation(function() { - TurkServer.log({ - _userId: user._id, - _meta: "idle", - _timestamp: user.status.lastActivity - }); // Overridden to a past value - - attemptCallbacks(idleCallbacks, this, "Exception in user idle callback"); - } - , { + return TurkServer.Instance.getInstance(groupId).bindOperation( + function() { + TurkServer.log({ + _userId: user._id, + _meta: "idle", + _timestamp: user.status.lastActivity + }); // Overridden to a past value + + attemptCallbacks(idleCallbacks, this, "Exception in user idle callback"); + }, + { userId: user._id, event: "idle" - }); + } + ); }; // Because activity on any session will make a user active, we use this in // order to properly record the last activity time on the client const sessionActive = function(doc) { let groupId; - if (getUserNonAdmin(doc.userId) == null) { return; } + if (getUserNonAdmin(doc.userId) == null) { + return; + } - if ((groupId = Partitioner.getUserGroup(doc.userId)) == null) { return; } + if ((groupId = Partitioner.getUserGroup(doc.userId)) == null) { + return; + } const asst = TurkServer.Assignment.getCurrentUserAssignment(doc.userId); asst._isActive(groupId, doc.lastActivity); - return TurkServer.Instance.getInstance(groupId).bindOperation(function() { - TurkServer.log({ - _userId: doc.userId, - _meta: "active", - _timestamp: doc.lastActivity - }); // Also overridden - - attemptCallbacks(activeCallbacks, this, "Exception in user active callback"); - } - , { + return TurkServer.Instance.getInstance(groupId).bindOperation( + function() { + TurkServer.log({ + _userId: doc.userId, + _meta: "active", + _timestamp: doc.lastActivity + }); // Also overridden + + attemptCallbacks(activeCallbacks, this, "Exception in user active callback"); + }, + { userId: doc.userId, event: "active" - }); + } + ); }; /* @@ -184,21 +215,24 @@ UserStatus.events.on("connectionActive", sessionActive); // we're interested in the contents of the entire user document when someone goes // online/offline or idle/active. Meteor.startup(function() { - - Meteor.users.find({ - "admin": {$exists: false}, // Excluding admin - "status.online": true // User is online - }).observe({ + Meteor.users + .find({ + admin: { $exists: false }, // Excluding admin + "status.online": true // User is online + }) + .observe({ added: userReconnect, removed: userDisconnect }); - return Meteor.users.find({ - "admin": {$exists: false}, // Excluding admin - "status.idle": true // User is idle - }).observe({ - added: userIdle - }); + return Meteor.users + .find({ + admin: { $exists: false }, // Excluding admin + "status.idle": true // User is idle + }) + .observe({ + added: userIdle + }); }); /* @@ -211,20 +245,21 @@ Meteor.startup(function() { TestUtils.connCallbacks = { sessionReconnect(doc) { sessionReconnect(doc); - return userReconnect( Meteor.users.findOne(doc.userId) ); + return userReconnect(Meteor.users.findOne(doc.userId)); }, sessionDisconnect(doc) { - return userDisconnect( Meteor.users.findOne(doc.userId) ); + return userDisconnect(Meteor.users.findOne(doc.userId)); }, sessionIdle(doc) { // We need to set the status.lastActivity field here, as in user-status, // because the callback expects to read its value - Meteor.users.update(doc.userId, - {$set: {"status.lastActivity": doc.lastActivity }}); + Meteor.users.update(doc.userId, { + $set: { "status.lastActivity": doc.lastActivity } + }); - return userIdle( Meteor.users.findOne(doc.userId) ); + return userIdle(Meteor.users.findOne(doc.userId)); }, sessionActive @@ -238,22 +273,25 @@ Meteor.methods({ "ts-set-username"(username) { // TODO may need validation here due to bad browsers/bad people const userId = Meteor.userId(); - if (!userId) { return; } + if (!userId) { + return; + } // No directOperation needed here since partitioner recognizes username as // a unique index - if (Meteor.users.findOne({username}) != null) { + if (Meteor.users.findOne({ username }) != null) { throw new Meteor.Error(409, ErrMsg.usernameTaken); } - return Meteor.users.update(userId, - {$set: {username}}); + return Meteor.users.update(userId, { $set: { username } }); }, "ts-submit-exitdata"(doc, panel) { let token; const userId = Meteor.userId(); - if (!userId) { throw new Meteor.Error(403, ErrMsg.authErr); } + if (!userId) { + throw new Meteor.Error(403, ErrMsg.authErr); + } // TODO what if this doesn't exist? const asst = TurkServer.Assignment.currentAssignment(); @@ -268,7 +306,7 @@ Meteor.methods({ contact: panel.contact, available: { times: panel.times, - updated: new Date + updated: new Date() } }); } @@ -276,7 +314,7 @@ Meteor.methods({ // Destroy the token for this connection, so that a resume login will not // be used for future HITs. Returning true should cause the HIT to submit on // the client side, but if that doesn't work, the user will be logged out. - if (token = Accounts._getLoginToken(this.connection.id)) { + if ((token = Accounts._getLoginToken(this.connection.id))) { // This $pulls tokens from services.resume.loginTokens, and should work // in the same way that Accounts._expireTokens effects cleanup. Accounts.destroyToken(userId, token); @@ -286,4 +324,3 @@ Meteor.methods({ return true; } }); - diff --git a/server/instance.js b/server/instance.js index 67464b9..7ca7d64 100644 --- a/server/instance.js +++ b/server/instance.js @@ -10,13 +10,12 @@ const _instances = new Map(); /** * @summary Represents a group or slice on the server, containing some users. - * These functions are available only on the server. This object is - * automatically constructed from TurkServer.Instance.getInstance. + * These functions are available only on the server. This object is + * automatically constructed from TurkServer.Instance.getInstance. * @class * @instancename instance */ class Instance { - /** * @summary Get the instance by its id. * @param {String} groupId @@ -26,14 +25,14 @@ class Instance { check(groupId, String); let inst = _instances.get(groupId); - if( inst != null ) return inst; + if (inst != null) return inst; if (Experiments.findOne(groupId) == null) { throw new Error(`Instance does not exist: ${groupId}`); } // A fiber may have created this at the same time; if so use that one - if( inst = _instances.get(groupId) && inst != null ) return inst; + if ((inst = _instances.get(groupId) && inst != null)) return inst; inst = new TurkServer.Instance(groupId); _instances.set(groupId, inst); @@ -58,7 +57,7 @@ class Instance { } constructor(groupId) { - if ( _instances.get(groupId) ) { + if (_instances.get(groupId)) { throw new Error("Instance already exists; use getInstance"); } @@ -81,16 +80,15 @@ class Instance { */ setup() { // Can't use fat arrow here. - this.bindOperation( function() { + this.bindOperation(function() { TurkServer.log({ _meta: "initialized", treatmentData: this.instance.treatment() }); - for( var handler of init_queue ) { + for (var handler of init_queue) { handler.call(this); } - }); } @@ -121,14 +119,17 @@ class Instance { }); // Set experiment start time if this was first person to join - Experiments.update({ - _id: this.groupId, - startTime: null - }, { - $set: { - startTime: new Date + Experiments.update( + { + _id: this.groupId, + startTime: null + }, + { + $set: { + startTime: new Date() + } } - }); + ); // Record instance Id in Assignment asst._joinInstance(this.groupId); @@ -158,7 +159,7 @@ class Instance { getTreatmentNames() { const instance = Experiments.findOne(this.groupId); - return instance && instance.treatments || []; + return (instance && instance.treatments) || []; } /** @@ -168,11 +169,16 @@ class Instance { treatment() { const instance = Experiments.findOne(this.groupId); - return instance && TurkServer._mergeTreatments(Treatments.find({ - name: { - $in: instance.treatments - } - })); + return ( + instance && + TurkServer._mergeTreatments( + Treatments.find({ + name: { + $in: instance.treatments + } + }) + ) + ); } /** @@ -181,7 +187,7 @@ class Instance { */ getDuration() { const instance = Experiments.findOne(this.groupId); - return (instance.endTime || new Date) - instance.startTime; + return (instance.endTime || new Date()) - instance.startTime; } /** @@ -217,12 +223,12 @@ class Instance { }); // Sometimes we may want to allow users to continue to access partition data - if( !returnToLobby ) return; + if (!returnToLobby) return; const users = Experiments.findOne(this.groupId).users; if (users == null) return; - for( userId of users ) { + for (userId of users) { this.sendUserToLobby(userId); } } diff --git a/server/lobby_server.js b/server/lobby_server.js index 1ffd9f7..092247d 100644 --- a/server/lobby_server.js +++ b/server/lobby_server.js @@ -7,9 +7,7 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const { - EventEmitter -} = Npm.require('events'); +const { EventEmitter } = Npm.require("events"); // TODO add index on LobbyStatus if needed @@ -21,7 +19,9 @@ TurkServer.Lobby = class Lobby { } addAssignment(asst) { - if (asst.batchId !== this.batchId) { throw new Error("unexpected batchId"); } + if (asst.batchId !== this.batchId) { + throw new Error("unexpected batchId"); + } // Insert or update status in lobby LobbyStatus.upsert(asst.userId, { @@ -30,32 +30,32 @@ TurkServer.Lobby = class Lobby { batchId: this.batchId, asstId: asst.asstId } - } - ); + }); Meteor.users.update(asst.userId, { $set: { "turkserver.state": "lobby" } - } - ); + }); return Meteor.defer(() => this.events.emit("user-join", asst)); } getAssignments(selector) { - selector = _.extend(selector || {}, - {batchId: this.batchId}); - return Array.from(LobbyStatus.find(selector).fetch()).map((record) => TurkServer.Assignment.getAssignment(record.asstId)); + selector = _.extend(selector || {}, { batchId: this.batchId }); + return Array.from(LobbyStatus.find(selector).fetch()).map(record => + TurkServer.Assignment.getAssignment(record.asstId) + ); } // TODO move status updates into specific assigners toggleStatus(userId) { const existing = LobbyStatus.findOne(userId); - if (!existing) { throw new Meteor.Error(403, ErrMsg.userNotInLobbyErr); } + if (!existing) { + throw new Meteor.Error(403, ErrMsg.userNotInLobbyErr); + } const newStatus = !existing.status; - LobbyStatus.update(userId, - {$set: { status: newStatus }}); + LobbyStatus.update(userId, { $set: { status: newStatus } }); const asst = TurkServer.Assignment.getCurrentUserAssignment(userId); return Meteor.defer(() => this.events.emit("user-status", asst, newStatus)); @@ -63,7 +63,7 @@ TurkServer.Lobby = class Lobby { // Takes a group of users from the lobby without triggering the user-leave event. pluckUsers(userIds) { - return LobbyStatus.remove({_id : {$in: userIds} }); + return LobbyStatus.remove({ _id: { $in: userIds } }); } removeAssignment(asst) { @@ -78,13 +78,15 @@ TurkServer.Lobby = class Lobby { // TODO can we simplify this by publishing users with turkserver.state = "lobby", // if we use batch IDs in a smart way? Meteor.publish("lobby", function(batchId) { - if (batchId == null) { return []; } + if (batchId == null) { + return []; + } const sub = this; - const handle = LobbyStatus.find({batchId}).observeChanges({ + const handle = LobbyStatus.find({ batchId }).observeChanges({ added(id, fields) { sub.added("ts.lobby", id, fields); - return sub.added("users", id, Meteor.users.findOne(id, {fields: {username: 1}})); + return sub.added("users", id, Meteor.users.findOne(id, { fields: { username: 1 } })); }, changed(id, fields) { return sub.changed("ts.lobby", id, fields); @@ -103,19 +105,24 @@ Meteor.publish("lobby", function(batchId) { // TODO publish this based on the batch of the active user Meteor.publish(null, function() { const sub = this; - const subHandle = Batches.find({ - active: true, - lobby: true, - grouping: "groupSize", - groupVal: { $exists: true } - }, - {fields: { groupVal: 1 }} + const subHandle = Batches.find( + { + active: true, + lobby: true, + grouping: "groupSize", + groupVal: { $exists: true } + }, + { fields: { groupVal: 1 } } ).observeChanges({ added(id, fields) { - return sub.added("ts.config", "lobbyThreshold", { value: fields.groupVal }); + return sub.added("ts.config", "lobbyThreshold", { + value: fields.groupVal + }); }, changed(id, fields) { - return sub.changed("ts.config", "lobbyThreshold", { value: fields.groupVal }); + return sub.changed("ts.config", "lobbyThreshold", { + value: fields.groupVal + }); }, removed(id) { return sub.removed("ts.config", "lobbyThreshold"); @@ -128,16 +135,17 @@ Meteor.publish(null, function() { // Check for lobby state Meteor.methods({ - "toggleStatus"() { + toggleStatus() { const userId = Meteor.userId(); - if (!userId) { throw new Meteor.Error(403, ErrMsg.userIdErr); } + if (!userId) { + throw new Meteor.Error(403, ErrMsg.userIdErr); + } TurkServer.Batch.currentBatch().lobby.toggleStatus(userId); return this.unblock(); } }); - // Clear lobby status on startup // Just clear lobby users for assignment, but not lobby state Meteor.startup(() => LobbyStatus.remove({})); diff --git a/server/logging.js b/server/logging.js index 509bac8..19128b7 100644 --- a/server/logging.js +++ b/server/logging.js @@ -16,18 +16,28 @@ Logs._ensureIndex({ Logs.before.insert(function(userId, doc) { // Never log admin actions // TODO this means admin-initiated teardown events aren't recorded - if (__guard__(Meteor.users.findOne(userId), x => x.admin)) { return false; } + if (__guard__(Meteor.users.findOne(userId), x => x.admin)) { + return false; + } let groupId = Partitioner._currentGroup.get(); if (!groupId) { - if (!userId) { throw new Meteor.Error(403, ErrMsg.userIdErr); } + if (!userId) { + throw new Meteor.Error(403, ErrMsg.userIdErr); + } groupId = Partitioner.getUserGroup(userId); - if (!groupId) { throw new Meteor.Error(403, ErrMsg.groupErr); } + if (!groupId) { + throw new Meteor.Error(403, ErrMsg.groupErr); + } } - if (userId) { doc._userId = userId; } + if (userId) { + doc._userId = userId; + } doc._groupId = groupId; - if (doc._timestamp == null) { doc._timestamp = new Date(); } // Allow specification of custom timestamps + if (doc._timestamp == null) { + doc._timestamp = new Date(); + } // Allow specification of custom timestamps return true; }); @@ -35,12 +45,13 @@ TurkServer.log = (doc, callback) => Logs.insert(doc, callback); Meteor.methods({ "ts-log"(doc) { - if (!Meteor.userId()) { Meteor._debug("Warning; received log request for anonymous user: ", doc); } + if (!Meteor.userId()) { + Meteor._debug("Warning; received log request for anonymous user: ", doc); + } Logs.insert(doc); } }); - function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== "undefined" && value !== null ? transform(value) : undefined; +} diff --git a/server/mturk.js b/server/mturk.js index 7484ad1..3248cd7 100644 --- a/server/mturk.js +++ b/server/mturk.js @@ -1,10 +1,9 @@ -const mturk = Npm.require('mturk-api'); -const JSPath = Npm.require('jspath'); +const mturk = Npm.require("mturk-api"); +const JSPath = Npm.require("jspath"); let api = undefined; -if ( !TurkServer.config.mturk.accessKeyId || - !TurkServer.config.mturk.secretAccessKey ) { +if (!TurkServer.config.mturk.accessKeyId || !TurkServer.config.mturk.secretAccessKey) { Meteor._debug("Missing Amazon API keys for connecting to MTurk. Please configure."); } else { const config = { @@ -13,8 +12,9 @@ if ( !TurkServer.config.mturk.accessKeyId || sandbox: TurkServer.config.mturk.sandbox }; - const promise = mturk.connect(config) - .then((api) => api) + const promise = mturk + .connect(config) + .then(api => api) .catch(console.error); api = Promise.resolve(promise).await(); } @@ -25,7 +25,7 @@ TurkServer.mturk = function(op, params) { return; } - const promise = api.req(op, params).then((resp) => resp); + const promise = api.req(op, params).then(resp => resp); const result = Promise.resolve(promise).await(); return transform(op, result); @@ -43,7 +43,7 @@ TurkServer.mturk = function(op, params) { This is just for compatibility with what the previous API returned. */ function transform(op, result) { - switch(op) { + switch (op) { case "CreateHIT": return JSPath.apply("..HITId[0]", result); case "GetAccountBalance": @@ -67,7 +67,7 @@ function transform(op, result) { TurkServer.Util = TurkServer.Util || {}; -TurkServer.Util.assignQualification = function(workerId, qualId, value, notify=true) { +TurkServer.Util.assignQualification = function(workerId, qualId, value, notify = true) { check(workerId, String); check(qualId, String); check(value, Match.Integer); @@ -76,27 +76,29 @@ TurkServer.Util.assignQualification = function(workerId, qualId, value, notify=t throw new Error("Unknown worker"); } - if (Workers.findOne({ + if ( + Workers.findOne({ _id: workerId, "quals.id": qualId - }) != null) { - + }) != null + ) { TurkServer.mturk("UpdateQualificationScore", { SubjectId: workerId, QualificationTypeId: qualId, IntegerValue: value }); - Workers.update({ - _id: workerId, - "quals.id": qualId - }, { - $set: { - "quals.$.value": value + Workers.update( + { + _id: workerId, + "quals.id": qualId + }, + { + $set: { + "quals.$.value": value + } } - }); - + ); } else { - TurkServer.mturk("AssignQualification", { WorkerId: workerId, QualificationTypeId: qualId, @@ -111,54 +113,68 @@ TurkServer.Util.assignQualification = function(workerId, qualId, value, notify=t } } }); - } }; Meteor.startup(function() { - Qualifications.upsert({ - name: "US Worker" - }, { - $set: { - QualificationTypeId: "00000000000000000071", - Comparator: "EqualTo", - LocaleValue: "US" + Qualifications.upsert( + { + name: "US Worker" + }, + { + $set: { + QualificationTypeId: "00000000000000000071", + Comparator: "EqualTo", + LocaleValue: "US" + } } - }); - Qualifications.upsert({ - name: "US or CA Worker" - }, { - $set: { - QualificationTypeId: "00000000000000000071", - Comparator: "In", - LocaleValue: ["US", "CA"] + ); + Qualifications.upsert( + { + name: "US or CA Worker" + }, + { + $set: { + QualificationTypeId: "00000000000000000071", + Comparator: "In", + LocaleValue: ["US", "CA"] + } } - }); - Qualifications.upsert({ - name: "> 100 HITs" - }, { - $set: { - QualificationTypeId: "00000000000000000040", - Comparator: "GreaterThan", - IntegerValue: "100" + ); + Qualifications.upsert( + { + name: "> 100 HITs" + }, + { + $set: { + QualificationTypeId: "00000000000000000040", + Comparator: "GreaterThan", + IntegerValue: "100" + } } - }); - Qualifications.upsert({ - name: "95% Approval" - }, { - $set: { - QualificationTypeId: "000000000000000000L0", - Comparator: "GreaterThanOrEqualTo", - IntegerValue: "95" + ); + Qualifications.upsert( + { + name: "95% Approval" + }, + { + $set: { + QualificationTypeId: "000000000000000000L0", + Comparator: "GreaterThanOrEqualTo", + IntegerValue: "95" + } } - }); - Qualifications.upsert({ - name: "Adult Worker" - }, { - $set: { - QualificationTypeId: "00000000000000000060", - Comparator: "EqualTo", - IntegerValue: "1" + ); + Qualifications.upsert( + { + name: "Adult Worker" + }, + { + $set: { + QualificationTypeId: "00000000000000000060", + Comparator: "EqualTo", + IntegerValue: "1" + } } - }); + ); }); diff --git a/server/server_api.js b/server/server_api.js index 2e8d792..a9d34a9 100644 --- a/server/server_api.js +++ b/server/server_api.js @@ -8,12 +8,14 @@ TurkServer.treatment = function() { const instance = TurkServer.Instance.currentInstance(); const asst = TurkServer.Assignment.currentAssignment(); - const instTreatments = instance && instance.getTreatmentNames() || []; - const asstTreatments = asst && asst.getTreatmentNames() || []; + const instTreatments = (instance && instance.getTreatmentNames()) || []; + const asstTreatments = (asst && asst.getTreatmentNames()) || []; - return TurkServer._mergeTreatments(Treatments.find({ + return TurkServer._mergeTreatments( + Treatments.find({ name: { $in: instTreatments.concat(asstTreatments) } - })); + }) + ); }; diff --git a/server/timers_server.js b/server/timers_server.js index 4a00db0..f2b2b2e 100644 --- a/server/timers_server.js +++ b/server/timers_server.js @@ -24,7 +24,6 @@ const ROUND_END_NEWROUND = "newstart"; * @namespace */ class Timers { - /** * @summary Starts a new round in the current instance. * @function TurkServer.Timers.startNewRound @@ -49,16 +48,15 @@ class Timers { } // Find the most recent round - let currentRound = null, index = 1; - if( (currentRound = RoundTimers.findOne({}, {sort: {index: -1}})) != null ) { + let currentRound = null, + index = 1; + if ((currentRound = RoundTimers.findOne({}, { sort: { index: -1 } })) != null) { index = currentRound.index + 1; // Try ending the current round if it is in progress // If we aren't ending this round, we shouldn't try to start a new one - if( !currentRound.ended && - !tryEndingRound(currentRound._id, ROUND_END_NEWROUND, now) ) { - throw new Error( - "Possible multiple concurrent calls to startNewRound detected.") + if (!currentRound.ended && !tryEndingRound(currentRound._id, ROUND_END_NEWROUND, now)) { + throw new Error("Possible multiple concurrent calls to startNewRound detected."); } } @@ -73,10 +71,8 @@ class Timers { }); scheduleRoundEnd(Partitioner.group(), newRoundId, endTime); - } - catch (e) { - throw new Error( - "Possible multiple concurrent calls to startNewRound detected.") + } catch (e) { + throw new Error("Possible multiple concurrent calls to startNewRound detected."); } } @@ -87,15 +83,14 @@ class Timers { */ static endCurrentRound() { let now = new Date(); - const current = RoundTimers.findOne({ended: false}); + const current = RoundTimers.findOne({ ended: false }); - if( current == null ) { + if (current == null) { throw new Error("No current round to end"); } - if ( !tryEndingRound(current._id, ROUND_END_MANUAL, now) ) { - throw new Error( - "Possible multiple concurrent calls to endCurrentRound detected."); + if (!tryEndingRound(current._id, ROUND_END_MANUAL, now)) { + throw new Error("Possible multiple concurrent calls to endCurrentRound detected."); } } @@ -134,18 +129,21 @@ function tryEndingRound(roundId, endType, endTime = null) { const update = { ended: true }; if (endTime != null) update.endTime = endTime; - const ups = RoundTimers.update({ - _id: roundId, - ended: false - }, { - $set: update - }); + const ups = RoundTimers.update( + { + _id: roundId, + ended: false + }, + { + $set: update + } + ); // If the round with this id already ended somehow, don't call handlers if (ups === 0) return false; // Succeeded - call handlers with the round end type - for( handler of _round_handlers ) { + for (handler of _round_handlers) { handler.call(null, endType); } return true; @@ -155,7 +153,7 @@ function tryEndingRound(roundId, endType, endTime = null) { function scheduleOutstandingRounds() { let scheduled = 0; - RoundTimers.direct.find({ended: false}).forEach( (round) => { + RoundTimers.direct.find({ ended: false }).forEach(round => { scheduleRoundEnd(round._groupId, round._id, round.endTime); scheduled++; }); @@ -179,7 +177,7 @@ TurkServer.Timers = Timers; /* Testing functions */ -TestUtils.clearRoundHandlers = function () { +TestUtils.clearRoundHandlers = function() { _round_handlers.length = 0; }; diff --git a/server/turkserver.js b/server/turkserver.js index f81697e..adb78b2 100644 --- a/server/turkserver.js +++ b/server/turkserver.js @@ -18,9 +18,15 @@ const adminOnly = { }; const always = { - insert() { return true; }, - update() { return true; }, - remove() { return true; } + insert() { + return true; + }, + update() { + return true; + }, + remove() { + return true; + } }; /* @@ -32,7 +38,7 @@ const always = { Batches.allow(adminOnly); Treatments.allow(adminOnly); -Treatments._ensureIndex({name: 1}, {unique: 1}); +Treatments._ensureIndex({ name: 1 }, { unique: 1 }); // Allow admin to make emergency adjustments to the lobby collection just in case LobbyStatus.allow(adminOnly); @@ -73,23 +79,28 @@ try { } catch (error) {} // Index HITTypes, but only for those that exist -HITTypes._ensureIndex({HITTypeId: 1}, { - name: "HITTypeId_1_sparse", - unique: 1, - sparse: 1 -}); +HITTypes._ensureIndex( + { HITTypeId: 1 }, + { + name: "HITTypeId_1_sparse", + unique: 1, + sparse: 1 + } +); -HITs._ensureIndex({HITId: 1}, {unique: 1}); +HITs._ensureIndex({ HITId: 1 }, { unique: 1 }); // TODO more careful indices on these collections // Index on unique assignment-worker pairs -Assignments._ensureIndex({ - hitId: 1, - assignmentId: 1, - workerId: 1 -} -, { unique: 1 }); +Assignments._ensureIndex( + { + hitId: 1, + assignmentId: 1, + workerId: 1 + }, + { unique: 1 } +); // Allow fast lookup of a worker's HIT assignments by status Assignments._ensureIndex({ @@ -118,26 +129,29 @@ try { // Publish turkserver user fields to a user Meteor.publish(null, function() { let workerId; - if (!this.userId) { return null; } + if (!this.userId) { + return null; + } - const cursors = [ - Meteor.users.find(this.userId, - {fields: { turkserver: 1 }}) - ]; + const cursors = [Meteor.users.find(this.userId, { fields: { turkserver: 1 } })]; // Current user assignment data, including idle and disconnection time // This won't be sent for the admin user if ((workerId = __guard__(Meteor.users.findOne(this.userId), x => x.workerId)) != null) { - cursors.push(Assignments.find({ - workerId, - status: "assigned" - }, { - fields: { - instances: 1, - treatments: 1, - bonusPayment: 1 - } - }) + cursors.push( + Assignments.find( + { + workerId, + status: "assigned" + }, + { + fields: { + instances: 1, + treatments: 1, + bonusPayment: 1 + } + } + ) ); } @@ -145,15 +159,19 @@ Meteor.publish(null, function() { }); Meteor.publish("tsTreatments", function(names) { - if ((names == null) || (names[0] == null)) { return []; } + if (names == null || names[0] == null) { + return []; + } check(names, [String]); - return Treatments.find({name: { $in: names }}); + return Treatments.find({ name: { $in: names } }); }); // Publish current experiment for a user, if it exists // This includes the data sent to the admin user Meteor.publish("tsCurrentExperiment", function(group) { - if (!this.userId) { return; } + if (!this.userId) { + return; + } return [ Experiments.find(group), RoundTimers.find() // Partitioned by group @@ -164,10 +182,12 @@ Meteor.publish("tsCurrentExperiment", function(group) { // TODO make this a bit more secure Meteor.publish("tsLoginBatches", function(batchId) { // Never send the batch list to logged-in users. - if (this.userId != null) { return []; } + if (this.userId != null) { + return []; + } // If an erroneous batchId was sent, don't just send the whole list. - if ((arguments.length > 0) && (batchId != null)) { + if (arguments.length > 0 && batchId != null) { return Batches.find(batchId); } else { return Batches.find(); @@ -176,23 +196,26 @@ Meteor.publish("tsLoginBatches", function(batchId) { Meteor.publish(null, function() { let workerId; - if (this.userId == null) { return []; } + if (this.userId == null) { + return []; + } // Publish specific batch if logged in // This should work for now because an assignment is made upon login - if ((workerId = __guard__(Meteor.users.findOne(this.userId), x => x.workerId)) == null) { return []; } + if ((workerId = __guard__(Meteor.users.findOne(this.userId), x => x.workerId)) == null) { + return []; + } const sub = this; - const handle = Assignments.find({workerId, status: "assigned"}).observeChanges({ + const handle = Assignments.find({ + workerId, + status: "assigned" + }).observeChanges({ added(id, fields) { - const { - batchId - } = Assignments.findOne(id); + const { batchId } = Assignments.findOne(id); return sub.added("ts.batches", batchId, Batches.findOne(batchId)); }, removed(id) { - const { - batchId - } = Assignments.findOne(id); + const { batchId } = Assignments.findOne(id); return sub.removed("ts.batches", batchId); } }); @@ -204,5 +227,5 @@ Meteor.publish(null, function() { TurkServer.startup = func => Meteor.startup(() => Partitioner.directOperation(func)); function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== "undefined" && value !== null ? transform(value) : undefined; +} diff --git a/tests/admin_tests.js b/tests/admin_tests.js index e3c748f..a53eff5 100644 --- a/tests/admin_tests.js +++ b/tests/admin_tests.js @@ -11,18 +11,15 @@ const hitTypeId = "mturkHITType"; // Create dummy batch and HIT Type Batches.upsert({ _id: batchId }, { _id: batchId }); -HITTypes.upsert({HITTypeId: hitTypeId}, - {$set: { batchId }}); +HITTypes.upsert({ HITTypeId: hitTypeId }, { $set: { batchId } }); // Temporarily disable the admin check during these tests const _checkAdmin = TurkServer.checkAdmin; const withCleanup = TestUtils.getCleanupWrapper({ before() { - Batches.upsert(batchId, { $set: - { active: false } - }); - return TurkServer.checkAdmin = function() {}; + Batches.upsert(batchId, { $set: { active: false } }); + return (TurkServer.checkAdmin = function() {}); }, after() { @@ -30,183 +27,204 @@ const withCleanup = TestUtils.getCleanupWrapper({ // Clean up emails and workers created for testing e-mails WorkerEmails.remove({}); - Workers.remove({test: "admin"}); + Workers.remove({ test: "admin" }); TestUtils.mturkAPI.handler = null; - return TurkServer.checkAdmin = _checkAdmin; + return (TurkServer.checkAdmin = _checkAdmin); } }); -Tinytest.add("admin - create HIT for active batch", withCleanup(function(test) { - - const newHitId = Random.id(); - TestUtils.mturkAPI.handler = function(op, params) { - switch (op) { - case "CreateHIT": return newHitId; - case "GetHIT": return { CreationTime: new Date }; // Stub out the GetHIT call with some arbitrary data - } - }; - - Batches.upsert(batchId, {$set: { active: true }}); - - // test - Meteor.call("ts-admin-create-hit", hitTypeId, {}); - - const hit = HITs.findOne({HITId: newHitId}); - - test.isTrue(hit); - test.equal(hit.HITId, newHitId); - return test.equal(hit.HITTypeId, hitTypeId); -}) +Tinytest.add( + "admin - create HIT for active batch", + withCleanup(function(test) { + const newHitId = Random.id(); + TestUtils.mturkAPI.handler = function(op, params) { + switch (op) { + case "CreateHIT": + return newHitId; + case "GetHIT": + return { CreationTime: new Date() }; // Stub out the GetHIT call with some arbitrary data + } + }; + + Batches.upsert(batchId, { $set: { active: true } }); + + // test + Meteor.call("ts-admin-create-hit", hitTypeId, {}); + + const hit = HITs.findOne({ HITId: newHitId }); + + test.isTrue(hit); + test.equal(hit.HITId, newHitId); + return test.equal(hit.HITTypeId, hitTypeId); + }) ); -Tinytest.add("admin - create HIT for inactive batch", withCleanup(test => test.throws(() => Meteor.call("ts-admin-create-hit", hitTypeId, {}) -, e => e.error === 403)) +Tinytest.add( + "admin - create HIT for inactive batch", + withCleanup(test => + test.throws(() => Meteor.call("ts-admin-create-hit", hitTypeId, {}), e => e.error === 403) + ) ); -Tinytest.add("admin - extend HIT for active batch", withCleanup(function(test) { - - const HITId = Random.id(); - HITs.insert({ HITId, HITTypeId: hitTypeId }); - Batches.upsert(batchId, {$set: { active: true }}); - - // Need to return something for GetHIT else complaining from Mongo et al - TestUtils.mturkAPI.handler = function(op, params) { - switch (op) { - case "GetHIT": return { HITId }; - } - }; - - return Meteor.call("ts-admin-extend-hit", { HITId });})); - -Tinytest.add("admin - extend HIT for inactive batch", withCleanup(function(test) { +Tinytest.add( + "admin - extend HIT for active batch", + withCleanup(function(test) { + const HITId = Random.id(); + HITs.insert({ HITId, HITTypeId: hitTypeId }); + Batches.upsert(batchId, { $set: { active: true } }); + + // Need to return something for GetHIT else complaining from Mongo et al + TestUtils.mturkAPI.handler = function(op, params) { + switch (op) { + case "GetHIT": + return { HITId }; + } + }; + + return Meteor.call("ts-admin-extend-hit", { HITId }); + }) +); - const HITId = Random.id(); - HITs.insert({ HITId, HITTypeId: hitTypeId }); +Tinytest.add( + "admin - extend HIT for inactive batch", + withCleanup(function(test) { + const HITId = Random.id(); + HITs.insert({ HITId, HITTypeId: hitTypeId }); - test.throws(() => Meteor.call("ts-admin-extend-hit", { HITId })); - return e => e.error === 403; -}) + test.throws(() => Meteor.call("ts-admin-extend-hit", { HITId })); + return e => e.error === 403; + }) ); -Tinytest.add("admin - email - create message from existing", withCleanup(function(test) { - const workers = (__range__(1, 100, true).map((i) => Random.id())); +Tinytest.add( + "admin - email - create message from existing", + withCleanup(function(test) { + const workers = __range__(1, 100, true).map(i => Random.id()); - const existingId = WorkerEmails.insert({ - subject: "test", - message: "test message", - recipients: workers - }); + const existingId = WorkerEmails.insert({ + subject: "test", + message: "test message", + recipients: workers + }); - const subject = "test2"; - const message = "another test message"; + const subject = "test2"; + const message = "another test message"; - const newId = Meteor.call("ts-admin-create-message", subject, message, existingId); + const newId = Meteor.call("ts-admin-create-message", subject, message, existingId); - const newEmail = WorkerEmails.findOne(newId); + const newEmail = WorkerEmails.findOne(newId); - test.equal(newEmail.subject, subject); - test.equal(newEmail.message, message); - test.length(newEmail.recipients, workers.length); - test.isTrue(_.isEqual(newEmail.recipients, workers)); - return test.isFalse(newEmail.sentTime); -}) + test.equal(newEmail.subject, subject); + test.equal(newEmail.message, message); + test.length(newEmail.recipients, workers.length); + test.isTrue(_.isEqual(newEmail.recipients, workers)); + return test.isFalse(newEmail.sentTime); + }) ); -Tinytest.add("admin - email - send and record message", withCleanup(function(test) { - // Create fake workers - const workerIds = ( __range__(1, 100, true).map((x) => Workers.insert({test: "admin"})) ); - test.equal(workerIds.length, 100); - - const subject = "test sending"; - let message = "test sending message"; - - const emailId = WorkerEmails.insert({ - subject, - message, - recipients: workerIds - }); - - // Record all the API calls that were made - let apiWorkers = []; - TestUtils.mturkAPI.handler = function(op, params) { - test.equal(params.Subject, subject); - test.equal(params.MessageText, message); - return apiWorkers = apiWorkers.concat(params.WorkerId); - }; - - message = Meteor.call("ts-admin-send-message", emailId); - // First word is the number of messages sent - // XXX this test may be a little janky - const count = parseInt(message.split(" ")[0]); - - test.equal(count, workerIds.length); - test.length(apiWorkers, workerIds.length); - test.isTrue(_.isEqual(apiWorkers, workerIds)); - - // Test that email sending got saved to workers - let checkedWorkers = 0; - Workers.find({_id: {$in: workerIds}}).forEach(function(worker) { - test.equal(worker.emailsReceived[0], emailId); - return checkedWorkers++; - }); - - test.equal(checkedWorkers, workerIds.length); - - // Test that sent date was recorded - return test.instanceOf(WorkerEmails.findOne(emailId).sentTime, Date); -}) +Tinytest.add( + "admin - email - send and record message", + withCleanup(function(test) { + // Create fake workers + const workerIds = __range__(1, 100, true).map(x => Workers.insert({ test: "admin" })); + test.equal(workerIds.length, 100); + + const subject = "test sending"; + let message = "test sending message"; + + const emailId = WorkerEmails.insert({ + subject, + message, + recipients: workerIds + }); + + // Record all the API calls that were made + let apiWorkers = []; + TestUtils.mturkAPI.handler = function(op, params) { + test.equal(params.Subject, subject); + test.equal(params.MessageText, message); + return (apiWorkers = apiWorkers.concat(params.WorkerId)); + }; + + message = Meteor.call("ts-admin-send-message", emailId); + // First word is the number of messages sent + // XXX this test may be a little janky + const count = parseInt(message.split(" ")[0]); + + test.equal(count, workerIds.length); + test.length(apiWorkers, workerIds.length); + test.isTrue(_.isEqual(apiWorkers, workerIds)); + + // Test that email sending got saved to workers + let checkedWorkers = 0; + Workers.find({ _id: { $in: workerIds } }).forEach(function(worker) { + test.equal(worker.emailsReceived[0], emailId); + return checkedWorkers++; + }); + + test.equal(checkedWorkers, workerIds.length); + + // Test that sent date was recorded + return test.instanceOf(WorkerEmails.findOne(emailId).sentTime, Date); + }) ); -Tinytest.add("admin - assign worker qualification", withCleanup(function(test) { - const qual = "blahblah"; - const value = 2; - const workerId = Workers.insert({}); - - TestUtils.mturkAPI.handler = function(op, params) { - test.equal(op, "AssignQualification"); - test.equal(params.QualificationTypeId, qual); - test.equal(params.WorkerId, workerId); - test.equal(params.IntegerValue, value); - return test.equal(params.SendNotification, false); - }; - - TurkServer.Util.assignQualification(workerId, qual, value, false); - - // Check that worker has been updated - const worker = Workers.findOne(workerId); - test.equal(worker.quals[0].id, qual); - return test.equal(worker.quals[0].value, 2); -}) +Tinytest.add( + "admin - assign worker qualification", + withCleanup(function(test) { + const qual = "blahblah"; + const value = 2; + const workerId = Workers.insert({}); + + TestUtils.mturkAPI.handler = function(op, params) { + test.equal(op, "AssignQualification"); + test.equal(params.QualificationTypeId, qual); + test.equal(params.WorkerId, workerId); + test.equal(params.IntegerValue, value); + return test.equal(params.SendNotification, false); + }; + + TurkServer.Util.assignQualification(workerId, qual, value, false); + + // Check that worker has been updated + const worker = Workers.findOne(workerId); + test.equal(worker.quals[0].id, qual); + return test.equal(worker.quals[0].value, 2); + }) ); -Tinytest.add("admin - update worker qualification", withCleanup(function(test) { - const qual = "blahblah"; - const value = 10; - - const workerId = Workers.insert({ - quals: [ { - id: qual, - value: 2 - } ] - }); - - TestUtils.mturkAPI.handler = function(op, params) { - test.equal(op, "UpdateQualificationScore"); - test.equal(params.QualificationTypeId, qual); - test.equal(params.SubjectId, workerId); - return test.equal(params.IntegerValue, value); - }; - - TurkServer.Util.assignQualification(workerId, qual, value, false); - - // Check that worker has been updated - const worker = Workers.findOne(workerId); - - test.length(worker.quals, 1); - test.equal(worker.quals[0].id, qual); - return test.equal(worker.quals[0].value, value); -}) +Tinytest.add( + "admin - update worker qualification", + withCleanup(function(test) { + const qual = "blahblah"; + const value = 10; + + const workerId = Workers.insert({ + quals: [ + { + id: qual, + value: 2 + } + ] + }); + + TestUtils.mturkAPI.handler = function(op, params) { + test.equal(op, "UpdateQualificationScore"); + test.equal(params.QualificationTypeId, qual); + test.equal(params.SubjectId, workerId); + return test.equal(params.IntegerValue, value); + }; + + TurkServer.Util.assignQualification(workerId, qual, value, false); + + // Check that worker has been updated + const worker = Workers.findOne(workerId); + + test.length(worker.quals, 1); + test.equal(worker.quals[0].id, qual); + return test.equal(worker.quals[0].value, value); + }) ); function __range__(left, right, inclusive) { @@ -217,4 +235,4 @@ function __range__(left, right, inclusive) { range.push(i); } return range; -} \ No newline at end of file +} diff --git a/tests/assigner_tests.js b/tests/assigner_tests.js index 271765b..ee90dc0 100644 --- a/tests/assigner_tests.js +++ b/tests/assigner_tests.js @@ -15,20 +15,23 @@ const withCleanup = TestUtils.getCleanupWrapper({ before() { // Create a random batch and corresponding lobby for assigner tests const batchId = Batches.insert({}); - return batch = TurkServer.Batch.getBatch(batchId); + return (batch = TurkServer.Batch.getBatch(batchId)); }, after() { Experiments.remove({ batchId: batch.batchId }); return Assignments.remove({ batchId: batch.batchId }); - }}); + } +}); -const tutorialTreatments = [ "tutorial" ]; -const groupTreatments = [ "group" ]; +const tutorialTreatments = ["tutorial"]; +const groupTreatments = ["group"]; TurkServer.ensureTreatmentExists({ - name: "tutorial"}); + name: "tutorial" +}); TurkServer.ensureTreatmentExists({ - name: "group"}); + name: "group" +}); const createAssignment = function() { const workerId = Random.id(); @@ -43,766 +46,870 @@ const createAssignment = function() { }); }; -Tinytest.add("assigners - tutorialGroup - assigner picks up existing instance", withCleanup(function(test) { - const assigner = new TurkServer.Assigners.TutorialGroupAssigner(tutorialTreatments, groupTreatments); +Tinytest.add( + "assigners - tutorialGroup - assigner picks up existing instance", + withCleanup(function(test) { + const assigner = new TurkServer.Assigners.TutorialGroupAssigner( + tutorialTreatments, + groupTreatments + ); - const instance = batch.createInstance(groupTreatments); - instance.setup(); + const instance = batch.createInstance(groupTreatments); + instance.setup(); - batch.setAssigner(assigner); + batch.setAssigner(assigner); - test.equal(assigner.instance, instance); - return test.equal(assigner.autoAssign, true); -}) + test.equal(assigner.instance, instance); + return test.equal(assigner.autoAssign, true); + }) ); -Tinytest.add("assigners - tutorialGroup - initial lobby gets tutorial", withCleanup(function(test) { - const assigner = new TurkServer.Assigners.TutorialGroupAssigner(tutorialTreatments, groupTreatments); - batch.setAssigner(assigner); +Tinytest.add( + "assigners - tutorialGroup - initial lobby gets tutorial", + withCleanup(function(test) { + const assigner = new TurkServer.Assigners.TutorialGroupAssigner( + tutorialTreatments, + groupTreatments + ); + batch.setAssigner(assigner); - test.equal(assigner.autoAssign, false); + test.equal(assigner.autoAssign, false); - const asst = createAssignment(); - TestUtils.connCallbacks.sessionReconnect({userId: asst.userId}); + const asst = createAssignment(); + TestUtils.connCallbacks.sessionReconnect({ userId: asst.userId }); - TestUtils.sleep(150); // YES!! + TestUtils.sleep(150); // YES!! - const user = Meteor.users.findOne(asst.userId); - const instances = asst.getInstances(); + const user = Meteor.users.findOne(asst.userId); + const instances = asst.getInstances(); - test.equal(user.turkserver.state, "experiment"); - test.length(instances, 1); + test.equal(user.turkserver.state, "experiment"); + test.length(instances, 1); - test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 0); - const exp = Experiments.findOne(instances[0].id); - return test.equal(exp.treatments, tutorialTreatments); -}) + test.equal(LobbyStatus.find({ batchId: batch.batchId }).count(), 0); + const exp = Experiments.findOne(instances[0].id); + return test.equal(exp.treatments, tutorialTreatments); + }) ); -Tinytest.add("assigners - tutorialGroup - autoAssign event triggers properly", withCleanup(function(test) { - - const assigner = new TurkServer.Assigners.TutorialGroupAssigner(tutorialTreatments, groupTreatments); - batch.setAssigner(assigner); +Tinytest.add( + "assigners - tutorialGroup - autoAssign event triggers properly", + withCleanup(function(test) { + const assigner = new TurkServer.Assigners.TutorialGroupAssigner( + tutorialTreatments, + groupTreatments + ); + batch.setAssigner(assigner); - const asst = createAssignment(); - // Pretend we already have a tutorial done - const tutorialInstance = batch.createInstance(tutorialTreatments); - tutorialInstance.setup(); - tutorialInstance.addAssignment(asst); - tutorialInstance.teardown(); + const asst = createAssignment(); + // Pretend we already have a tutorial done + const tutorialInstance = batch.createInstance(tutorialTreatments); + tutorialInstance.setup(); + tutorialInstance.addAssignment(asst); + tutorialInstance.teardown(); - TestUtils.sleep(100); // So the user joins the lobby properly + TestUtils.sleep(100); // So the user joins the lobby properly - let user = Meteor.users.findOne(asst.userId); - let instances = asst.getInstances(); + let user = Meteor.users.findOne(asst.userId); + let instances = asst.getInstances(); - test.equal(user.turkserver.state, "lobby"); - test.length(instances, 1); + test.equal(user.turkserver.state, "lobby"); + test.length(instances, 1); - batch.lobby.events.emit("auto-assign"); + batch.lobby.events.emit("auto-assign"); - TestUtils.sleep(100); + TestUtils.sleep(100); - user = Meteor.users.findOne(asst.userId); - instances = asst.getInstances(); + user = Meteor.users.findOne(asst.userId); + instances = asst.getInstances(); - test.equal(user.turkserver.state, "experiment"); - test.length(instances, 2); + test.equal(user.turkserver.state, "experiment"); + test.length(instances, 2); - test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 0); - const exp = Experiments.findOne(instances[1].id); - return test.equal(exp.treatments, groupTreatments); -}) + test.equal(LobbyStatus.find({ batchId: batch.batchId }).count(), 0); + const exp = Experiments.findOne(instances[1].id); + return test.equal(exp.treatments, groupTreatments); + }) ); -Tinytest.add("assigners - tutorialGroup - final send to exit survey", withCleanup(function(test) { - - const assigner = new TurkServer.Assigners.TutorialGroupAssigner(tutorialTreatments, groupTreatments); - batch.setAssigner(assigner); - - const asst = createAssignment(); - // Pretend we already have a tutorial done - const tutorialInstance = batch.createInstance(tutorialTreatments); - tutorialInstance.setup(); - tutorialInstance.addAssignment(asst); - tutorialInstance.teardown(); - - TestUtils.sleep(100); // So the user joins the lobby properly - - const groupInstance = batch.createInstance(groupTreatments); - groupInstance.setup(); - groupInstance.addAssignment(asst); - groupInstance.teardown(); - - TestUtils.sleep(100); - - const user = Meteor.users.findOne(asst.userId); - const instances = asst.getInstances(); - - test.equal(user.turkserver.state, "exitsurvey"); - return test.length(instances, 2); -}) +Tinytest.add( + "assigners - tutorialGroup - final send to exit survey", + withCleanup(function(test) { + const assigner = new TurkServer.Assigners.TutorialGroupAssigner( + tutorialTreatments, + groupTreatments + ); + batch.setAssigner(assigner); + + const asst = createAssignment(); + // Pretend we already have a tutorial done + const tutorialInstance = batch.createInstance(tutorialTreatments); + tutorialInstance.setup(); + tutorialInstance.addAssignment(asst); + tutorialInstance.teardown(); + + TestUtils.sleep(100); // So the user joins the lobby properly + + const groupInstance = batch.createInstance(groupTreatments); + groupInstance.setup(); + groupInstance.addAssignment(asst); + groupInstance.teardown(); + + TestUtils.sleep(100); + + const user = Meteor.users.findOne(asst.userId); + const instances = asst.getInstances(); + + test.equal(user.turkserver.state, "exitsurvey"); + return test.length(instances, 2); + }) ); // Setup for multi tests below TurkServer.ensureTreatmentExists({ - name: "tutorial"}); + name: "tutorial" +}); TurkServer.ensureTreatmentExists({ - name: "parallel_worlds"}); + name: "parallel_worlds" +}); -const multiGroupTreatments = [ "parallel_worlds" ]; +const multiGroupTreatments = ["parallel_worlds"]; /* Randomized multi-group assigner */ -Tinytest.add("assigners - tutorialRandomizedGroup - initial lobby gets tutorial", withCleanup(function(test) { - const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( - tutorialTreatments, multiGroupTreatments, [8, 16, 32]); - - batch.setAssigner(assigner); - - const asst = createAssignment(); - TestUtils.connCallbacks.sessionReconnect({userId: asst.userId}); - - TestUtils.sleep(150); - - const user = Meteor.users.findOne(asst.userId); - const instances = asst.getInstances(); - - // should be in experiment - test.equal(user.turkserver.state, "experiment"); - test.length(instances, 1); - // should not be in lobby - test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 0); - // should be in a tutorial - const exp = Experiments.findOne(instances[0].id); - return test.equal(exp.treatments, tutorialTreatments); -}) +Tinytest.add( + "assigners - tutorialRandomizedGroup - initial lobby gets tutorial", + withCleanup(function(test) { + const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( + tutorialTreatments, + multiGroupTreatments, + [8, 16, 32] + ); + + batch.setAssigner(assigner); + + const asst = createAssignment(); + TestUtils.connCallbacks.sessionReconnect({ userId: asst.userId }); + + TestUtils.sleep(150); + + const user = Meteor.users.findOne(asst.userId); + const instances = asst.getInstances(); + + // should be in experiment + test.equal(user.turkserver.state, "experiment"); + test.length(instances, 1); + // should not be in lobby + test.equal(LobbyStatus.find({ batchId: batch.batchId }).count(), 0); + // should be in a tutorial + const exp = Experiments.findOne(instances[0].id); + return test.equal(exp.treatments, tutorialTreatments); + }) ); -Tinytest.add("assigners - tutorialRandomizedGroup - send to exit survey", withCleanup(function(test) { - const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( - tutorialTreatments, multiGroupTreatments, [8, 16, 32]); - - batch.setAssigner(assigner); - - const asst = createAssignment(); - // Pretend we already have two instances done - Assignments.update(asst.asstId, { - $push: { - instances: { - $each: [ - { id: Random.id() }, - { id: Random.id() } - ] +Tinytest.add( + "assigners - tutorialRandomizedGroup - send to exit survey", + withCleanup(function(test) { + const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( + tutorialTreatments, + multiGroupTreatments, + [8, 16, 32] + ); + + batch.setAssigner(assigner); + + const asst = createAssignment(); + // Pretend we already have two instances done + Assignments.update(asst.asstId, { + $push: { + instances: { + $each: [{ id: Random.id() }, { id: Random.id() }] + } } - } - }); + }); - TestUtils.connCallbacks.sessionReconnect({userId: asst.userId}); + TestUtils.connCallbacks.sessionReconnect({ userId: asst.userId }); - TestUtils.sleep(100); + TestUtils.sleep(100); - const user = Meteor.users.findOne(asst.userId); - const instances = asst.getInstances(); + const user = Meteor.users.findOne(asst.userId); + const instances = asst.getInstances(); - test.equal(user.turkserver.state, "exitsurvey"); - return test.length(instances, 2); -}) + test.equal(user.turkserver.state, "exitsurvey"); + return test.length(instances, 2); + }) ); -Tinytest.add("assigners - tutorialRandomizedGroup - set up instances", withCleanup(function(test) { - const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( - tutorialTreatments, multiGroupTreatments, [8, 16, 32]); - - batch.setAssigner(assigner); - - assigner.setup(); - - // Verify that four instances were created with the right treatments - const created = Experiments.find({ batchId: batch.batchId }).fetch(); - - test.length(created, 4); - - // Sort by group size and test - created.sort(function(a, b) { - if (a.treatments[0] === "parallel_worlds") { return 1; - } else if (b.treatments[0] === "parallel_worlds") { return -1; - // grab the part after "group_" - } else { return parseInt(a.treatments[0].substring(6)) - parseInt(b.treatments[0].substring(6)); } - }); +Tinytest.add( + "assigners - tutorialRandomizedGroup - set up instances", + withCleanup(function(test) { + const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( + tutorialTreatments, + multiGroupTreatments, + [8, 16, 32] + ); + + batch.setAssigner(assigner); + + assigner.setup(); + + // Verify that four instances were created with the right treatments + const created = Experiments.find({ batchId: batch.batchId }).fetch(); + + test.length(created, 4); + + // Sort by group size and test + created.sort(function(a, b) { + if (a.treatments[0] === "parallel_worlds") { + return 1; + } else if (b.treatments[0] === "parallel_worlds") { + return -1; + // grab the part after "group_" + } else { + return parseInt(a.treatments[0].substring(6)) - parseInt(b.treatments[0].substring(6)); + } + }); - test.equal(created[0].treatments, [ "group_8", "parallel_worlds" ]); - test.equal(created[1].treatments, [ "group_16", "parallel_worlds" ]); - test.equal(created[2].treatments, [ "group_32", "parallel_worlds" ]); - // Buffer group - test.equal(created[3].treatments, [ "parallel_worlds" ]); + test.equal(created[0].treatments, ["group_8", "parallel_worlds"]); + test.equal(created[1].treatments, ["group_16", "parallel_worlds"]); + test.equal(created[2].treatments, ["group_32", "parallel_worlds"]); + // Buffer group + test.equal(created[3].treatments, ["parallel_worlds"]); - // Test that there are 56 randomization slots now with the right allocation - test.isFalse(assigner.autoAssign); - test.isTrue(assigner.bufferInstanceId); + // Test that there are 56 randomization slots now with the right allocation + test.isFalse(assigner.autoAssign); + test.isTrue(assigner.bufferInstanceId); - test.length(assigner.instanceSlots, 56); - test.equal(assigner.instanceSlotIndex, 0); + test.length(assigner.instanceSlots, 56); + test.equal(assigner.instanceSlotIndex, 0); - const allocation = _.countBy(assigner.instanceSlots, Object); - test.equal(allocation[created[0]._id], 8); - test.equal(allocation[created[1]._id], 16); - test.equal(allocation[created[2]._id], 32); + const allocation = _.countBy(assigner.instanceSlots, Object); + test.equal(allocation[created[0]._id], 8); + test.equal(allocation[created[1]._id], 16); + test.equal(allocation[created[2]._id], 32); - // Calling setup again should not do anything - assigner.setup(); + // Calling setup again should not do anything + assigner.setup(); - return test.length(Experiments.find({ batchId: batch.batchId }).fetch(), 4); -}) + return test.length(Experiments.find({ batchId: batch.batchId }).fetch(), 4); + }) ); -Tinytest.add("assigners - tutorialRandomizedGroup - set up reusing existing instances", withCleanup(function(test) { - const groupArr = [ - 1, 1, 1, 1, 1, 1, 1, 1, - 2, 2, 2, 2, - 4, 4, - 8, 16, 32 - ]; +Tinytest.add( + "assigners - tutorialRandomizedGroup - set up reusing existing instances", + withCleanup(function(test) { + const groupArr = [1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 8, 16, 32]; - const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( - tutorialTreatments, multiGroupTreatments, groupArr); + const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( + tutorialTreatments, + multiGroupTreatments, + groupArr + ); - // Create one existing treatment of group size 1 - const instance = batch.createInstance( ["group_1"].concat(multiGroupTreatments) ); - instance.setup(); + // Create one existing treatment of group size 1 + const instance = batch.createInstance(["group_1"].concat(multiGroupTreatments)); + instance.setup(); - batch.setAssigner(assigner); + batch.setAssigner(assigner); - assigner.setup(); + assigner.setup(); - // Verify that 18 instances were created with the right treatments - const created = Experiments.find({ batchId: batch.batchId }).fetch(); + // Verify that 18 instances were created with the right treatments + const created = Experiments.find({ batchId: batch.batchId }).fetch(); - test.length(created, 18); + test.length(created, 18); - // Test that there are 56 randomization slots now with the right allocation - test.isFalse(assigner.autoAssign); - test.isTrue(assigner.bufferInstanceId); + // Test that there are 56 randomization slots now with the right allocation + test.isFalse(assigner.autoAssign); + test.isTrue(assigner.bufferInstanceId); - test.length(assigner.instanceSlots, 80); - return test.equal(assigner.instanceSlotIndex, 0); -}) + test.length(assigner.instanceSlots, 80); + return test.equal(assigner.instanceSlotIndex, 0); + }) ); -Tinytest.add("assigners - tutorialRandomizedGroup - pick up existing instances", withCleanup(function(test) { - const groupArr = [8, 16, 32]; - const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( - tutorialTreatments, multiGroupTreatments, groupArr); - - // Generate the config that the group assigner would have - const groupConfig = TurkServer.Assigners.TutorialRandomizedGroupAssigner - .generateConfig(groupArr, multiGroupTreatments); - - const created = []; - - for (let i = 0; i < groupConfig.length; i++) { - const conf = groupConfig[i]; - const instance = batch.createInstance(conf.treatments); - instance.setup(); - - created.push(instance.groupId); - } +Tinytest.add( + "assigners - tutorialRandomizedGroup - pick up existing instances", + withCleanup(function(test) { + const groupArr = [8, 16, 32]; + const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( + tutorialTreatments, + multiGroupTreatments, + groupArr + ); + + // Generate the config that the group assigner would have + const groupConfig = TurkServer.Assigners.TutorialRandomizedGroupAssigner.generateConfig( + groupArr, + multiGroupTreatments + ); + + const created = []; + + for (let i = 0; i < groupConfig.length; i++) { + const conf = groupConfig[i]; + const instance = batch.createInstance(conf.treatments); + instance.setup(); + + created.push(instance.groupId); + } - batch.setAssigner(assigner); + batch.setAssigner(assigner); - // Test that there are 56 randomization slots now with the right allocation - test.isFalse(assigner.autoAssign); - test.isTrue(assigner.bufferInstanceId); + // Test that there are 56 randomization slots now with the right allocation + test.isFalse(assigner.autoAssign); + test.isTrue(assigner.bufferInstanceId); - test.length(assigner.instanceSlots, 56); - test.equal(assigner.instanceSlotIndex, 0); + test.length(assigner.instanceSlots, 56); + test.equal(assigner.instanceSlotIndex, 0); - const allocation = _.countBy(assigner.instanceSlots, Object); - test.equal(allocation[created[0]], 8); - test.equal(allocation[created[1]], 16); - return test.equal(allocation[created[2]], 32); -}) + const allocation = _.countBy(assigner.instanceSlots, Object); + test.equal(allocation[created[0]], 8); + test.equal(allocation[created[1]], 16); + return test.equal(allocation[created[2]], 32); + }) ); -Tinytest.add("assigners - tutorialRandomizedGroup - resume with partial allocation", withCleanup(function(test) { - const groupArr = [8, 16, 32]; - const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( - tutorialTreatments, multiGroupTreatments, groupArr); - - // Generate the config that the group assigner would have - const groupConfig = TurkServer.Assigners.TutorialRandomizedGroupAssigner - .generateConfig(groupArr, multiGroupTreatments); - - const created = []; - - for (let i = 0; i < groupConfig.length; i++) { - const conf = groupConfig[i]; - const instance = batch.createInstance(conf.treatments); - instance.setup(); - - // Fill each group half full - for (let j = 1, end = conf.size/2, asc = 1 <= end; asc ? j <= end : j >= end; asc ? j++ : j--) { - const asst = createAssignment(); - - // Pretend like this instance did the tutorial - const tutorialInstance = batch.createInstance(tutorialTreatments); - tutorialInstance.setup(); - tutorialInstance.addAssignment(asst); - tutorialInstance.teardown(); +Tinytest.add( + "assigners - tutorialRandomizedGroup - resume with partial allocation", + withCleanup(function(test) { + const groupArr = [8, 16, 32]; + const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( + tutorialTreatments, + multiGroupTreatments, + groupArr + ); + + // Generate the config that the group assigner would have + const groupConfig = TurkServer.Assigners.TutorialRandomizedGroupAssigner.generateConfig( + groupArr, + multiGroupTreatments + ); + + const created = []; + + for (let i = 0; i < groupConfig.length; i++) { + const conf = groupConfig[i]; + const instance = batch.createInstance(conf.treatments); + instance.setup(); + + // Fill each group half full + for ( + let j = 1, end = conf.size / 2, asc = 1 <= end; + asc ? j <= end : j >= end; + asc ? j++ : j-- + ) { + const asst = createAssignment(); + + // Pretend like this instance did the tutorial + const tutorialInstance = batch.createInstance(tutorialTreatments); + tutorialInstance.setup(); + tutorialInstance.addAssignment(asst); + tutorialInstance.teardown(); + + instance.addAssignment(asst); + } - instance.addAssignment(asst); + created.push(instance.groupId); } - created.push(instance.groupId); - } - - // Run it - batch.setAssigner(assigner); + // Run it + batch.setAssigner(assigner); - // Test that there are 28 randomization slots now with the right allocation - // auto-assign should be enabled because there are people in it - test.isTrue(assigner.autoAssign); - test.isTrue(assigner.bufferInstanceId); + // Test that there are 28 randomization slots now with the right allocation + // auto-assign should be enabled because there are people in it + test.isTrue(assigner.autoAssign); + test.isTrue(assigner.bufferInstanceId); - test.length(assigner.instanceSlots, 28); - test.equal(assigner.instanceSlotIndex, 0); + test.length(assigner.instanceSlots, 28); + test.equal(assigner.instanceSlotIndex, 0); - const allocation = _.countBy(assigner.instanceSlots, Object); - test.equal(allocation[created[0]], 8/2); - test.equal(allocation[created[1]], 16/2); - return test.equal(allocation[created[2]], 32/2); -}) + const allocation = _.countBy(assigner.instanceSlots, Object); + test.equal(allocation[created[0]], 8 / 2); + test.equal(allocation[created[1]], 16 / 2); + return test.equal(allocation[created[2]], 32 / 2); + }) ); -Tinytest.add("assigners - tutorialRandomizedGroup - resume with fully allocated groups", withCleanup(function(test) { - const groupArr = [8, 16, 32]; - const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( - tutorialTreatments, multiGroupTreatments, groupArr); - - // Generate the config that the group assigner would have - const groupConfig = TurkServer.Assigners.TutorialRandomizedGroupAssigner - .generateConfig(groupArr, multiGroupTreatments); - - const created = []; - - for (let i = 0; i < groupConfig.length; i++) { - const conf = groupConfig[i]; - const instance = batch.createInstance(conf.treatments); - instance.setup(); - - // Fill each group half full - for (let j = 1, end = conf.size, asc = 1 <= end; asc ? j <= end : j >= end; asc ? j++ : j--) { - const asst = createAssignment(); - - // Pretend like this instance did the tutorial - const tutorialInstance = batch.createInstance(tutorialTreatments); - tutorialInstance.setup(); - tutorialInstance.addAssignment(asst); - tutorialInstance.teardown(); +Tinytest.add( + "assigners - tutorialRandomizedGroup - resume with fully allocated groups", + withCleanup(function(test) { + const groupArr = [8, 16, 32]; + const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( + tutorialTreatments, + multiGroupTreatments, + groupArr + ); + + // Generate the config that the group assigner would have + const groupConfig = TurkServer.Assigners.TutorialRandomizedGroupAssigner.generateConfig( + groupArr, + multiGroupTreatments + ); + + const created = []; + + for (let i = 0; i < groupConfig.length; i++) { + const conf = groupConfig[i]; + const instance = batch.createInstance(conf.treatments); + instance.setup(); + + // Fill each group half full + for (let j = 1, end = conf.size, asc = 1 <= end; asc ? j <= end : j >= end; asc ? j++ : j--) { + const asst = createAssignment(); + + // Pretend like this instance did the tutorial + const tutorialInstance = batch.createInstance(tutorialTreatments); + tutorialInstance.setup(); + tutorialInstance.addAssignment(asst); + tutorialInstance.teardown(); + + instance.addAssignment(asst); + } - instance.addAssignment(asst); + created.push(instance.groupId); } - created.push(instance.groupId); - } - - // Run it - batch.setAssigner(assigner); + // Run it + batch.setAssigner(assigner); - // auto-assign should be enabled because there are people in it - test.isTrue(assigner.autoAssign); - test.isTrue(assigner.bufferInstanceId); + // auto-assign should be enabled because there are people in it + test.isTrue(assigner.autoAssign); + test.isTrue(assigner.bufferInstanceId); - test.length(assigner.instanceSlots, 0); - return test.equal(assigner.instanceSlotIndex, 0); -}) + test.length(assigner.instanceSlots, 0); + return test.equal(assigner.instanceSlotIndex, 0); + }) ); -Tinytest.add("assigners - tutorialRandomizedGroup - assign with waiting room and sequential", withCleanup(function(test) { - let exp, groupSize, instance, users; - let i; - const groupArr = [8, 16, 32]; +Tinytest.add( + "assigners - tutorialRandomizedGroup - assign with waiting room and sequential", + withCleanup(function(test) { + let exp, groupSize, instance, users; + let i; + const groupArr = [8, 16, 32]; - const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( - tutorialTreatments, multiGroupTreatments, groupArr); + const assigner = new TurkServer.Assigners.TutorialRandomizedGroupAssigner( + tutorialTreatments, + multiGroupTreatments, + groupArr + ); - batch.setAssigner(assigner); + batch.setAssigner(assigner); - assigner.setup(); // Create instances + assigner.setup(); // Create instances - test.isFalse(assigner.autoAssign); - test.length(assigner.instanceSlots, 56); - test.equal(assigner.instanceSlotIndex, 0); + test.isFalse(assigner.autoAssign); + test.length(assigner.instanceSlots, 56); + test.equal(assigner.instanceSlotIndex, 0); - // Get the config that the group assigner would have - const groupConfigMulti = assigner.groupConfig; + // Get the config that the group assigner would have + const groupConfigMulti = assigner.groupConfig; - const assts = ((() => { - const result = []; - for (i = 1; i <= 64; i++) { - result.push(createAssignment()); + const assts = (() => { + const result = []; + for (i = 1; i <= 64; i++) { + result.push(createAssignment()); + } + return result; + })(); + + // Pretend they have all done the tutorial + for (let asst of Array.from(assts)) { + Assignments.update(asst.asstId, { + $push: { instances: { id: Random.id() } } + }); } - return result; - })()); - - // Pretend they have all done the tutorial - for (let asst of Array.from(assts)) { - Assignments.update(asst.asstId, - {$push: { instances: { id: Random.id() } }}); - } - // Make the first half join - for (i = 0; i <= 27; i++) { - TestUtils.connCallbacks.sessionReconnect({userId: assts[i].userId}); - } + // Make the first half join + for (i = 0; i <= 27; i++) { + TestUtils.connCallbacks.sessionReconnect({ userId: assts[i].userId }); + } - TestUtils.sleep(500); // Give enough time for lobby functions to process + TestUtils.sleep(500); // Give enough time for lobby functions to process - // should have 32 users in lobby - test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 28); + // should have 32 users in lobby + test.equal(LobbyStatus.find({ batchId: batch.batchId }).count(), 28); - // Run auto-assign - assigner.assignAll(); + // Run auto-assign + assigner.assignAll(); - test.isTrue(assigner.autoAssign); - test.length(assigner.instanceSlots, 56); - test.equal(assigner.instanceSlotIndex, 28); + test.isTrue(assigner.autoAssign); + test.length(assigner.instanceSlots, 56); + test.equal(assigner.instanceSlotIndex, 28); - TestUtils.sleep(500); // Give enough time for lobby functions to process - // should have 0 users in lobby - test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 0); + TestUtils.sleep(500); // Give enough time for lobby functions to process + // should have 0 users in lobby + test.equal(LobbyStatus.find({ batchId: batch.batchId }).count(), 0); - const exps = Experiments.find({ batchId: batch.batchId }).fetch(); + const exps = Experiments.find({ batchId: batch.batchId }).fetch(); - // Check that the groups have the right size and treatments - let totalAdded = 0; - for (exp of Array.from(exps)) { - instance = TurkServer.Instance.getInstance(exp._id); - ({ - groupSize - } = instance.treatment()); + // Check that the groups have the right size and treatments + let totalAdded = 0; + for (exp of Array.from(exps)) { + instance = TurkServer.Instance.getInstance(exp._id); + ({ groupSize } = instance.treatment()); - if (groupSize != null) { - users = instance.users(); - test.isTrue(users.length < groupSize); - totalAdded += users.length; - } else { // Buffer group should be empty - test.length(instance.users(), 0); + if (groupSize != null) { + users = instance.users(); + test.isTrue(users.length < groupSize); + totalAdded += users.length; + } else { + // Buffer group should be empty + test.length(instance.users(), 0); + } } - } - test.equal(totalAdded, 28); + test.equal(totalAdded, 28); - // Fill in remaining users - for (i = 28; i <= 63; i++) { - TestUtils.connCallbacks.sessionReconnect({userId: assts[i].userId}); - } + // Fill in remaining users + for (i = 28; i <= 63; i++) { + TestUtils.connCallbacks.sessionReconnect({ userId: assts[i].userId }); + } - test.isTrue(assigner.autoAssign); - test.length(assigner.instanceSlots, 56); - test.equal(assigner.instanceSlotIndex, 56); + test.isTrue(assigner.autoAssign); + test.length(assigner.instanceSlots, 56); + test.equal(assigner.instanceSlotIndex, 56); - TestUtils.sleep(800); + TestUtils.sleep(800); - // Should have no one in lobby - test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 0); + // Should have no one in lobby + test.equal(LobbyStatus.find({ batchId: batch.batchId }).count(), 0); - // All groups should be filled with 8 in buffer - totalAdded = 0; - for (exp of Array.from(exps)) { - instance = TurkServer.Instance.getInstance(exp._id); - ({ - groupSize - } = instance.treatment()); + // All groups should be filled with 8 in buffer + totalAdded = 0; + for (exp of Array.from(exps)) { + instance = TurkServer.Instance.getInstance(exp._id); + ({ groupSize } = instance.treatment()); - users = instance.users(); + users = instance.users(); - if (groupSize != null) { - test.length(users, groupSize); - totalAdded += users.length; - } else { // Buffer group should have 8 users - test.length(users, 8); - totalAdded += users.length; + if (groupSize != null) { + test.length(users, groupSize); + totalAdded += users.length; + } else { + // Buffer group should have 8 users + test.length(users, 8); + totalAdded += users.length; + } } - } - test.equal(totalAdded, 64); + test.equal(totalAdded, 64); - // Test auto-stopping - const lastInstance = TurkServer.Instance.getInstance(assigner.bufferInstanceId); - lastInstance.teardown(); + // Test auto-stopping + const lastInstance = TurkServer.Instance.getInstance(assigner.bufferInstanceId); + lastInstance.teardown(); - const slackerAsst = createAssignment(); + const slackerAsst = createAssignment(); - Assignments.update(slackerAsst.asstId, - {$push: { instances: { id: Random.id() } }}); + Assignments.update(slackerAsst.asstId, { + $push: { instances: { id: Random.id() } } + }); - TestUtils.connCallbacks.sessionReconnect({userId: slackerAsst.userId}); + TestUtils.connCallbacks.sessionReconnect({ userId: slackerAsst.userId }); - TestUtils.sleep(150); + TestUtils.sleep(150); - // ensure that user is still in lobby - const user = Meteor.users.findOne(slackerAsst.userId); - const instances = slackerAsst.getInstances(); + // ensure that user is still in lobby + const user = Meteor.users.findOne(slackerAsst.userId); + const instances = slackerAsst.getInstances(); - // should still be in lobby - test.equal(user.turkserver.state, "lobby"); - test.length(instances, 1); - return test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 1); -}) + // should still be in lobby + test.equal(user.turkserver.state, "lobby"); + test.length(instances, 1); + return test.equal(LobbyStatus.find({ batchId: batch.batchId }).count(), 1); + }) ); /* Multi-group assigner */ -Tinytest.add("assigners - tutorialMultiGroup - initial lobby gets tutorial", withCleanup(function(test) { - const assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( - tutorialTreatments, multiGroupTreatments, [16, 16]); - batch.setAssigner(assigner); - - const asst = createAssignment(); - TestUtils.connCallbacks.sessionReconnect({userId: asst.userId}); - - TestUtils.sleep(150); // YES!! - - const user = Meteor.users.findOne(asst.userId); - const instances = asst.getInstances(); - - // should be in experiment - test.equal(user.turkserver.state, "experiment"); - test.length(instances, 1); - // should not be in lobby - test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 0); - // should be in a tutorial - const exp = Experiments.findOne(instances[0].id); - return test.equal(exp.treatments, tutorialTreatments); -}) +Tinytest.add( + "assigners - tutorialMultiGroup - initial lobby gets tutorial", + withCleanup(function(test) { + const assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( + tutorialTreatments, + multiGroupTreatments, + [16, 16] + ); + batch.setAssigner(assigner); + + const asst = createAssignment(); + TestUtils.connCallbacks.sessionReconnect({ userId: asst.userId }); + + TestUtils.sleep(150); // YES!! + + const user = Meteor.users.findOne(asst.userId); + const instances = asst.getInstances(); + + // should be in experiment + test.equal(user.turkserver.state, "experiment"); + test.length(instances, 1); + // should not be in lobby + test.equal(LobbyStatus.find({ batchId: batch.batchId }).count(), 0); + // should be in a tutorial + const exp = Experiments.findOne(instances[0].id); + return test.equal(exp.treatments, tutorialTreatments); + }) ); -Tinytest.add("assigners - tutorialMultiGroup - resumes from partial", withCleanup(function(test) { - let conf, instance, j; - let asc1, end1; - const groupArr = [ 1, 1, 1, 1, 2, 2, 4, 4, 8, 16, 32, 16, 8, 4, 4, 2, 2, 1, 1, 1, 1 ]; +Tinytest.add( + "assigners - tutorialMultiGroup - resumes from partial", + withCleanup(function(test) { + let conf, instance, j; + let asc1, end1; + const groupArr = [1, 1, 1, 1, 2, 2, 4, 4, 8, 16, 32, 16, 8, 4, 4, 2, 2, 1, 1, 1, 1]; + + const assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( + tutorialTreatments, + multiGroupTreatments, + groupArr + ); + + // Generate the config that the group assigner would have + const groupConfigMulti = TurkServer.Assigners.TutorialMultiGroupAssigner.generateConfig( + groupArr, + multiGroupTreatments + ); + + const borkedGroup = 10; + const filledAmount = 16; + + // Say we are in the middle of the group of 32: index 10 + for (let i = 0; i < groupConfigMulti.length; i++) { + var asc, end; + conf = groupConfigMulti[i]; + if (i === borkedGroup) { + break; + } - const assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( - tutorialTreatments, multiGroupTreatments, groupArr); + instance = batch.createInstance(conf.treatments); + instance.setup(); - // Generate the config that the group assigner would have - const groupConfigMulti = TurkServer.Assigners.TutorialMultiGroupAssigner - .generateConfig(groupArr, multiGroupTreatments); + for (j = 1, end = conf.size, asc = 1 <= end; asc ? j <= end : j >= end; asc ? j++ : j--) { + const asst = createAssignment(); - const borkedGroup = 10; - const filledAmount = 16; + // Pretend like this instance did the tutorial + const tutorialInstance = batch.createInstance(tutorialTreatments); + tutorialInstance.setup(); + tutorialInstance.addAssignment(asst); + tutorialInstance.teardown(); - // Say we are in the middle of the group of 32: index 10 - for (let i = 0; i < groupConfigMulti.length; i++) { - var asc, end; - conf = groupConfigMulti[i]; - if (i === borkedGroup) { break; } + instance.addAssignment(asst); + } + } + conf = groupConfigMulti[borkedGroup]; instance = batch.createInstance(conf.treatments); instance.setup(); - - for (j = 1, end = conf.size, asc = 1 <= end; asc ? j <= end : j >= end; asc ? j++ : j--) { - const asst = createAssignment(); - - // Pretend like this instance did the tutorial - const tutorialInstance = batch.createInstance(tutorialTreatments); - tutorialInstance.setup(); - tutorialInstance.addAssignment(asst); - tutorialInstance.teardown(); - - instance.addAssignment(asst); + for ( + j = 1, end1 = filledAmount, asc1 = 1 <= end1; + asc1 ? j <= end1 : j >= end1; + asc1 ? j++ : j-- + ) { + instance.addAssignment(createAssignment()); } - } - - conf = groupConfigMulti[borkedGroup]; - instance = batch.createInstance(conf.treatments); - instance.setup(); - for (j = 1, end1 = filledAmount, asc1 = 1 <= end1; asc1 ? j <= end1 : j >= end1; asc1 ? j++ : j--) { instance.addAssignment(createAssignment()); } - batch.setAssigner(assigner); + batch.setAssigner(assigner); - test.equal(assigner.currentGroup, borkedGroup); - test.equal(assigner.currentInstance, instance); - return test.equal(assigner.currentFilled, filledAmount); -}) + test.equal(assigner.currentGroup, borkedGroup); + test.equal(assigner.currentInstance, instance); + return test.equal(assigner.currentFilled, filledAmount); + }) ); -Tinytest.add("assigners - tutorialMultiGroup - resumes on group boundary", withCleanup(function(test) { - let instance; - const groupArr = [ 1, 1, 1, 1, 2, 2, 4, 4, 8, 16, 32, 16, 8, 4, 4, 2, 2, 1, 1, 1, 1 ]; - - const assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( - tutorialTreatments, multiGroupTreatments, groupArr); - - // Generate the config that the group assigner would have - const groupConfigMulti = TurkServer.Assigners.TutorialMultiGroupAssigner - .generateConfig(groupArr, multiGroupTreatments); - - const borkedGroup = 2; - - // Say we are in the middle of the group of 32: index 10 - for (let i = 0; i < groupConfigMulti.length; i++) { - const conf = groupConfigMulti[i]; - if (i === borkedGroup) { break; } +Tinytest.add( + "assigners - tutorialMultiGroup - resumes on group boundary", + withCleanup(function(test) { + let instance; + const groupArr = [1, 1, 1, 1, 2, 2, 4, 4, 8, 16, 32, 16, 8, 4, 4, 2, 2, 1, 1, 1, 1]; + + const assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( + tutorialTreatments, + multiGroupTreatments, + groupArr + ); + + // Generate the config that the group assigner would have + const groupConfigMulti = TurkServer.Assigners.TutorialMultiGroupAssigner.generateConfig( + groupArr, + multiGroupTreatments + ); + + const borkedGroup = 2; + + // Say we are in the middle of the group of 32: index 10 + for (let i = 0; i < groupConfigMulti.length; i++) { + const conf = groupConfigMulti[i]; + if (i === borkedGroup) { + break; + } - instance = batch.createInstance(conf.treatments); - instance.setup(); + instance = batch.createInstance(conf.treatments); + instance.setup(); - for (let j = 1, end = conf.size, asc = 1 <= end; asc ? j <= end : j >= end; asc ? j++ : j--) { - const asst = createAssignment(); + for (let j = 1, end = conf.size, asc = 1 <= end; asc ? j <= end : j >= end; asc ? j++ : j--) { + const asst = createAssignment(); - // Pretend like this instance did the tutorial - const tutorialInstance = batch.createInstance(tutorialTreatments); - tutorialInstance.setup(); - tutorialInstance.addAssignment(asst); - tutorialInstance.teardown(); + // Pretend like this instance did the tutorial + const tutorialInstance = batch.createInstance(tutorialTreatments); + tutorialInstance.setup(); + tutorialInstance.addAssignment(asst); + tutorialInstance.teardown(); - instance.addAssignment(asst); + instance.addAssignment(asst); + } } - } - batch.setAssigner(assigner); + batch.setAssigner(assigner); - test.equal(assigner.currentGroup, borkedGroup - 1); - test.equal(assigner.currentInstance, instance); - test.equal(assigner.currentFilled, groupConfigMulti[borkedGroup - 1].size); + test.equal(assigner.currentGroup, borkedGroup - 1); + test.equal(assigner.currentInstance, instance); + test.equal(assigner.currentFilled, groupConfigMulti[borkedGroup - 1].size); - // Test reconfiguration into new groups - const newArray = [16, 16]; - assigner.configure(newArray); + // Test reconfiguration into new groups + const newArray = [16, 16]; + assigner.configure(newArray); - test.equal(assigner.groupArray, newArray); - test.equal(assigner.currentGroup, -1); - test.equal(assigner.currentInstance, null); - return test.equal(assigner.currentFilled, 0); -}) + test.equal(assigner.groupArray, newArray); + test.equal(assigner.currentGroup, -1); + test.equal(assigner.currentInstance, null); + return test.equal(assigner.currentFilled, 0); + }) ); -Tinytest.add("assigners - tutorialMultiGroup - send to exit survey", withCleanup(function(test) { - const assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( - tutorialTreatments, multiGroupTreatments, [16, 16]); - - batch.setAssigner(assigner); - - const asst = createAssignment(); - // Pretend we already have two instances done - Assignments.update(asst.asstId, { - $push: { - instances: { - $each: [ - { id: Random.id() }, - { id: Random.id() } - ] +Tinytest.add( + "assigners - tutorialMultiGroup - send to exit survey", + withCleanup(function(test) { + const assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( + tutorialTreatments, + multiGroupTreatments, + [16, 16] + ); + + batch.setAssigner(assigner); + + const asst = createAssignment(); + // Pretend we already have two instances done + Assignments.update(asst.asstId, { + $push: { + instances: { + $each: [{ id: Random.id() }, { id: Random.id() }] + } } - } - }); + }); - TestUtils.connCallbacks.sessionReconnect({userId: asst.userId}); + TestUtils.connCallbacks.sessionReconnect({ userId: asst.userId }); - TestUtils.sleep(100); + TestUtils.sleep(100); - const user = Meteor.users.findOne(asst.userId); - const instances = asst.getInstances(); + const user = Meteor.users.findOne(asst.userId); + const instances = asst.getInstances(); - test.equal(user.turkserver.state, "exitsurvey"); - return test.length(instances, 2); -}) + test.equal(user.turkserver.state, "exitsurvey"); + return test.length(instances, 2); + }) ); -Tinytest.add("assigners - tutorialMultiGroup - simultaneous multiple assignment", withCleanup(function(test) { - let asst; - let i; - const groupArr = [ 1, 1, 1, 1, 2, 2, 4, 4, 8, 16, 32 ]; +Tinytest.add( + "assigners - tutorialMultiGroup - simultaneous multiple assignment", + withCleanup(function(test) { + let asst; + let i; + const groupArr = [1, 1, 1, 1, 2, 2, 4, 4, 8, 16, 32]; - const assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( - tutorialTreatments, multiGroupTreatments, groupArr); + const assigner = new TurkServer.Assigners.TutorialMultiGroupAssigner( + tutorialTreatments, + multiGroupTreatments, + groupArr + ); - batch.setAssigner(assigner); + batch.setAssigner(assigner); - // Get the config that the group assigner would have - const groupConfigMulti = assigner.groupConfig; + // Get the config that the group assigner would have + const groupConfigMulti = assigner.groupConfig; - const assts = ((() => { - const result = []; - for (i = 1; i <= 80; i++) { - result.push(createAssignment()); + const assts = (() => { + const result = []; + for (i = 1; i <= 80; i++) { + result.push(createAssignment()); + } + return result; + })(); + + // Pretend they have all done the tutorial + for (asst of Array.from(assts)) { + Assignments.update(asst.asstId, { + $push: { instances: { id: Random.id() } } + }); } - return result; - })()); - // Pretend they have all done the tutorial - for (asst of Array.from(assts)) { - Assignments.update(asst.asstId, - {$push: { instances: { id: Random.id() } }}); - } - - // Make them all join simultaneously - lobby join is deferred - for (asst of Array.from(assts)) { - // TODO some sort of weirdness (write fence?) prevents us from deferring these - TestUtils.connCallbacks.sessionReconnect({userId: asst.userId}); - } + // Make them all join simultaneously - lobby join is deferred + for (asst of Array.from(assts)) { + // TODO some sort of weirdness (write fence?) prevents us from deferring these + TestUtils.connCallbacks.sessionReconnect({ userId: asst.userId }); + } - TestUtils.sleep(500); // Give enough time for lobby functions to process + TestUtils.sleep(500); // Give enough time for lobby functions to process - const exps = Experiments.find({batchId: batch.batchId}, {sort: {startTime: 1}}).fetch(); + const exps = Experiments.find({ batchId: batch.batchId }, { sort: { startTime: 1 } }).fetch(); - // Check that the groups have the right size and treatments - i = 0; - while (i < groupConfigMulti.length) { - const group = groupConfigMulti[i]; - const exp = exps[i]; + // Check that the groups have the right size and treatments + i = 0; + while (i < groupConfigMulti.length) { + const group = groupConfigMulti[i]; + const exp = exps[i]; - test.equal(exp.treatments[0], group.treatments[0]); - test.equal(exp.treatments[1], group.treatments[1]); + test.equal(exp.treatments[0], group.treatments[0]); + test.equal(exp.treatments[1], group.treatments[1]); - test.equal(exp.users.length, group.size); + test.equal(exp.users.length, group.size); - i++; - } + i++; + } - test.length(exps, groupConfigMulti.length); + test.length(exps, groupConfigMulti.length); - // Should have people in lobby - test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 8); + // Should have people in lobby + test.equal(LobbyStatus.find({ batchId: batch.batchId }).count(), 8); - // Test auto-stopping - const lastInstance = TurkServer.Instance.getInstance(exps[exps.length - 1]._id); - lastInstance.teardown(); + // Test auto-stopping + const lastInstance = TurkServer.Instance.getInstance(exps[exps.length - 1]._id); + lastInstance.teardown(); - const slackerAsst = createAssignment(); + const slackerAsst = createAssignment(); - Assignments.update(slackerAsst.asstId, - {$push: { instances: { id: Random.id() } }}); + Assignments.update(slackerAsst.asstId, { + $push: { instances: { id: Random.id() } } + }); - TestUtils.connCallbacks.sessionReconnect({userId: slackerAsst.userId}); + TestUtils.connCallbacks.sessionReconnect({ userId: slackerAsst.userId }); - TestUtils.sleep(150); + TestUtils.sleep(150); - // assigner should have stopped - test.equal(assigner.stopped, true); + // assigner should have stopped + test.equal(assigner.stopped, true); - // ensure that user is still in lobby - const user = Meteor.users.findOne(slackerAsst.userId); - const instances = slackerAsst.getInstances(); + // ensure that user is still in lobby + const user = Meteor.users.findOne(slackerAsst.userId); + const instances = slackerAsst.getInstances(); - // should still be in lobby - test.equal(user.turkserver.state, "lobby"); - test.length(instances, 1); - test.equal(LobbyStatus.find({batchId: batch.batchId}).count(), 9); + // should still be in lobby + test.equal(user.turkserver.state, "lobby"); + test.length(instances, 1); + test.equal(LobbyStatus.find({ batchId: batch.batchId }).count(), 9); - // Test resetting, if we launch new set a different day - batch.lobby.events.emit("reset-multi-groups"); + // Test resetting, if we launch new set a different day + batch.lobby.events.emit("reset-multi-groups"); - test.equal(assigner.stopped, false); - test.equal(assigner.groupArray, groupArr); // Still same config - test.equal(assigner.currentGroup, -1); - test.equal(assigner.currentInstance, null); - return test.equal(assigner.currentFilled, 0); -}) + test.equal(assigner.stopped, false); + test.equal(assigner.groupArray, groupArr); // Still same config + test.equal(assigner.currentGroup, -1); + test.equal(assigner.currentInstance, null); + return test.equal(assigner.currentFilled, 0); + }) ); diff --git a/tests/auth_tests.js b/tests/auth_tests.js index ae79824..eed30e2 100644 --- a/tests/auth_tests.js +++ b/tests/auth_tests.js @@ -20,28 +20,28 @@ const workerId2 = "authWorkerId2"; const experimentId = "authExperimentId"; // Ensure that users with these workerIds exist -Meteor.users.upsert("authUser1", {$set: {workerId}}); -Meteor.users.upsert("authUser2", {$set: {workerId: workerId2}}); +Meteor.users.upsert("authUser1", { $set: { workerId } }); +Meteor.users.upsert("authUser2", { $set: { workerId: workerId2 } }); const authBatchId = "authBatch"; const otherBatchId = "someOtherBatch"; // Set up a dummy batch if (Batches.findOne(authBatchId) == null) { - Batches.insert({_id: authBatchId}); + Batches.insert({ _id: authBatchId }); } // Set up a dummy HIT type and HITs -HITTypes.upsert({HITTypeId: hitType}, { - $set: { - batchId: authBatchId +HITTypes.upsert( + { HITTypeId: hitType }, + { + $set: { + batchId: authBatchId + } } -} ); -HITs.upsert({HITId: hitId}, - {$set: {HITTypeId: hitType}}); -HITs.upsert({HITId: hitId2}, - {$set: {HITTypeId: hitType}}); +HITs.upsert({ HITId: hitId }, { $set: { HITTypeId: hitType } }); +HITs.upsert({ HITId: hitId2 }, { $set: { HITTypeId: hitType } }); // We can use the after wrapper here because the tests are synchronous const withCleanup = TestUtils.getCleanupWrapper({ @@ -53,412 +53,451 @@ const withCleanup = TestUtils.getCleanupWrapper({ }, after() { // Only remove assignments created here to avoid side effects on server-client tests - return Assignments.remove({$or: [ {batchId: authBatchId}, {batchId: otherBatchId} ]}); + return Assignments.remove({ + $or: [{ batchId: authBatchId }, { batchId: otherBatchId }] + }); } }); -Tinytest.add("auth - with first time hit assignment", withCleanup(function(test) { - const asst = TestUtils.authenticateWorker({ - batchId: authBatchId, - hitId, - assignmentId, - workerId - }); - - // Test in-memory stored values - test.equal(asst.batchId, authBatchId); - test.equal(asst.hitId, hitId); - test.equal(asst.assignmentId, assignmentId); - test.equal(asst.workerId, workerId); - test.equal(asst.userId, "authUser1"); - - // Test database storage - const record = Assignments.findOne({ - hitId, - assignmentId - }); - - test.isTrue(record); - test.equal(record.workerId, workerId, "workerId not saved"); - return test.equal(record.batchId, authBatchId); -}) +Tinytest.add( + "auth - with first time hit assignment", + withCleanup(function(test) { + const asst = TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId, + assignmentId, + workerId + }); + + // Test in-memory stored values + test.equal(asst.batchId, authBatchId); + test.equal(asst.hitId, hitId); + test.equal(asst.assignmentId, assignmentId); + test.equal(asst.workerId, workerId); + test.equal(asst.userId, "authUser1"); + + // Test database storage + const record = Assignments.findOne({ + hitId, + assignmentId + }); + + test.isTrue(record); + test.equal(record.workerId, workerId, "workerId not saved"); + return test.equal(record.batchId, authBatchId); + }) ); -Tinytest.add("auth - reject incorrect batch", withCleanup(function(test) { - const testFunc = () => TestUtils.authenticateWorker({ - batchId: otherBatchId, - hitId, - assignmentId, - workerId - }); +Tinytest.add( + "auth - reject incorrect batch", + withCleanup(function(test) { + const testFunc = () => + TestUtils.authenticateWorker({ + batchId: otherBatchId, + hitId, + assignmentId, + workerId + }); + + return test.throws(testFunc, e => e.error === 403 && e.reason === ErrMsg.unexpectedBatch); + }) +); - return test.throws(testFunc, e => (e.error === 403) && (e.reason === ErrMsg.unexpectedBatch)); -}) +Tinytest.add( + "auth - connection to inactive batch is rejected", + withCleanup(function(test) { + // Active is set to back to true on cleanup + Batches.update(authBatchId, { $unset: { active: false } }); + + const testFunc = () => + TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId, + assignmentId, + workerId + }); + + return test.throws(testFunc, e => e.error === 403 && e.reason === ErrMsg.batchInactive); + }) ); -Tinytest.add("auth - connection to inactive batch is rejected", withCleanup(function(test) { - // Active is set to back to true on cleanup - Batches.update(authBatchId, {$unset: {active: false}}); +Tinytest.add( + "auth - reconnect - with existing hit assignment", + withCleanup(function(test) { + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, + status: "assigned" + }); - const testFunc = () => TestUtils.authenticateWorker({ - batchId: authBatchId, - hitId, - assignmentId, - workerId - }); + // This needs to return an assignment + const asst = TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId, + assignmentId, + workerId + }); - return test.throws(testFunc, e => (e.error === 403) && (e.reason === ErrMsg.batchInactive)); -}) -); + const record = Assignments.findOne({ + hitId, + assignmentId, + workerId + }); + + test.equal(asst, TurkServer.Assignment.getAssignment(record._id)); + test.equal(asst.batchId, authBatchId); + test.equal(asst.hitId, hitId); + test.equal(asst.assignmentId, assignmentId); + test.equal(asst.workerId, workerId); + test.equal(asst.userId, "authUser1"); -Tinytest.add("auth - reconnect - with existing hit assignment", withCleanup(function(test) { - Assignments.insert({ - batchId: authBatchId, - hitId, - assignmentId, - workerId, - status: "assigned" - }); - - // This needs to return an assignment - const asst = TestUtils.authenticateWorker({ - batchId: authBatchId, - hitId, - assignmentId, - workerId - }); - - const record = Assignments.findOne({ - hitId, - assignmentId, - workerId - }); - - test.equal(asst, TurkServer.Assignment.getAssignment(record._id)); - test.equal(asst.batchId, authBatchId); - test.equal(asst.hitId, hitId); - test.equal(asst.assignmentId, assignmentId); - test.equal(asst.workerId, workerId); - test.equal(asst.userId, "authUser1"); - - return test.equal(record.status, "assigned"); -}) + return test.equal(record.status, "assigned"); + }) ); -Tinytest.add("auth - reconnect - with existing hit after batch is inactive", withCleanup(function(test) { - // Active is set to back to true on cleanup - Batches.update(authBatchId, {$unset: {active: false}}); - - Assignments.insert({ - batchId: authBatchId, - hitId, - assignmentId, - workerId, - status: "assigned" - }); - - TestUtils.authenticateWorker({ - batchId: authBatchId, - hitId, - assignmentId, - workerId - }); - - const record = Assignments.findOne({ - hitId, - assignmentId, - workerId - }); - - return test.equal(record.status, "assigned"); -}) +Tinytest.add( + "auth - reconnect - with existing hit after batch is inactive", + withCleanup(function(test) { + // Active is set to back to true on cleanup + Batches.update(authBatchId, { $unset: { active: false } }); + + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, + status: "assigned" + }); + + TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId, + assignmentId, + workerId + }); + + const record = Assignments.findOne({ + hitId, + assignmentId, + workerId + }); + + return test.equal(record.status, "assigned"); + }) ); -Tinytest.add("auth - with overlapping hit in experiment", withCleanup(function(test) { - Assignments.insert({ - batchId: authBatchId, - hitId, - assignmentId, - workerId, - status: "assigned", - experimentId - }); - - // Authenticate with different worker - const asst = TestUtils.authenticateWorker({ - batchId: authBatchId, - hitId, - assignmentId, - workerId: workerId2 - }); - - const prevRecord = Assignments.findOne({ - hitId, - assignmentId, - workerId - }); - - const newRecord = Assignments.findOne({ - hitId, - assignmentId, - workerId: workerId2 - }); - - test.isTrue(asst); - test.equal(asst, TurkServer.Assignment.getAssignment(newRecord._id)); - - test.equal(prevRecord.status, "returned"); - - return test.equal(newRecord.status, "assigned"); -}) +Tinytest.add( + "auth - with overlapping hit in experiment", + withCleanup(function(test) { + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, + status: "assigned", + experimentId + }); + + // Authenticate with different worker + const asst = TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId, + assignmentId, + workerId: workerId2 + }); + + const prevRecord = Assignments.findOne({ + hitId, + assignmentId, + workerId + }); + + const newRecord = Assignments.findOne({ + hitId, + assignmentId, + workerId: workerId2 + }); + + test.isTrue(asst); + test.equal(asst, TurkServer.Assignment.getAssignment(newRecord._id)); + + test.equal(prevRecord.status, "returned"); + + return test.equal(newRecord.status, "assigned"); + }) ); -Tinytest.add("auth - with overlapping hit completed", withCleanup(function(test) { - // This case should not happen often - Assignments.insert({ - batchId: authBatchId, - hitId, - assignmentId, - workerId, - status: "completed" - }); - - // Authenticate with different worker - const asst = TestUtils.authenticateWorker({ - batchId: authBatchId, - hitId, - assignmentId, - workerId: workerId2 - }); - - const prevRecord = Assignments.findOne({ - hitId, - assignmentId, - workerId - }); - - const newRecord = Assignments.findOne({ - hitId, - assignmentId, - workerId: workerId2 - }); - - test.isTrue(asst); - test.equal(asst, TurkServer.Assignment.getAssignment(newRecord._id)); - - test.equal(prevRecord.status, "completed"); - - return test.equal(newRecord.status, "assigned"); -}) +Tinytest.add( + "auth - with overlapping hit completed", + withCleanup(function(test) { + // This case should not happen often + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, + status: "completed" + }); + + // Authenticate with different worker + const asst = TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId, + assignmentId, + workerId: workerId2 + }); + + const prevRecord = Assignments.findOne({ + hitId, + assignmentId, + workerId + }); + + const newRecord = Assignments.findOne({ + hitId, + assignmentId, + workerId: workerId2 + }); + + test.isTrue(asst); + test.equal(asst, TurkServer.Assignment.getAssignment(newRecord._id)); + + test.equal(prevRecord.status, "completed"); + + return test.equal(newRecord.status, "assigned"); + }) ); -Tinytest.add("auth - same worker completed hit", withCleanup(function(test) { - Assignments.insert({ - batchId: authBatchId, - hitId, - assignmentId, - workerId, - status: "completed" - }); - - const testFunc = () => TestUtils.authenticateWorker({ - batchId: authBatchId, - hitId, - assignmentId, - workerId - }); - - return test.throws(testFunc, e => (e.error === 403) && (e.reason === ErrMsg.alreadyCompleted)); -}) +Tinytest.add( + "auth - same worker completed hit", + withCleanup(function(test) { + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, + status: "completed" + }); + + const testFunc = () => + TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId, + assignmentId, + workerId + }); + + return test.throws(testFunc, e => e.error === 403 && e.reason === ErrMsg.alreadyCompleted); + }) ); -Tinytest.add("auth - limit - concurrent across hits", withCleanup(function(test) { - Assignments.insert({ - batchId: authBatchId, - hitId, - assignmentId, - workerId, - status: "assigned" - }); - - const testFunc = () => TestUtils.authenticateWorker({ - batchId: authBatchId, - hitId: hitId2, - assignmentId : assignmentId2, - workerId - }); - - return test.throws(testFunc, e => (e.error === 403) && (e.reason === ErrMsg.simultaneousLimit)); -}) +Tinytest.add( + "auth - limit - concurrent across hits", + withCleanup(function(test) { + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, + status: "assigned" + }); + + const testFunc = () => + TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId: hitId2, + assignmentId: assignmentId2, + workerId + }); + + return test.throws(testFunc, e => e.error === 403 && e.reason === ErrMsg.simultaneousLimit); + }) ); // Not sure this test needs to exist because only 1 assignment per worker for a HIT -Tinytest.add("auth - limit - concurrent across assts", withCleanup(function(test) { - Assignments.insert({ - batchId: authBatchId, - hitId, - assignmentId, - workerId, - status: "assigned" - }); - - const testFunc = () => TestUtils.authenticateWorker({ - batchId: authBatchId, - hitId, - assignmentId : assignmentId2, - workerId - }); - - return test.throws(testFunc, e => (e.error === 403) && (e.reason === ErrMsg.simultaneousLimit)); -}) +Tinytest.add( + "auth - limit - concurrent across assts", + withCleanup(function(test) { + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, + status: "assigned" + }); + + const testFunc = () => + TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId, + assignmentId: assignmentId2, + workerId + }); + + return test.throws(testFunc, e => e.error === 403 && e.reason === ErrMsg.simultaneousLimit); + }) ); -Tinytest.add("auth - limit - too many total", withCleanup(function(test) { - Assignments.insert({ - batchId: authBatchId, - hitId, - assignmentId, - workerId, - status: "completed" - }); - // Should not trigger concurrent limit - - const testFunc = () => TestUtils.authenticateWorker({ - batchId: authBatchId, - hitId: hitId2, - assignmentId : assignmentId2, - workerId - }); - - return test.throws(testFunc, e => (e.error === 403) && (e.reason === ErrMsg.batchLimit)); -}) +Tinytest.add( + "auth - limit - too many total", + withCleanup(function(test) { + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, + status: "completed" + }); + // Should not trigger concurrent limit + + const testFunc = () => + TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId: hitId2, + assignmentId: assignmentId2, + workerId + }); + + return test.throws(testFunc, e => e.error === 403 && e.reason === ErrMsg.batchLimit); + }) ); -Tinytest.add("auth - limit - returns not allowed in batch", withCleanup(function(test) { - Assignments.insert({ - batchId: authBatchId, - hitId, - assignmentId, - workerId, - status: "returned" - }); - // Should not trigger concurrent limit - - const testFunc = () => TestUtils.authenticateWorker({ - batchId: authBatchId, - hitId: hitId2, - assignmentId : assignmentId2, - workerId - }); - - return test.throws(testFunc, e => (e.error === 403) && (e.reason === ErrMsg.batchLimit)); -}) +Tinytest.add( + "auth - limit - returns not allowed in batch", + withCleanup(function(test) { + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, + status: "returned" + }); + // Should not trigger concurrent limit + + const testFunc = () => + TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId: hitId2, + assignmentId: assignmentId2, + workerId + }); + + return test.throws(testFunc, e => e.error === 403 && e.reason === ErrMsg.batchLimit); + }) ); -Tinytest.add("auth - limit - returns allowed in batch", withCleanup(function(test) { - Batches.update(authBatchId, {$set: {allowReturns: true}}); - - Assignments.insert({ - batchId: authBatchId, - hitId, - assignmentId, - workerId, - status: "returned" - }); - - const asst = TestUtils.authenticateWorker({ - batchId: authBatchId, - hitId: hitId2, - assignmentId : assignmentId2, - workerId - }); - - const prevRecord = Assignments.findOne({ - hitId, - assignmentId, - workerId - }); - - const newRecord = Assignments.findOne({ - hitId: hitId2, - assignmentId: assignmentId2, - workerId - }); - - test.isTrue(asst); - test.equal(asst, TurkServer.Assignment.getAssignment(newRecord._id)); - - test.equal(prevRecord.status, "returned"); - test.equal(prevRecord.batchId, authBatchId); - - test.equal(newRecord.status, "assigned"); - return test.equal(newRecord.batchId, authBatchId); -}) +Tinytest.add( + "auth - limit - returns allowed in batch", + withCleanup(function(test) { + Batches.update(authBatchId, { $set: { allowReturns: true } }); + + Assignments.insert({ + batchId: authBatchId, + hitId, + assignmentId, + workerId, + status: "returned" + }); + + const asst = TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId: hitId2, + assignmentId: assignmentId2, + workerId + }); + + const prevRecord = Assignments.findOne({ + hitId, + assignmentId, + workerId + }); + + const newRecord = Assignments.findOne({ + hitId: hitId2, + assignmentId: assignmentId2, + workerId + }); + + test.isTrue(asst); + test.equal(asst, TurkServer.Assignment.getAssignment(newRecord._id)); + + test.equal(prevRecord.status, "returned"); + test.equal(prevRecord.batchId, authBatchId); + + test.equal(newRecord.status, "assigned"); + return test.equal(newRecord.batchId, authBatchId); + }) ); -Tinytest.add("auth - limit - allowed after previous batch", withCleanup(function(test) { - Assignments.insert({ - batchId: otherBatchId, - hitId, - assignmentId, - workerId, - status: "completed" - }); +Tinytest.add( + "auth - limit - allowed after previous batch", + withCleanup(function(test) { + Assignments.insert({ + batchId: otherBatchId, + hitId, + assignmentId, + workerId, + status: "completed" + }); // Should not trigger concurrent limit - const asst = TestUtils.authenticateWorker({ - batchId: authBatchId, - hitId: hitId2, - assignmentId : assignmentId2, - workerId - }); - - const prevRecord = Assignments.findOne({ - hitId, - assignmentId, - workerId - }); - - const newRecord = Assignments.findOne({ - hitId: hitId2, - assignmentId: assignmentId2, - workerId - }); - - test.isTrue(asst); - test.equal(asst, TurkServer.Assignment.getAssignment(newRecord._id)); - - test.equal(prevRecord.status, "completed"); - test.equal(prevRecord.batchId, "someOtherBatch"); - - test.equal(newRecord.status, "assigned"); - return test.equal(newRecord.batchId, authBatchId); -}) + const asst = TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId: hitId2, + assignmentId: assignmentId2, + workerId + }); + + const prevRecord = Assignments.findOne({ + hitId, + assignmentId, + workerId + }); + + const newRecord = Assignments.findOne({ + hitId: hitId2, + assignmentId: assignmentId2, + workerId + }); + + test.isTrue(asst); + test.equal(asst, TurkServer.Assignment.getAssignment(newRecord._id)); + + test.equal(prevRecord.status, "completed"); + test.equal(prevRecord.batchId, "someOtherBatch"); + + test.equal(newRecord.status, "assigned"); + return test.equal(newRecord.batchId, authBatchId); + }) ); // Worker is used for the test below -Meteor.users.upsert("testWorker", {$set: {workerId: "testingWorker"}}); - -Tinytest.add("auth - testing HIT login doesn't require existing HIT", withCleanup(function(test) { - const asst = TestUtils.authenticateWorker({ - batchId: authBatchId, - hitId: "testingHIT", - assignmentId: "testingAsst", - workerId: "testingWorker", - test: true - }); - - // Test database storage - const record = Assignments.findOne({ - hitId: "testingHIT", - assignmentId: "testingAsst" - }); - - test.isTrue(asst); - test.equal(asst, TurkServer.Assignment.getAssignment(record._id)); - - test.isTrue(record); - test.equal(record.workerId, "testingWorker"); - return test.equal(record.batchId, authBatchId); -}) +Meteor.users.upsert("testWorker", { $set: { workerId: "testingWorker" } }); + +Tinytest.add( + "auth - testing HIT login doesn't require existing HIT", + withCleanup(function(test) { + const asst = TestUtils.authenticateWorker({ + batchId: authBatchId, + hitId: "testingHIT", + assignmentId: "testingAsst", + workerId: "testingWorker", + test: true + }); + + // Test database storage + const record = Assignments.findOne({ + hitId: "testingHIT", + assignmentId: "testingAsst" + }); + + test.isTrue(asst); + test.equal(asst, TurkServer.Assignment.getAssignment(record._id)); + + test.isTrue(record); + test.equal(record.workerId, "testingWorker"); + return test.equal(record.batchId, authBatchId); + }) ); diff --git a/tests/connection_tests.js b/tests/connection_tests.js index 1f7e7b2..d833124 100644 --- a/tests/connection_tests.js +++ b/tests/connection_tests.js @@ -18,7 +18,7 @@ const workerId = "connectionWorkerId"; const userId = "connectionUserId"; -Meteor.users.upsert(userId, {$set: {workerId}}); +Meteor.users.upsert(userId, { $set: { workerId } }); let asst = null; @@ -26,14 +26,15 @@ const instanceId = "connectionInstance"; const instance = batch.createInstance(); // Create an assignment. Should only be used at most once per test case. -const createAssignment = () => TurkServer.Assignment.createAssignment({ - batchId, - hitId, - assignmentId, - workerId, - acceptTime: new Date(), - status: "assigned" -}); +const createAssignment = () => + TurkServer.Assignment.createAssignment({ + batchId, + hitId, + assignmentId, + workerId, + acceptTime: new Date(), + status: "assigned" + }); const withCleanup = TestUtils.getCleanupWrapper({ before() {}, @@ -43,281 +44,315 @@ const withCleanup = TestUtils.getCleanupWrapper({ // Clear user group Partitioner.clearUserGroup(userId); // Clear any assignments we created - Assignments.remove({batchId}); + Assignments.remove({ batchId }); // Unset user state return Meteor.users.update(userId, { $unset: { "turkserver.state": null } - } - ); + }); } }); -Tinytest.add("connection - get existing assignment creates and preserves object", withCleanup(function(test) { - const asstId = Assignments.insert({ - batchId, - hitId, - assignmentId, - workerId, - acceptTime: new Date(), - status: "assigned" - }); - - asst = TurkServer.Assignment.getAssignment(asstId); - const asst2 = TurkServer.Assignment.getAssignment(asstId); - - return test.equal(asst2, asst); -}) +Tinytest.add( + "connection - get existing assignment creates and preserves object", + withCleanup(function(test) { + const asstId = Assignments.insert({ + batchId, + hitId, + assignmentId, + workerId, + acceptTime: new Date(), + status: "assigned" + }); + + asst = TurkServer.Assignment.getAssignment(asstId); + const asst2 = TurkServer.Assignment.getAssignment(asstId); + + return test.equal(asst2, asst); + }) ); -Tinytest.add("connection - assignment object preserved upon creation", withCleanup(function(test) { - asst = createAssignment(); - const asst2 = TurkServer.Assignment.getAssignment(asst.asstId); +Tinytest.add( + "connection - assignment object preserved upon creation", + withCleanup(function(test) { + asst = createAssignment(); + const asst2 = TurkServer.Assignment.getAssignment(asst.asstId); - return test.equal(asst2, asst); -}) + return test.equal(asst2, asst); + }) ); -Tinytest.add("connection - get active user assignment", withCleanup(function(test) { - asst = createAssignment(); - const asst2 = TurkServer.Assignment.getCurrentUserAssignment(asst.userId); +Tinytest.add( + "connection - get active user assignment", + withCleanup(function(test) { + asst = createAssignment(); + const asst2 = TurkServer.Assignment.getCurrentUserAssignment(asst.userId); - return test.equal(asst2, asst); -}) + return test.equal(asst2, asst); + }) ); -Tinytest.add("connection - assignment removed from cache after return", withCleanup(function(test) { - asst = createAssignment(); - asst.setReturned(); +Tinytest.add( + "connection - assignment removed from cache after return", + withCleanup(function(test) { + asst = createAssignment(); + asst.setReturned(); - // Let cache cleanup do its thing - TestUtils.sleep(200); + // Let cache cleanup do its thing + TestUtils.sleep(200); - return test.isUndefined(TurkServer.Assignment.getCurrentUserAssignment(asst.userId)); -}) + return test.isUndefined(TurkServer.Assignment.getCurrentUserAssignment(asst.userId)); + }) ); -Tinytest.add("connection - assignment removed from cache after completion", withCleanup(function(test) { - asst = createAssignment(); - asst.showExitSurvey(); - asst.setCompleted({}); +Tinytest.add( + "connection - assignment removed from cache after completion", + withCleanup(function(test) { + asst = createAssignment(); + asst.showExitSurvey(); + asst.setCompleted({}); - // Let cache cleanup do its thing - TestUtils.sleep(200); + // Let cache cleanup do its thing + TestUtils.sleep(200); - return test.isUndefined(TurkServer.Assignment.getCurrentUserAssignment(asst.userId)); -}) + return test.isUndefined(TurkServer.Assignment.getCurrentUserAssignment(asst.userId)); + }) ); -Tinytest.add("connection - user added to lobby", withCleanup(function(test) { - asst = createAssignment(); - TestUtils.connCallbacks.sessionReconnect({ userId }); +Tinytest.add( + "connection - user added to lobby", + withCleanup(function(test) { + asst = createAssignment(); + TestUtils.connCallbacks.sessionReconnect({ userId }); - const lobbyUsers = batch.lobby.getAssignments(); - const user = Meteor.users.findOne(userId); + const lobbyUsers = batch.lobby.getAssignments(); + const user = Meteor.users.findOne(userId); - test.equal(lobbyUsers.length, 1); - test.equal(lobbyUsers[0], asst); - test.equal(lobbyUsers[0].userId, userId); + test.equal(lobbyUsers.length, 1); + test.equal(lobbyUsers[0], asst); + test.equal(lobbyUsers[0].userId, userId); - return test.equal(user.turkserver.state, "lobby"); -}) + return test.equal(user.turkserver.state, "lobby"); + }) ); -Tinytest.add("connection - user disconnecting and reconnecting to lobby", withCleanup(function(test) { - asst = createAssignment(); +Tinytest.add( + "connection - user disconnecting and reconnecting to lobby", + withCleanup(function(test) { + asst = createAssignment(); - TestUtils.connCallbacks.sessionReconnect({ userId }); + TestUtils.connCallbacks.sessionReconnect({ userId }); - TestUtils.connCallbacks.sessionDisconnect({ userId }); + TestUtils.connCallbacks.sessionDisconnect({ userId }); - let lobbyUsers = batch.lobby.getAssignments(); - let user = Meteor.users.findOne(userId); + let lobbyUsers = batch.lobby.getAssignments(); + let user = Meteor.users.findOne(userId); - test.equal(lobbyUsers.length, 0); - test.equal(user.turkserver.state, "lobby"); + test.equal(lobbyUsers.length, 0); + test.equal(user.turkserver.state, "lobby"); - TestUtils.connCallbacks.sessionReconnect({ userId }); + TestUtils.connCallbacks.sessionReconnect({ userId }); - lobbyUsers = batch.lobby.getAssignments(); - user = Meteor.users.findOne(userId); + lobbyUsers = batch.lobby.getAssignments(); + user = Meteor.users.findOne(userId); - test.equal(lobbyUsers.length, 1); - test.equal(lobbyUsers[0], asst); - test.equal(lobbyUsers[0].userId, userId); - return test.equal(user.turkserver.state, "lobby"); -}) + test.equal(lobbyUsers.length, 1); + test.equal(lobbyUsers[0], asst); + test.equal(lobbyUsers[0].userId, userId); + return test.equal(user.turkserver.state, "lobby"); + }) ); -Tinytest.add("connection - user sent to exit survey", withCleanup(function(test) { - asst = createAssignment(); - asst.showExitSurvey(); +Tinytest.add( + "connection - user sent to exit survey", + withCleanup(function(test) { + asst = createAssignment(); + asst.showExitSurvey(); - const user = Meteor.users.findOne(userId); + const user = Meteor.users.findOne(userId); - return test.equal(user.turkserver.state, "exitsurvey"); -}) + return test.equal(user.turkserver.state, "exitsurvey"); + }) ); -Tinytest.add("connection - user submitting HIT", withCleanup(function(test) { - asst = createAssignment(); +Tinytest.add( + "connection - user submitting HIT", + withCleanup(function(test) { + asst = createAssignment(); - Meteor.users.update(userId, { - $set: { - "turkserver.state": "exitsurvey" - } - } - ); + Meteor.users.update(userId, { + $set: { + "turkserver.state": "exitsurvey" + } + }); - const exitData = {foo: "bar"}; + const exitData = { foo: "bar" }; - asst.setCompleted( exitData ); + asst.setCompleted(exitData); - const user = Meteor.users.findOne(userId); - const asstData = Assignments.findOne(asst.asstId); + const user = Meteor.users.findOne(userId); + const asstData = Assignments.findOne(asst.asstId); - test.isFalse(user.turkserver != null ? user.turkserver.state : undefined); + test.isFalse(user.turkserver != null ? user.turkserver.state : undefined); - test.isTrue(asst.isCompleted()); - test.equal(asstData.status, "completed"); - test.instanceOf(asstData.submitTime, Date); - return test.equal(asstData.exitdata, exitData); -}) + test.isTrue(asst.isCompleted()); + test.equal(asstData.status, "completed"); + test.instanceOf(asstData.submitTime, Date); + return test.equal(asstData.exitdata, exitData); + }) ); -Tinytest.add("connection - improper submission of HIT", withCleanup(function(test) { - asst = createAssignment(); +Tinytest.add( + "connection - improper submission of HIT", + withCleanup(function(test) { + asst = createAssignment(); - return test.throws(() => asst.setCompleted({}) - , e => (e.error === 403) && (e.reason === ErrMsg.stateErr)); -}) + return test.throws( + () => asst.setCompleted({}), + e => e.error === 403 && e.reason === ErrMsg.stateErr + ); + }) ); -Tinytest.add("connection - set assignment as returned", withCleanup(function(test) { - asst = createAssignment(); - TestUtils.connCallbacks.sessionReconnect({ userId }); +Tinytest.add( + "connection - set assignment as returned", + withCleanup(function(test) { + asst = createAssignment(); + TestUtils.connCallbacks.sessionReconnect({ userId }); - asst.setReturned(); + asst.setReturned(); - const user = Meteor.users.findOne(userId); - const asstData = Assignments.findOne(asst.asstId); + const user = Meteor.users.findOne(userId); + const asstData = Assignments.findOne(asst.asstId); - test.equal(asstData.status, "returned"); - return test.isFalse(user.turkserver != null ? user.turkserver.state : undefined); -}) + test.equal(asstData.status, "returned"); + return test.isFalse(user.turkserver != null ? user.turkserver.state : undefined); + }) ); -Tinytest.add("connection - user resuming into instance", withCleanup(function(test) { - asst = createAssignment(); - instance.addAssignment(asst); - TestUtils.connCallbacks.sessionReconnect({ userId }); +Tinytest.add( + "connection - user resuming into instance", + withCleanup(function(test) { + asst = createAssignment(); + instance.addAssignment(asst); + TestUtils.connCallbacks.sessionReconnect({ userId }); - const user = Meteor.users.findOne(userId); + const user = Meteor.users.findOne(userId); - test.equal(batch.lobby.getAssignments().length, 0); - return test.equal(user.turkserver.state, "experiment"); -}) + test.equal(batch.lobby.getAssignments().length, 0); + return test.equal(user.turkserver.state, "experiment"); + }) ); -Tinytest.add("connection - user resuming into exit survey", withCleanup(function(test) { - asst = createAssignment(); - Meteor.users.update(userId, { - $set: { - "turkserver.state": "exitsurvey" - } - } - ); +Tinytest.add( + "connection - user resuming into exit survey", + withCleanup(function(test) { + asst = createAssignment(); + Meteor.users.update(userId, { + $set: { + "turkserver.state": "exitsurvey" + } + }); - TestUtils.connCallbacks.sessionReconnect({ userId }); + TestUtils.connCallbacks.sessionReconnect({ userId }); - const user = Meteor.users.findOne(userId); + const user = Meteor.users.findOne(userId); - test.equal(batch.lobby.getAssignments().length, 0); - return test.equal(user.turkserver.state, "exitsurvey"); -}) + test.equal(batch.lobby.getAssignments().length, 0); + return test.equal(user.turkserver.state, "exitsurvey"); + }) ); -Tinytest.add("connection - set payment amount", withCleanup(function(test) { - asst = createAssignment(); - test.isFalse(asst.getPayment()); +Tinytest.add( + "connection - set payment amount", + withCleanup(function(test) { + asst = createAssignment(); + test.isFalse(asst.getPayment()); - const amount = 1.00; + const amount = 1.0; - asst.setPayment(amount); - test.equal(asst.getPayment(), amount); + asst.setPayment(amount); + test.equal(asst.getPayment(), amount); - asst.addPayment(1.50); - return test.equal(asst.getPayment(), 2.50); -}) + asst.addPayment(1.5); + return test.equal(asst.getPayment(), 2.5); + }) ); -Tinytest.add("connection - increment null payment amount", withCleanup(function(test) { - asst = createAssignment(); - test.isFalse(asst.getPayment()); +Tinytest.add( + "connection - increment null payment amount", + withCleanup(function(test) { + asst = createAssignment(); + test.isFalse(asst.getPayment()); - const amount = 1.00; - asst.addPayment(amount); - return test.equal(asst.getPayment(), amount); -}) + const amount = 1.0; + asst.addPayment(amount); + return test.equal(asst.getPayment(), amount); + }) ); -Tinytest.add("connection - pay worker bonus", withCleanup(function(test) { - asst = createAssignment(); +Tinytest.add( + "connection - pay worker bonus", + withCleanup(function(test) { + asst = createAssignment(); - test.isFalse(asst._data().bonusPaid); + test.isFalse(asst._data().bonusPaid); - const amount = 10.00; - asst.setPayment(amount); + const amount = 10.0; + asst.setPayment(amount); - const message = "Thanks for your work!"; - asst.payBonus(message); + const message = "Thanks for your work!"; + asst.payBonus(message); - test.equal(TestUtils.mturkAPI.op, "GrantBonus"); - test.equal(TestUtils.mturkAPI.params.WorkerId, asst.workerId); - test.equal(TestUtils.mturkAPI.params.AssignmentId, asst.assignmentId); - test.equal(TestUtils.mturkAPI.params.BonusAmount.Amount, amount); - test.equal(TestUtils.mturkAPI.params.BonusAmount.CurrencyCode, "USD"); - test.equal(TestUtils.mturkAPI.params.Reason, message); + test.equal(TestUtils.mturkAPI.op, "GrantBonus"); + test.equal(TestUtils.mturkAPI.params.WorkerId, asst.workerId); + test.equal(TestUtils.mturkAPI.params.AssignmentId, asst.assignmentId); + test.equal(TestUtils.mturkAPI.params.BonusAmount.Amount, amount); + test.equal(TestUtils.mturkAPI.params.BonusAmount.CurrencyCode, "USD"); + test.equal(TestUtils.mturkAPI.params.Reason, message); - const asstData = asst._data(); - test.equal(asstData.bonusPayment, amount); - test.equal(asstData.bonusMessage, message); - return test.instanceOf(asstData.bonusPaid, Date); -}) + const asstData = asst._data(); + test.equal(asstData.bonusPayment, amount); + test.equal(asstData.bonusMessage, message); + return test.instanceOf(asstData.bonusPaid, Date); + }) ); -Tinytest.add("connection - throw on set/inc payment when bonus paid", withCleanup(function(test) { - asst = createAssignment(); +Tinytest.add( + "connection - throw on set/inc payment when bonus paid", + withCleanup(function(test) { + asst = createAssignment(); - Assignments.update(asst.asstId, { - $set: { - bonusPayment: 0.01, - bonusPaid: new Date, - bonusMessage: "blah" - } - } - ); + Assignments.update(asst.asstId, { + $set: { + bonusPayment: 0.01, + bonusPaid: new Date(), + bonusMessage: "blah" + } + }); - const amount = 1.00; + const amount = 1.0; - test.throws(() => asst.setPayment(amount)); - test.equal(asst.getPayment(), 0.01); + test.throws(() => asst.setPayment(amount)); + test.equal(asst.getPayment(), 0.01); - test.throws(() => asst.addPayment(1.50)); - return test.equal(asst.getPayment(), 0.01); -}) + test.throws(() => asst.addPayment(1.5)); + return test.equal(asst.getPayment(), 0.01); + }) ); -Tinytest.add("connection - throw on double payments", withCleanup(function(test) { - asst = createAssignment(); +Tinytest.add( + "connection - throw on double payments", + withCleanup(function(test) { + asst = createAssignment(); - const amount = 10.00; - asst.setPayment(amount); + const amount = 10.0; + asst.setPayment(amount); - const message = "Thanks for your work!"; - asst.payBonus(message); + const message = "Thanks for your work!"; + asst.payBonus(message); - return test.throws(() => asst.payBonus(message)); -}) + return test.throws(() => asst.payBonus(message)); + }) ); diff --git a/tests/experiment_client_tests.js b/tests/experiment_client_tests.js index 9530dc1..41a0a77 100644 --- a/tests/experiment_client_tests.js +++ b/tests/experiment_client_tests.js @@ -9,7 +9,6 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ if (Meteor.isServer) { - // Set up a treatment for testing TurkServer.ensureTreatmentExists({ name: "expWorldTreatment", @@ -23,7 +22,9 @@ if (Meteor.isServer) { // Some functions to make sure things are set up for the client login Accounts.validateLoginAttempt(function(info) { - if (!info.allowed) { return; } // Don't handle if login is being rejected + if (!info.allowed) { + return; + } // Don't handle if login is being rejected const userId = info.user._id; Partitioner.clearUserGroup(userId); // Remove any previous user group @@ -38,14 +39,13 @@ if (Meteor.isServer) { // Reset assignment for this worker Assignments.upsert(asst.asstId, { - $unset: { instances: null, - $unset: { treatments: null - } - } - } - ); + $unset: { instances: null, $unset: { treatments: null } } + }); - asst.getBatch().createInstance(["expWorldTreatment"]).addAssignment(asst); + asst + .getBatch() + .createInstance(["expWorldTreatment"]) + .addAssignment(asst); asst.addTreatment("expUserTreatment"); @@ -55,11 +55,11 @@ if (Meteor.isServer) { Meteor.methods({ getAssignmentData() { const userId = Meteor.userId(); - if (!userId) { throw new Meteor.Error(500, "Not logged in"); } - const { - workerId - } = Meteor.users.findOne(userId); - return Assignments.findOne({workerId, status: "assigned"}); + if (!userId) { + throw new Meteor.Error(500, "Not logged in"); + } + const { workerId } = Meteor.users.findOne(userId); + return Assignments.findOne({ workerId, status: "assigned" }); }, setAssignmentPayment(amount) { @@ -72,7 +72,7 @@ if (Meteor.isServer) { status: "assigned" }; - if (!(Assignments.update(selector, {$set: {instances: arr}}) > 0)) { + if (!(Assignments.update(selector, { $set: { instances: arr } }) > 0)) { throw new Meteor.Error(400, "Could not find assignment to update"); } }, @@ -92,23 +92,34 @@ if (Meteor.isClient) { const big_tol = 500; // max range we tolerate in a round trip to the server (async method) const expectedTreatment = { - fooProperty: "bar", // world - foo2: "baz" // user + fooProperty: "bar", // world + foo2: "baz" // user }; - const checkTreatments = (test, obj) => (() => { - const result = []; - for (let k in expectedTreatment) { - const v = expectedTreatment[k]; - result.push(test.equal(obj[k], v, `for key ${k} actual value ${obj[k]} doesn't match expected value ${v}`)); - } - return result; - })(); - - Tinytest.addAsync("experiment - client - login and creation of assignment metadata", (test, next) => InsecureLogin.ready(function() { - test.isTrue(Meteor.userId()); - return next(); - })); + const checkTreatments = (test, obj) => + (() => { + const result = []; + for (let k in expectedTreatment) { + const v = expectedTreatment[k]; + result.push( + test.equal( + obj[k], + v, + `for key ${k} actual value ${obj[k]} doesn't match expected value ${v}` + ) + ); + } + return result; + })(); + + Tinytest.addAsync( + "experiment - client - login and creation of assignment metadata", + (test, next) => + InsecureLogin.ready(function() { + test.isTrue(Meteor.userId()); + return next(); + }) + ); Tinytest.addAsync("experiment - client - IP address saved", function(test, next) { let returned = false; @@ -118,7 +129,12 @@ if (Meteor.isClient) { console.log("Got assignment data", JSON.stringify(res)); test.isTrue(__guard__(res != null ? res.ipAddr : undefined, x => x[0])); - if (Package['test-in-console'] == null) { test.equal(__guard__(res != null ? res.userAgent : undefined, x1 => x1[0]), navigator.userAgent); } + if (Package["test-in-console"] == null) { + test.equal( + __guard__(res != null ? res.userAgent : undefined, x1 => x1[0]), + navigator.userAgent + ); + } return next(); }); @@ -128,10 +144,13 @@ if (Meteor.isClient) { return next(); }; - return simplePoll((() => returned), (function() {}), fail, 2000); + return simplePoll(() => returned, function() {}, fail, 2000); }); - Tinytest.addAsync("experiment - client - received experiment and treatment", function(test, next) { + Tinytest.addAsync("experiment - client - received experiment and treatment", function( + test, + next + ) { let treatment = null; const verify = function() { @@ -164,20 +183,33 @@ if (Meteor.isClient) { }; // Poll until both treatments arrives - return simplePoll((function() { - treatment = TurkServer.treatment(); - if (treatment.treatments.length) { return true; } - }), verify, fail, 2000); + return simplePoll( + function() { + treatment = TurkServer.treatment(); + if (treatment.treatments.length) { + return true; + } + }, + verify, + fail, + 2000 + ); }); - Tinytest.addAsync("experiment - assignment - test treatments on server", (test, next) => // Even though this is a "client" test, it is testing a server function - // because assignment treatments are different on the client and server - Meteor.call("getServerTreatment", function(err, res) { - if (err != null) { test.fail(); } + Tinytest.addAsync("experiment - assignment - test treatments on server", ( + test, + next // Even though this is a "client" test, it is testing a server function + ) => + // because assignment treatments are different on the client and server + Meteor.call("getServerTreatment", function(err, res) { + if (err != null) { + test.fail(); + } - checkTreatments(test, res); - return next(); - })); + checkTreatments(test, res); + return next(); + }) + ); Tinytest.addAsync("experiment - client - current payment variable", function(test, next) { const amount = 0.42; @@ -188,7 +220,10 @@ if (Meteor.isClient) { }); }); - Tinytest.addAsync("experiment - assignment - assignment metadata and local time vars", function(test, next) { + Tinytest.addAsync("experiment - assignment - assignment metadata and local time vars", function( + test, + next + ) { let asstData = null; const verify = function() { @@ -212,10 +247,17 @@ if (Meteor.isClient) { }; // Poll until treatment data arrives - return simplePoll((function() { - asstData = Assignments.findOne(); - if (asstData != null) { return true; } - }), verify, fail, 2000); + return simplePoll( + function() { + asstData = Assignments.findOne(); + if (asstData != null) { + return true; + } + }, + verify, + fail, + 2000 + ); }); Tinytest.addAsync("experiment - assignment - no time fields", function(test, next) { @@ -269,8 +311,8 @@ if (Meteor.isClient) { const activeTime = TurkServer.Timers.activeTime(); test.isTrue(joinedTime >= 3000); - test.isTrue(joinedTime < (3000 + big_tol)); - test.isTrue(Math.abs((activeTime + 3000) - joinedTime) < tol); + test.isTrue(joinedTime < 3000 + big_tol); + test.isTrue(Math.abs(activeTime + 3000 - joinedTime) < tol); test.isTrue(activeTime >= 0); test.equal(UI._globalHelpers.tsIdleTime(), "0:00:01"); @@ -297,49 +339,52 @@ if (Meteor.isClient) { Next test edits instance fields, so client APIs may break state */ - Tinytest.addAsync(`experiment - instance - client selects correct instance of \ -multiple`, function(test, next) { - const fields = [ - { - id: Random.id(), - joinTime: new Date(TimeSync.serverTime() - (3600*1000)), - idleTime: 3000, - disconnectedTime: 5000 - }, - { - id: TurkServer.group(), - joinTime: new Date(TimeSync.serverTime() - 5000), - idleTime: 1000, - disconnectedTime: 2000 - } - ]; - - return Meteor.call("setAssignmentInstanceData", fields, function(err, res) { - test.isFalse(err); - Deps.flush(); // Help out the emboxed value thingies - - test.equal(TurkServer.Timers.idleTime(), 1000); - test.equal(TurkServer.Timers.disconnectedTime(), 2000); - - const joinedTime = TurkServer.Timers.joinedTime(); - const activeTime = TurkServer.Timers.activeTime(); - - test.isTrue(joinedTime >= 5000); - test.isTrue(joinedTime < (5000 + big_tol)); - - test.isTrue(Math.abs((activeTime + 3000) - joinedTime) < tol); - test.isTrue(activeTime >= 0); // Should not be negative - - test.equal(UI._globalHelpers.tsIdleTime(), "0:00:01"); - test.equal(UI._globalHelpers.tsDisconnectedTime(), "0:00:02"); - - return next(); - }); - }); + Tinytest.addAsync( + `experiment - instance - client selects correct instance of \ +multiple`, + function(test, next) { + const fields = [ + { + id: Random.id(), + joinTime: new Date(TimeSync.serverTime() - 3600 * 1000), + idleTime: 3000, + disconnectedTime: 5000 + }, + { + id: TurkServer.group(), + joinTime: new Date(TimeSync.serverTime() - 5000), + idleTime: 1000, + disconnectedTime: 2000 + } + ]; + + return Meteor.call("setAssignmentInstanceData", fields, function(err, res) { + test.isFalse(err); + Deps.flush(); // Help out the emboxed value thingies + + test.equal(TurkServer.Timers.idleTime(), 1000); + test.equal(TurkServer.Timers.disconnectedTime(), 2000); + + const joinedTime = TurkServer.Timers.joinedTime(); + const activeTime = TurkServer.Timers.activeTime(); + + test.isTrue(joinedTime >= 5000); + test.isTrue(joinedTime < 5000 + big_tol); + + test.isTrue(Math.abs(activeTime + 3000 - joinedTime) < tol); + test.isTrue(activeTime >= 0); // Should not be negative + + test.equal(UI._globalHelpers.tsIdleTime(), "0:00:01"); + test.equal(UI._globalHelpers.tsDisconnectedTime(), "0:00:02"); + + return next(); + }); + } + ); } - // TODO: add a test for submitting HIT and verify that resume token is removed +// TODO: add a test for submitting HIT and verify that resume token is removed function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== "undefined" && value !== null ? transform(value) : undefined; +} diff --git a/tests/experiment_tests.js b/tests/experiment_tests.js index cb98546..f1a9397 100644 --- a/tests/experiment_tests.js +++ b/tests/experiment_tests.js @@ -19,21 +19,35 @@ let disconnectContext = undefined; let idleContext = undefined; let activeContext = undefined; -TurkServer.initialize(function() { return setupContext = this; }); +TurkServer.initialize(function() { + return (setupContext = this); +}); TurkServer.initialize(function() { Doobie.insert({ - foo: "bar"}); + foo: "bar" + }); // Test deferred insert - return Meteor.defer(() => Doobie.insert({ - bar: "baz"})); + return Meteor.defer(() => + Doobie.insert({ + bar: "baz" + }) + ); }); -TurkServer.onConnect(function() { return reconnectContext = this; }); -TurkServer.onDisconnect(function() { return disconnectContext = this; }); -TurkServer.onIdle(function() { return idleContext = this; }); -TurkServer.onActive(function() { return activeContext = this; }); +TurkServer.onConnect(function() { + return (reconnectContext = this); +}); +TurkServer.onDisconnect(function() { + return (disconnectContext = this); +}); +TurkServer.onIdle(function() { + return (idleContext = this); +}); +TurkServer.onActive(function() { + return (activeContext = this); +}); // Ensure batch exists const batchId = "expBatch"; @@ -49,10 +63,13 @@ const batch = TurkServer.Batch.getBatch("expBatch"); const createAssignment = function() { const workerId = Random.id(); - const userId = Accounts.insertUserDoc({}, { - workerId, - turkserver: {state: "lobby"} // Created user goes in lobby - }); + const userId = Accounts.insertUserDoc( + {}, + { + workerId, + turkserver: { state: "lobby" } // Created user goes in lobby + } + ); return TurkServer.Assignment.createAssignment({ batchId: "expBatch", hitId: Random.id(), @@ -70,550 +87,606 @@ const withCleanup = TestUtils.getCleanupWrapper({ reconnectContext = undefined; disconnectContext = undefined; idleContext = undefined; - return activeContext = undefined; + return (activeContext = undefined); }, after() { // Delete assignments Assignments.remove({ batchId: "expBatch" }); // Delete generated log entries - Experiments.find({ batchId: "expBatch" }).forEach(exp => Logs.remove({_groupId: exp._id})); + Experiments.find({ batchId: "expBatch" }).forEach(exp => Logs.remove({ _groupId: exp._id })); // Delete experiments Experiments.remove({ batchId: "expBatch" }); // Clear contents of partitioned collection return Doobie.direct.remove({}); - }}); + } +}); -const lastLog = groupId => Logs.findOne({_groupId: groupId}, {sort: {_timestamp: -1}}); +const lastLog = groupId => Logs.findOne({ _groupId: groupId }, { sort: { _timestamp: -1 } }); -Tinytest.add("experiment - batch - creation and retrieval", withCleanup(function(test) { - // First get should create, second get should return same object - // TODO: this test will only run as intended on the first try - const batch2 = TurkServer.Batch.getBatch("expBatch"); +Tinytest.add( + "experiment - batch - creation and retrieval", + withCleanup(function(test) { + // First get should create, second get should return same object + // TODO: this test will only run as intended on the first try + const batch2 = TurkServer.Batch.getBatch("expBatch"); - return test.equal(batch2, batch); -}) + return test.equal(batch2, batch); + }) ); -Tinytest.add(`experiment - assignment - currentAssignment in standalone \ -server code returns null`, test => test.equal(TurkServer.Assignment.currentAssignment(), null)); +Tinytest.add( + `experiment - assignment - currentAssignment in standalone \ +server code returns null`, + test => test.equal(TurkServer.Assignment.currentAssignment(), null) +); -Tinytest.add("experiment - instance - throws error if doesn't exist", withCleanup(test => test.throws(() => TurkServer.Instance.getInstance("yabbadabbadoober"))) +Tinytest.add( + "experiment - instance - throws error if doesn't exist", + withCleanup(test => test.throws(() => TurkServer.Instance.getInstance("yabbadabbadoober"))) ); -Tinytest.add("experiment - instance - create", withCleanup(function(test) { - const treatments = [ "fooTreatment" ]; +Tinytest.add( + "experiment - instance - create", + withCleanup(function(test) { + const treatments = ["fooTreatment"]; - // Create a new id to test specified ID - const serverInstanceId = Random.id(); + // Create a new id to test specified ID + const serverInstanceId = Random.id(); - const instance = batch.createInstance(treatments, {_id: serverInstanceId}); - test.equal(instance.groupId, serverInstanceId); - test.instanceOf(instance, TurkServer.Instance); + const instance = batch.createInstance(treatments, { + _id: serverInstanceId + }); + test.equal(instance.groupId, serverInstanceId); + test.instanceOf(instance, TurkServer.Instance); - // Batch and treatments recorded - no start time until someone joins - const instanceData = Experiments.findOne(serverInstanceId); - test.equal(instanceData.batchId, "expBatch"); - test.equal(instanceData.treatments, treatments); + // Batch and treatments recorded - no start time until someone joins + const instanceData = Experiments.findOne(serverInstanceId); + test.equal(instanceData.batchId, "expBatch"); + test.equal(instanceData.treatments, treatments); - test.isFalse(instanceData.startTime); + test.isFalse(instanceData.startTime); - // Test that create meta event was recorded in log - const logEntry = lastLog(serverInstanceId); - test.isTrue(logEntry); - test.equal(logEntry != null ? logEntry._meta : undefined, "created"); + // Test that create meta event was recorded in log + const logEntry = lastLog(serverInstanceId); + test.isTrue(logEntry); + test.equal(logEntry != null ? logEntry._meta : undefined, "created"); - // Getting the instance again should get the same one - const inst2 = TurkServer.Instance.getInstance(serverInstanceId); - return test.equal(inst2, instance); -}) + // Getting the instance again should get the same one + const inst2 = TurkServer.Instance.getInstance(serverInstanceId); + return test.equal(inst2, instance); + }) ); -Tinytest.add("experiment - instance - setup context", withCleanup(function(test) { - const treatments = [ "fooTreatment" ]; - const instance = batch.createInstance(treatments); - TestUtils.sleep(10); // Enforce different log timestamp - instance.setup(); - - test.isTrue(setupContext); - const treatment = setupContext != null ? setupContext.instance.treatment() : undefined; - - test.equal(instance.batch(), TurkServer.Batch.getBatch("expBatch")); - - test.isTrue(treatment); - test.isTrue(Array.from(treatment.treatments).includes("fooTreatment"), - test.equal(treatment.fooProperty, "bar")); - test.equal(setupContext != null ? setupContext.instance.groupId : undefined, instance.groupId); - - // Check that the init _meta event was logged with treatment info - const logEntry = lastLog(instance.groupId); - test.isTrue(logEntry); - test.equal(logEntry != null ? logEntry._meta : undefined, "initialized"); - test.equal(logEntry != null ? logEntry.treatmentData : undefined, treatment); - test.equal(logEntry != null ? logEntry.treatmentData.treatments : undefined, treatments); - return test.equal(logEntry != null ? logEntry.treatmentData.fooProperty : undefined, "bar"); -}) +Tinytest.add( + "experiment - instance - setup context", + withCleanup(function(test) { + const treatments = ["fooTreatment"]; + const instance = batch.createInstance(treatments); + TestUtils.sleep(10); // Enforce different log timestamp + instance.setup(); + + test.isTrue(setupContext); + const treatment = setupContext != null ? setupContext.instance.treatment() : undefined; + + test.equal(instance.batch(), TurkServer.Batch.getBatch("expBatch")); + + test.isTrue(treatment); + test.isTrue( + Array.from(treatment.treatments).includes("fooTreatment"), + test.equal(treatment.fooProperty, "bar") + ); + test.equal(setupContext != null ? setupContext.instance.groupId : undefined, instance.groupId); + + // Check that the init _meta event was logged with treatment info + const logEntry = lastLog(instance.groupId); + test.isTrue(logEntry); + test.equal(logEntry != null ? logEntry._meta : undefined, "initialized"); + test.equal(logEntry != null ? logEntry.treatmentData : undefined, treatment); + test.equal(logEntry != null ? logEntry.treatmentData.treatments : undefined, treatments); + return test.equal(logEntry != null ? logEntry.treatmentData.fooProperty : undefined, "bar"); + }) ); -Tinytest.add("experiment - instance - teardown and log", withCleanup(function(test) { - const instance = batch.createInstance([]); - instance.setup(); - TestUtils.sleep(10); // Enforce different log timestamp - instance.teardown(); - - const logEntry = lastLog(instance.groupId); - test.isTrue(logEntry); - test.equal(logEntry != null ? logEntry._meta : undefined, "teardown"); - - const instanceData = Experiments.findOne(instance.groupId); - return test.instanceOf(instanceData.endTime, Date); -}) +Tinytest.add( + "experiment - instance - teardown and log", + withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); + TestUtils.sleep(10); // Enforce different log timestamp + instance.teardown(); + + const logEntry = lastLog(instance.groupId); + test.isTrue(logEntry); + test.equal(logEntry != null ? logEntry._meta : undefined, "teardown"); + + const instanceData = Experiments.findOne(instance.groupId); + return test.instanceOf(instanceData.endTime, Date); + }) ); -Tinytest.add("experiment - instance - get treatment on server", withCleanup(function(test) { - const instance = batch.createInstance(["fooTreatment"]); +Tinytest.add( + "experiment - instance - get treatment on server", + withCleanup(function(test) { + const instance = batch.createInstance(["fooTreatment"]); - // Note this only tests world treatments. Assignment treatments have to be - // tested with the janky client setup. + // Note this only tests world treatments. Assignment treatments have to be + // tested with the janky client setup. - // However, This also tests accessing server treatments outside of a client context. - instance.bindOperation(function() { - const treatment = TurkServer.treatment(); - test.equal(treatment.treatments[0], "fooTreatment"); - return test.equal(treatment.fooProperty, "bar"); - }); + // However, This also tests accessing server treatments outside of a client context. + instance.bindOperation(function() { + const treatment = TurkServer.treatment(); + test.equal(treatment.treatments[0], "fooTreatment"); + return test.equal(treatment.fooProperty, "bar"); + }); - // Undefined outside of an experiment instance - const treatment = TurkServer.treatment(); - return test.equal(treatment.fooProperty, undefined); -}) + // Undefined outside of an experiment instance + const treatment = TurkServer.treatment(); + return test.equal(treatment.fooProperty, undefined); + }) ); -Tinytest.add("experiment - instance - global group", withCleanup(function(test) { - const instance = batch.createInstance([]); - instance.setup(); // Inserts two items - - TestUtils.sleep(100); // Let deferred insert finish - - instance.bindOperation(() => Doobie.insert({ - foo2: "bar"})); - - const stuff = Partitioner.directOperation(() => Doobie.find().fetch()); - - test.length(stuff, 3); - - // Setup insert - test.equal(stuff[0].foo, "bar"); - test.equal(stuff[0]._groupId, instance.groupId); - // Deferred insert - test.equal(stuff[1].bar, "baz"); - test.equal(stuff[1]._groupId, instance.groupId); - // Bound insert - test.equal(stuff[2].foo2, "bar"); - return test.equal(stuff[2]._groupId, instance.groupId); -}) +Tinytest.add( + "experiment - instance - global group", + withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); // Inserts two items + + TestUtils.sleep(100); // Let deferred insert finish + + instance.bindOperation(() => + Doobie.insert({ + foo2: "bar" + }) + ); + + const stuff = Partitioner.directOperation(() => Doobie.find().fetch()); + + test.length(stuff, 3); + + // Setup insert + test.equal(stuff[0].foo, "bar"); + test.equal(stuff[0]._groupId, instance.groupId); + // Deferred insert + test.equal(stuff[1].bar, "baz"); + test.equal(stuff[1]._groupId, instance.groupId); + // Bound insert + test.equal(stuff[2].foo2, "bar"); + return test.equal(stuff[2]._groupId, instance.groupId); + }) ); -Tinytest.add("experiment - assignment - reject adding user to ended instance", withCleanup(function(test) { - const instance = batch.createInstance([]); - instance.setup(); +Tinytest.add( + "experiment - assignment - reject adding user to ended instance", + withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - instance.teardown(); + instance.teardown(); - const asst = createAssignment(); + const asst = createAssignment(); - test.throws(() => instance.addAssignment(asst)); + test.throws(() => instance.addAssignment(asst)); - const user = Meteor.users.findOne(asst.userId); - const asstData = Assignments.findOne(asst.asstId); + const user = Meteor.users.findOne(asst.userId); + const asstData = Assignments.findOne(asst.asstId); - test.isFalse(Partitioner.getUserGroup(asst.userId)); - test.length(instance.users(), 0); - test.equal(user.turkserver.state, "lobby"); + test.isFalse(Partitioner.getUserGroup(asst.userId)); + test.length(instance.users(), 0); + test.equal(user.turkserver.state, "lobby"); - return test.isFalse(asstData.instances); -}) + return test.isFalse(asstData.instances); + }) ); -Tinytest.add("experiment - assignment - addAssignment records start time and instance id", withCleanup(function(test) { - let needle; - const instance = batch.createInstance([]); - instance.setup(); +Tinytest.add( + "experiment - assignment - addAssignment records start time and instance id", + withCleanup(function(test) { + let needle; + const instance = batch.createInstance([]); + instance.setup(); - const asst = createAssignment(); + const asst = createAssignment(); - instance.addAssignment(asst); + instance.addAssignment(asst); - const user = Meteor.users.findOne(asst.userId); - const asstData = Assignments.findOne(asst.asstId); - const instanceData = Experiments.findOne(instance.groupId); + const user = Meteor.users.findOne(asst.userId); + const asstData = Assignments.findOne(asst.asstId); + const instanceData = Experiments.findOne(instance.groupId); - test.equal(Partitioner.getUserGroup(asst.userId), instance.groupId); + test.equal(Partitioner.getUserGroup(asst.userId), instance.groupId); - test.isTrue((needle = asst.userId, Array.from(instance.users()).includes(needle))); - test.instanceOf(instanceData.startTime, Date); + test.isTrue(((needle = asst.userId), Array.from(instance.users()).includes(needle))); + test.instanceOf(instanceData.startTime, Date); - test.equal(user.turkserver.state, "experiment"); - test.instanceOf(asstData.instances, Array); + test.equal(user.turkserver.state, "experiment"); + test.instanceOf(asstData.instances, Array); - test.isTrue(asstData.instances[0]); - test.equal(asstData.instances[0].id, instance.groupId); - return test.isTrue(asstData.instances[0].joinTime); -}) + test.isTrue(asstData.instances[0]); + test.equal(asstData.instances[0].id, instance.groupId); + return test.isTrue(asstData.instances[0].joinTime); + }) ); -Tinytest.add("experiment - assignment - second addAssignment does not change date", withCleanup(function(test) { - const instance = batch.createInstance([]); - instance.setup(); +Tinytest.add( + "experiment - assignment - second addAssignment does not change date", + withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - const asst = createAssignment(); + const asst = createAssignment(); - instance.addAssignment(asst); + instance.addAssignment(asst); - let instanceData = Experiments.findOne(instance.groupId); - test.instanceOf(instanceData.startTime, Date); + let instanceData = Experiments.findOne(instance.groupId); + test.instanceOf(instanceData.startTime, Date); - const startedDate = instanceData.startTime; + const startedDate = instanceData.startTime; - TestUtils.sleep(10); - // Add a second user - const asst2 = createAssignment(); - instance.addAssignment(asst2); + TestUtils.sleep(10); + // Add a second user + const asst2 = createAssignment(); + instance.addAssignment(asst2); - instanceData = Experiments.findOne(instance.groupId); - // Should be the same date as originally - return test.equal(instanceData.startTime, startedDate); -}) + instanceData = Experiments.findOne(instance.groupId); + // Should be the same date as originally + return test.equal(instanceData.startTime, startedDate); + }) ); -Tinytest.add("experiment - assignment - teardown with returned assignment", withCleanup(function(test) { - const instance = batch.createInstance([]); - instance.setup(); +Tinytest.add( + "experiment - assignment - teardown with returned assignment", + withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - const asst = createAssignment(); + const asst = createAssignment(); - instance.addAssignment(asst); + instance.addAssignment(asst); - asst.setReturned(); + asst.setReturned(); - instance.teardown(); // This should not throw + instance.teardown(); // This should not throw - const user = Meteor.users.findOne(asst.userId); - const asstData = Assignments.findOne(asst.asstId); + const user = Meteor.users.findOne(asst.userId); + const asstData = Assignments.findOne(asst.asstId); - test.isFalse(Partitioner.getUserGroup(asst.userId)); - test.isFalse(user.turkserver != null ? user.turkserver.state : undefined); - test.isTrue(asstData.instances[0]); - return test.equal(asstData.status, "returned"); -}) + test.isFalse(Partitioner.getUserGroup(asst.userId)); + test.isFalse(user.turkserver != null ? user.turkserver.state : undefined); + test.isTrue(asstData.instances[0]); + return test.equal(asstData.status, "returned"); + }) ); -Tinytest.add("experiment - assignment - user disconnect and reconnect", withCleanup(function(test) { - const instance = batch.createInstance([]); - instance.setup(); +Tinytest.add( + "experiment - assignment - user disconnect and reconnect", + withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - const asst = createAssignment(); + const asst = createAssignment(); - instance.addAssignment(asst); + instance.addAssignment(asst); - TestUtils.connCallbacks.sessionDisconnect({ - userId: asst.userId}); + TestUtils.connCallbacks.sessionDisconnect({ + userId: asst.userId + }); - test.isTrue(disconnectContext); - test.equal(disconnectContext != null ? disconnectContext.event : undefined, "disconnected"); - test.equal(disconnectContext != null ? disconnectContext.instance : undefined, instance); - test.equal(disconnectContext != null ? disconnectContext.userId : undefined, asst.userId); + test.isTrue(disconnectContext); + test.equal(disconnectContext != null ? disconnectContext.event : undefined, "disconnected"); + test.equal(disconnectContext != null ? disconnectContext.instance : undefined, instance); + test.equal(disconnectContext != null ? disconnectContext.userId : undefined, asst.userId); - let asstData = Assignments.findOne(asst.asstId); + let asstData = Assignments.findOne(asst.asstId); - // TODO ensure the accounting here is done correctly - let discTime = null; + // TODO ensure the accounting here is done correctly + let discTime = null; - test.isTrue(asstData.instances[0]); - test.isTrue(asstData.instances[0].joinTime); - test.isTrue((discTime = asstData.instances[0].lastDisconnect)); + test.isTrue(asstData.instances[0]); + test.isTrue(asstData.instances[0].joinTime); + test.isTrue((discTime = asstData.instances[0].lastDisconnect)); - TestUtils.connCallbacks.sessionReconnect({ - userId: asst.userId}); + TestUtils.connCallbacks.sessionReconnect({ + userId: asst.userId + }); - test.isTrue(reconnectContext); - test.equal(reconnectContext != null ? reconnectContext.event : undefined, "connected"); - test.equal(reconnectContext != null ? reconnectContext.instance : undefined, instance); - test.equal(reconnectContext != null ? reconnectContext.userId : undefined, asst.userId); + test.isTrue(reconnectContext); + test.equal(reconnectContext != null ? reconnectContext.event : undefined, "connected"); + test.equal(reconnectContext != null ? reconnectContext.instance : undefined, instance); + test.equal(reconnectContext != null ? reconnectContext.userId : undefined, asst.userId); - asstData = Assignments.findOne(asst.asstId); - test.isFalse(asstData.instances[0].lastDisconnect); - // We don't know the exact length of disconnection, but make sure it's in the right ballpark - test.isTrue(asstData.instances[0].disconnectedTime > 0); - return test.isTrue(asstData.instances[0].disconnectedTime < (Date.now() - discTime)); -}) + asstData = Assignments.findOne(asst.asstId); + test.isFalse(asstData.instances[0].lastDisconnect); + // We don't know the exact length of disconnection, but make sure it's in the right ballpark + test.isTrue(asstData.instances[0].disconnectedTime > 0); + return test.isTrue(asstData.instances[0].disconnectedTime < Date.now() - discTime); + }) ); -Tinytest.add("experiment - assignment - user idle and re-activate", withCleanup(function(test) { - const instance = batch.createInstance([]); - instance.setup(); +Tinytest.add( + "experiment - assignment - user idle and re-activate", + withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - const asst = createAssignment(); + const asst = createAssignment(); - instance.addAssignment(asst); + instance.addAssignment(asst); - const idleTime = new Date(); + const idleTime = new Date(); - TestUtils.connCallbacks.sessionIdle({ - userId: asst.userId, - lastActivity: idleTime - }); + TestUtils.connCallbacks.sessionIdle({ + userId: asst.userId, + lastActivity: idleTime + }); - test.isTrue(idleContext); - test.equal(idleContext != null ? idleContext.event : undefined, "idle"); - test.equal(idleContext != null ? idleContext.instance : undefined, instance); - test.equal(idleContext != null ? idleContext.userId : undefined, asst.userId); + test.isTrue(idleContext); + test.equal(idleContext != null ? idleContext.event : undefined, "idle"); + test.equal(idleContext != null ? idleContext.instance : undefined, instance); + test.equal(idleContext != null ? idleContext.userId : undefined, asst.userId); - let asstData = Assignments.findOne(asst.asstId); - test.isTrue(asstData.instances[0]); - test.isTrue(asstData.instances[0].joinTime); - test.equal(asstData.instances[0].lastIdle, idleTime); + let asstData = Assignments.findOne(asst.asstId); + test.isTrue(asstData.instances[0]); + test.isTrue(asstData.instances[0].joinTime); + test.equal(asstData.instances[0].lastIdle, idleTime); - const offset = 1000; - const activeTime = new Date(idleTime.getTime() + offset); + const offset = 1000; + const activeTime = new Date(idleTime.getTime() + offset); - TestUtils.connCallbacks.sessionActive({ - userId: asst.userId, - lastActivity: activeTime - }); + TestUtils.connCallbacks.sessionActive({ + userId: asst.userId, + lastActivity: activeTime + }); - test.isTrue(activeContext); - test.equal(activeContext != null ? activeContext.event : undefined, "active"); - test.equal(activeContext != null ? activeContext.instance : undefined, instance); - test.equal(activeContext != null ? activeContext.userId : undefined, asst.userId); + test.isTrue(activeContext); + test.equal(activeContext != null ? activeContext.event : undefined, "active"); + test.equal(activeContext != null ? activeContext.instance : undefined, instance); + test.equal(activeContext != null ? activeContext.userId : undefined, asst.userId); - asstData = Assignments.findOne(asst.asstId); - test.isFalse(asstData.instances[0].lastIdle); - test.equal(asstData.instances[0].idleTime, offset); + asstData = Assignments.findOne(asst.asstId); + test.isFalse(asstData.instances[0].lastIdle); + test.equal(asstData.instances[0].idleTime, offset); - // Another bout of inactivity - const secondIdleTime = new Date(activeTime.getTime() + 5000); - const secondActiveTime = new Date(secondIdleTime.getTime() + offset); + // Another bout of inactivity + const secondIdleTime = new Date(activeTime.getTime() + 5000); + const secondActiveTime = new Date(secondIdleTime.getTime() + offset); - TestUtils.connCallbacks.sessionIdle({ - userId: asst.userId, - lastActivity: secondIdleTime - }); + TestUtils.connCallbacks.sessionIdle({ + userId: asst.userId, + lastActivity: secondIdleTime + }); - TestUtils.connCallbacks.sessionActive({ - userId: asst.userId, - lastActivity: secondActiveTime - }); + TestUtils.connCallbacks.sessionActive({ + userId: asst.userId, + lastActivity: secondActiveTime + }); - asstData = Assignments.findOne(asst.asstId); - test.isFalse(asstData.instances[0].lastIdle); - return test.equal(asstData.instances[0].idleTime, offset + offset); -}) + asstData = Assignments.findOne(asst.asstId); + test.isFalse(asstData.instances[0].lastIdle); + return test.equal(asstData.instances[0].idleTime, offset + offset); + }) ); -Tinytest.add("experiment - assignment - user disconnect while idle", withCleanup(function(test) { - const instance = batch.createInstance([]); - instance.setup(); +Tinytest.add( + "experiment - assignment - user disconnect while idle", + withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - const asst = createAssignment(); + const asst = createAssignment(); - instance.addAssignment(asst); + instance.addAssignment(asst); - const idleTime = new Date(); + const idleTime = new Date(); - TestUtils.connCallbacks.sessionIdle({ - userId: asst.userId, - lastActivity: idleTime - }); + TestUtils.connCallbacks.sessionIdle({ + userId: asst.userId, + lastActivity: idleTime + }); - TestUtils.connCallbacks.sessionDisconnect({ - userId: asst.userId}); - - const asstData = Assignments.findOne(asst.asstId); - test.isTrue(asstData.instances[0].joinTime); - // Check that idle fields exist - test.isFalse(asstData.instances[0].lastIdle); - test.isTrue(asstData.instances[0].idleTime); - // Check that disconnect fields exist - return test.isTrue(asstData.instances[0].lastDisconnect); -}) + TestUtils.connCallbacks.sessionDisconnect({ + userId: asst.userId + }); + + const asstData = Assignments.findOne(asst.asstId); + test.isTrue(asstData.instances[0].joinTime); + // Check that idle fields exist + test.isFalse(asstData.instances[0].lastIdle); + test.isTrue(asstData.instances[0].idleTime); + // Check that disconnect fields exist + return test.isTrue(asstData.instances[0].lastDisconnect); + }) ); -Tinytest.add("experiment - assignment - idleness is cleared on reconnection", withCleanup(function(test) { - const instance = batch.createInstance([]); - instance.setup(); +Tinytest.add( + "experiment - assignment - idleness is cleared on reconnection", + withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - const asst = createAssignment(); + const asst = createAssignment(); - instance.addAssignment(asst); + instance.addAssignment(asst); - const idleTime = new Date(); + const idleTime = new Date(); - TestUtils.connCallbacks.sessionDisconnect({ - userId: asst.userId}); + TestUtils.connCallbacks.sessionDisconnect({ + userId: asst.userId + }); - TestUtils.connCallbacks.sessionIdle({ - userId: asst.userId, - lastActivity: idleTime - }); + TestUtils.connCallbacks.sessionIdle({ + userId: asst.userId, + lastActivity: idleTime + }); - TestUtils.sleep(100); + TestUtils.sleep(100); - TestUtils.connCallbacks.sessionReconnect({ - userId: asst.userId}); + TestUtils.connCallbacks.sessionReconnect({ + userId: asst.userId + }); - const asstData = Assignments.findOne(asst.asstId); + const asstData = Assignments.findOne(asst.asstId); - test.isTrue(asstData.instances[0].joinTime); - // Check that idleness was not counted - test.isFalse(asstData.instances[0].lastIdle); - test.isFalse(asstData.instances[0].idleTime); - // Check that disconnect fields exist - test.isFalse(asstData.instances[0].lastDisconnect); - return test.isTrue(asstData.instances[0].disconnectedTime); -}) + test.isTrue(asstData.instances[0].joinTime); + // Check that idleness was not counted + test.isFalse(asstData.instances[0].lastIdle); + test.isFalse(asstData.instances[0].idleTime); + // Check that disconnect fields exist + test.isFalse(asstData.instances[0].lastDisconnect); + return test.isTrue(asstData.instances[0].disconnectedTime); + }) ); -Tinytest.add("experiment - assignment - teardown while disconnected", withCleanup(function(test) { - const instance = batch.createInstance([]); - instance.setup(); +Tinytest.add( + "experiment - assignment - teardown while disconnected", + withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - const asst = createAssignment(); + const asst = createAssignment(); - instance.addAssignment(asst); + instance.addAssignment(asst); - TestUtils.connCallbacks.sessionDisconnect({ - userId: asst.userId}); + TestUtils.connCallbacks.sessionDisconnect({ + userId: asst.userId + }); - let discTime = null; - let asstData = Assignments.findOne(asst.asstId); - test.isTrue(discTime = asstData.instances[0].lastDisconnect); + let discTime = null; + let asstData = Assignments.findOne(asst.asstId); + test.isTrue((discTime = asstData.instances[0].lastDisconnect)); - instance.teardown(); + instance.teardown(); - asstData = Assignments.findOne(asst.asstId); + asstData = Assignments.findOne(asst.asstId); - test.isFalse(Partitioner.getUserGroup(asst.userId)); + test.isFalse(Partitioner.getUserGroup(asst.userId)); - test.isTrue(asstData.instances[0].leaveTime); - test.isFalse(asstData.instances[0].lastDisconnect); - // We don't know the exact length of disconnection, but make sure it's in the right ballpark - test.isTrue(asstData.instances[0].disconnectedTime > 0); - return test.isTrue(asstData.instances[0].disconnectedTime < (Date.now() - discTime)); -}) + test.isTrue(asstData.instances[0].leaveTime); + test.isFalse(asstData.instances[0].lastDisconnect); + // We don't know the exact length of disconnection, but make sure it's in the right ballpark + test.isTrue(asstData.instances[0].disconnectedTime > 0); + return test.isTrue(asstData.instances[0].disconnectedTime < Date.now() - discTime); + }) ); -Tinytest.add("experiment - assignment - teardown while idle", withCleanup(function(test) { - const instance = batch.createInstance([]); - instance.setup(); +Tinytest.add( + "experiment - assignment - teardown while idle", + withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - const asst = createAssignment(); + const asst = createAssignment(); - instance.addAssignment(asst); + instance.addAssignment(asst); - const idleTime = new Date(); + const idleTime = new Date(); - TestUtils.connCallbacks.sessionIdle({ - userId: asst.userId, - lastActivity: idleTime - }); + TestUtils.connCallbacks.sessionIdle({ + userId: asst.userId, + lastActivity: idleTime + }); - instance.teardown(); + instance.teardown(); - const asstData = Assignments.findOne(asst.asstId); + const asstData = Assignments.findOne(asst.asstId); - test.isFalse(Partitioner.getUserGroup(asst.userId)); + test.isFalse(Partitioner.getUserGroup(asst.userId)); - test.isTrue(asstData.instances[0].leaveTime); - test.isFalse(asstData.instances[0].lastIdle); - return test.isTrue(asstData.instances[0].idleTime); -}) + test.isTrue(asstData.instances[0].leaveTime); + test.isFalse(asstData.instances[0].lastIdle); + return test.isTrue(asstData.instances[0].idleTime); + }) ); -Tinytest.add("experiment - assignment - leave instance after teardown", withCleanup(function(test) { - const instance = batch.createInstance([]); - instance.setup(); +Tinytest.add( + "experiment - assignment - leave instance after teardown", + withCleanup(function(test) { + const instance = batch.createInstance([]); + instance.setup(); - const asst = createAssignment(); + const asst = createAssignment(); - instance.addAssignment(asst); + instance.addAssignment(asst); - // Immediately disconnect - TestUtils.connCallbacks.sessionDisconnect({ - userId: asst.userId}); + // Immediately disconnect + TestUtils.connCallbacks.sessionDisconnect({ + userId: asst.userId + }); - instance.teardown(false); + instance.teardown(false); - // Wait a bit to ensure we have the right value; the above should have - // completed within this interval - TestUtils.sleep(200); + // Wait a bit to ensure we have the right value; the above should have + // completed within this interval + TestUtils.sleep(200); - // Could do either of the below - instance.sendUserToLobby(asst.userId); + // Could do either of the below + instance.sendUserToLobby(asst.userId); - const asstData = Assignments.findOne(asst.asstId); + const asstData = Assignments.findOne(asst.asstId); - test.isFalse(Partitioner.getUserGroup(asst.userId)); + test.isFalse(Partitioner.getUserGroup(asst.userId)); - test.isTrue(asstData.instances[0].leaveTime); - test.isFalse(asstData.instances[0].lastDisconnect); - // We don't know the exact length of disconnection, but make sure it's in the right ballpark - test.isTrue(asstData.instances[0].disconnectedTime > 0); - return test.isTrue(asstData.instances[0].disconnectedTime < 200); -}) + test.isTrue(asstData.instances[0].leaveTime); + test.isFalse(asstData.instances[0].lastDisconnect); + // We don't know the exact length of disconnection, but make sure it's in the right ballpark + test.isTrue(asstData.instances[0].disconnectedTime > 0); + return test.isTrue(asstData.instances[0].disconnectedTime < 200); + }) ); -Tinytest.add("experiment - assignment - teardown and join second instance", withCleanup(function(test) { - let needle, needle1; - const instance = batch.createInstance([]); - instance.setup(); +Tinytest.add( + "experiment - assignment - teardown and join second instance", + withCleanup(function(test) { + let needle, needle1; + const instance = batch.createInstance([]); + instance.setup(); - const asst = createAssignment(); + const asst = createAssignment(); - instance.addAssignment(asst); + instance.addAssignment(asst); - instance.teardown(); + instance.teardown(); - let user = Meteor.users.findOne(asst.userId); - let asstData = Assignments.findOne(asst.asstId); + let user = Meteor.users.findOne(asst.userId); + let asstData = Assignments.findOne(asst.asstId); - test.isFalse(Partitioner.getUserGroup(asst.userId)); + test.isFalse(Partitioner.getUserGroup(asst.userId)); - test.isTrue((needle = asst.userId, Array.from(instance.users()).includes(needle))); // Shouldn't have been removed - test.equal(user.turkserver.state, "lobby"); - test.instanceOf(asstData.instances, Array); + test.isTrue(((needle = asst.userId), Array.from(instance.users()).includes(needle))); // Shouldn't have been removed + test.equal(user.turkserver.state, "lobby"); + test.instanceOf(asstData.instances, Array); - test.isTrue(asstData.instances[0]); - test.equal(asstData.instances[0].id, instance.groupId); - test.isTrue(asstData.instances[0].joinTime); - test.isTrue(asstData.instances[0].leaveTime); + test.isTrue(asstData.instances[0]); + test.equal(asstData.instances[0].id, instance.groupId); + test.isTrue(asstData.instances[0].joinTime); + test.isTrue(asstData.instances[0].leaveTime); - const instance2 = batch.createInstance([]); - instance2.setup(); + const instance2 = batch.createInstance([]); + instance2.setup(); - instance2.addAssignment(asst); + instance2.addAssignment(asst); - user = Meteor.users.findOne(asst.userId); + user = Meteor.users.findOne(asst.userId); - test.equal(Partitioner.getUserGroup(asst.userId), instance2.groupId); - test.equal(user.turkserver.state, "experiment"); + test.equal(Partitioner.getUserGroup(asst.userId), instance2.groupId); + test.equal(user.turkserver.state, "experiment"); - instance2.teardown(); + instance2.teardown(); - user = Meteor.users.findOne(asst.userId); - asstData = Assignments.findOne(asst.asstId); + user = Meteor.users.findOne(asst.userId); + asstData = Assignments.findOne(asst.asstId); - test.isFalse(Partitioner.getUserGroup(asst.userId)); + test.isFalse(Partitioner.getUserGroup(asst.userId)); - test.isTrue((needle1 = asst.userId, Array.from(instance2.users()).includes(needle1))); // Shouldn't have been removed - test.equal(user.turkserver.state, "lobby"); - test.instanceOf(asstData.instances, Array); + test.isTrue(((needle1 = asst.userId), Array.from(instance2.users()).includes(needle1))); // Shouldn't have been removed + test.equal(user.turkserver.state, "lobby"); + test.instanceOf(asstData.instances, Array); - // Make sure array-based updates worked - test.isTrue(asstData.instances[1]); - test.equal(asstData.instances[1].id, instance2.groupId); - test.notEqual(asstData.instances[0].joinTime, asstData.instances[1].joinTime); - return test.notEqual(asstData.instances[0].leaveTime, asstData.instances[1].leaveTime); -}) + // Make sure array-based updates worked + test.isTrue(asstData.instances[1]); + test.equal(asstData.instances[1].id, instance2.groupId); + test.notEqual(asstData.instances[0].joinTime, asstData.instances[1].joinTime); + return test.notEqual(asstData.instances[0].leaveTime, asstData.instances[1].leaveTime); + }) ); diff --git a/tests/helper_tests.js b/tests/helper_tests.js index 99bc0cd..338b70c 100644 --- a/tests/helper_tests.js +++ b/tests/helper_tests.js @@ -8,15 +8,19 @@ // TODO: try implementing Meteor.isServer stuff with setUserId if (Meteor.isClient) { - Tinytest.addAsync("helpers - isAdmin", (test, next) => InsecureLogin.ready(function() { - // this should be straight up false - isFalse might take `undefined` for an answer. - test.equal(TurkServer.isAdmin(), false); - return next(); - })); + Tinytest.addAsync("helpers - isAdmin", (test, next) => + InsecureLogin.ready(function() { + // this should be straight up false - isFalse might take `undefined` for an answer. + test.equal(TurkServer.isAdmin(), false); + return next(); + }) + ); Tinytest.addAsync("helpers - checkAdmin", function(test, next) { - test.throws(() => TurkServer.checkAdmin() - , e => (e.error === 403) && (e.reason === ErrMsg.notAdminErr)); + test.throws( + () => TurkServer.checkAdmin(), + e => e.error === 403 && e.reason === ErrMsg.notAdminErr + ); return next(); }); @@ -30,6 +34,10 @@ if (Meteor.isClient) { /* Timer helper tests - server/client */ -Tinytest.add("timers - formatMillis renders 0 properly", test => test.equal(TurkServer.Util.formatMillis(0), "0:00:00")); +Tinytest.add("timers - formatMillis renders 0 properly", test => + test.equal(TurkServer.Util.formatMillis(0), "0:00:00") +); -Tinytest.add("timers - formatMillis renders negative values properly", test => test.equal(TurkServer.Util.formatMillis(-1000), "-0:00:01")); +Tinytest.add("timers - formatMillis renders negative values properly", test => + test.equal(TurkServer.Util.formatMillis(-1000), "-0:00:01") +); diff --git a/tests/insecure_login.js b/tests/insecure_login.js index 3d53186..80e7de0 100644 --- a/tests/insecure_login.js +++ b/tests/insecure_login.js @@ -1,16 +1,16 @@ InsecureLogin = { queue: [], ran: false, - ready: function (callback) { + ready: function(callback) { this.queue.push(callback); if (this.ran) this.unwind(); }, - run: function () { + run: function() { this.ran = true; this.unwind(); }, - unwind: function () { - _.each(this.queue, function (callback) { + unwind: function() { + _.each(this.queue, function(callback) { callback(); }); this.queue = []; @@ -24,14 +24,16 @@ if (Meteor.isClient) { var batchId = "expClientBatch"; Accounts.callLoginMethod({ - methodArguments: [{ - hitId: hitId, - assignmentId: assignmentId, - workerId: workerId, - batchId: batchId, - test: true - }], - userCallback: function (err) { + methodArguments: [ + { + hitId: hitId, + assignmentId: assignmentId, + workerId: workerId, + batchId: batchId, + test: true + } + ], + userCallback: function(err) { if (err) console.log(err); else { console.info("HIT login successful!"); diff --git a/tests/lobby_tests.js b/tests/lobby_tests.js index 2a536be..3634018 100644 --- a/tests/lobby_tests.js +++ b/tests/lobby_tests.js @@ -11,9 +11,7 @@ if (Meteor.isServer) { const batchId = "lobbyBatchTest"; Batches.upsert({ _id: batchId }, { _id: batchId }); - const { - lobby - } = TurkServer.Batch.getBatch(batchId); + const { lobby } = TurkServer.Batch.getBatch(batchId); const userId = "lobbyUser"; @@ -23,15 +21,18 @@ if (Meteor.isServer) { } }); - Assignments.upsert({ - batchId, - hitId: "lobbyTestHIT", - assignmentId: "lobbyTestAsst" - }, { $set: { - workerId: "lobbyTestWorker", - status: "assigned" - } -} + Assignments.upsert( + { + batchId, + hitId: "lobbyTestHIT", + assignmentId: "lobbyTestAsst" + }, + { + $set: { + workerId: "lobbyTestWorker", + status: "assigned" + } + } ); const asst = TurkServer.Assignment.getCurrentUserAssignment(userId); @@ -40,84 +41,92 @@ if (Meteor.isServer) { let changedUserId = null; let leftUserId = null; - lobby.events.on("user-join", asst => joinedUserId = asst.userId); - lobby.events.on("user-status", asst => changedUserId = asst.userId); - lobby.events.on("user-leave", asst => leftUserId = asst.userId); + lobby.events.on("user-join", asst => (joinedUserId = asst.userId)); + lobby.events.on("user-status", asst => (changedUserId = asst.userId)); + lobby.events.on("user-leave", asst => (leftUserId = asst.userId)); const withCleanup = TestUtils.getCleanupWrapper({ before() { lobby.pluckUsers([userId]); joinedUserId = null; changedUserId = null; - return leftUserId = null; + return (leftUserId = null); }, after() {} }); // Basic tests just to make sure joining/leaving works as intended - Tinytest.addAsync("lobby - add user", withCleanup(function(test, next) { - lobby.addAssignment(asst); - - return Meteor.defer(function() { - test.equal(joinedUserId, userId); - - const lobbyAssts = lobby.getAssignments(); - test.length(lobbyAssts, 1); - test.equal(lobbyAssts[0], asst); - test.equal(lobbyAssts[0].userId, userId); - - const lobbyData = LobbyStatus.findOne(userId); - test.equal(lobbyData.batchId, batchId); - test.equal(lobbyData.asstId, asst.asstId); - - return next(); - }); - }) + Tinytest.addAsync( + "lobby - add user", + withCleanup(function(test, next) { + lobby.addAssignment(asst); + + return Meteor.defer(function() { + test.equal(joinedUserId, userId); + + const lobbyAssts = lobby.getAssignments(); + test.length(lobbyAssts, 1); + test.equal(lobbyAssts[0], asst); + test.equal(lobbyAssts[0].userId, userId); + + const lobbyData = LobbyStatus.findOne(userId); + test.equal(lobbyData.batchId, batchId); + test.equal(lobbyData.asstId, asst.asstId); + + return next(); + }); + }) ); // TODO update this test for generalized lobby user state - Tinytest.addAsync("lobby - change state", withCleanup(function(test, next) { - lobby.addAssignment(asst); - lobby.toggleStatus(asst.userId); - - const lobbyUsers = lobby.getAssignments(); - test.length(lobbyUsers, 1); - test.equal(lobbyUsers[0], asst); - test.equal(lobbyUsers[0].userId, userId); - - // TODO: use better API for accessing user status - test.equal(__guard__(LobbyStatus.findOne(asst.userId), x => x.status), true); - - return Meteor.defer(function() { - test.equal(changedUserId, userId); - return next(); - }); - }) + Tinytest.addAsync( + "lobby - change state", + withCleanup(function(test, next) { + lobby.addAssignment(asst); + lobby.toggleStatus(asst.userId); + + const lobbyUsers = lobby.getAssignments(); + test.length(lobbyUsers, 1); + test.equal(lobbyUsers[0], asst); + test.equal(lobbyUsers[0].userId, userId); + + // TODO: use better API for accessing user status + test.equal(__guard__(LobbyStatus.findOne(asst.userId), x => x.status), true); + + return Meteor.defer(function() { + test.equal(changedUserId, userId); + return next(); + }); + }) ); - Tinytest.addAsync("lobby - remove user", withCleanup(function(test, next) { - lobby.addAssignment(asst); - lobby.removeAssignment(asst); + Tinytest.addAsync( + "lobby - remove user", + withCleanup(function(test, next) { + lobby.addAssignment(asst); + lobby.removeAssignment(asst); - const lobbyUsers = lobby.getAssignments(); - test.length(lobbyUsers, 0); + const lobbyUsers = lobby.getAssignments(); + test.length(lobbyUsers, 0); - return Meteor.defer(function() { - test.equal(leftUserId, userId); - return next(); - }); - }) + return Meteor.defer(function() { + test.equal(leftUserId, userId); + return next(); + }); + }) ); - Tinytest.addAsync("lobby - remove nonexistent user", withCleanup(function(test, next) { - // TODO create an assignment with some other state here - lobby.removeAssignment("rando"); - - return Meteor.defer(function() { - test.equal(leftUserId, null); - return next(); - }); - }) + Tinytest.addAsync( + "lobby - remove nonexistent user", + withCleanup(function(test, next) { + // TODO create an assignment with some other state here + lobby.removeAssignment("rando"); + + return Meteor.defer(function() { + test.equal(leftUserId, null); + return next(); + }); + }) ); } @@ -140,5 +149,5 @@ if (Meteor.isClient) { // simplePoll (-> (groupSize = TSConfig.findOne("lobbyThreshold"))? ), verify, fail, 2000 function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== "undefined" && value !== null ? transform(value) : undefined; +} diff --git a/tests/logging_tests.js b/tests/logging_tests.js index 0ded518..18fc514 100644 --- a/tests/logging_tests.js +++ b/tests/logging_tests.js @@ -8,22 +8,26 @@ */ // Server methods if (Meteor.isServer) { - const testGroup = "poop"; Meteor.methods({ // Clear anything in logs for the given group clearLogs() { let group; - if ((group = Partitioner.group()) == null) { throw new Meteor.Error(403, "no group assigned"); } - Logs.remove({ // Should be same as {}, but more explicit - _groupId: group}); + if ((group = Partitioner.group()) == null) { + throw new Meteor.Error(403, "no group assigned"); + } + Logs.remove({ + // Should be same as {}, but more explicit + _groupId: group + }); }, getLogs(selector) { let group; - if ((group = Partitioner.group()) == null) { throw new Meteor.Error(403, "no group assigned"); } - selector = _.extend((selector || {}), - {_groupId: group}); + if ((group = Partitioner.group()) == null) { + throw new Meteor.Error(403, "no group assigned"); + } + selector = _.extend(selector || {}, { _groupId: group }); return Logs.find(selector).fetch(); } }); @@ -32,10 +36,11 @@ if (Meteor.isServer) { Partitioner.bindGroup(testGroup, function() { Meteor.call("clearLogs"); return TurkServer.log({ - boo: "hoo"}); + boo: "hoo" + }); }); - const doc = Logs.findOne({boo: "hoo"}); + const doc = Logs.findOne({ boo: "hoo" }); test.equal(doc.boo, "hoo"); test.isTrue(doc._groupId); @@ -53,7 +58,7 @@ if (Meteor.isServer) { }); }); - const doc = Logs.findOne({boo: "hoo"}); + const doc = Logs.findOne({ boo: "hoo" }); test.isTrue(doc._timestamp); return test.equal(doc._timestamp, past); }); @@ -62,26 +67,30 @@ if (Meteor.isServer) { // Client methods // These run after the experiment client tests, so they should be logged in if (Meteor.isClient) { - Tinytest.addAsync("logging - initialize test", (test, next) => Meteor.call("clearLogs", function(err, res) { - test.isFalse(err); - return next(); - })); + Tinytest.addAsync("logging - initialize test", (test, next) => + Meteor.call("clearLogs", function(err, res) { + test.isFalse(err); + return next(); + }) + ); testAsyncMulti("logging - groupId and timestamp", [ - (test, expect) => TurkServer.log({foo: "bar"}, expect((err, res) => test.isFalse(err)) - ) - , - (test, expect) => Meteor.call("getLogs", {foo: "bar"}, expect(function(err, res) { - test.isFalse(err); - test.length(res, 1); + (test, expect) => TurkServer.log({ foo: "bar" }, expect((err, res) => test.isFalse(err))), + (test, expect) => + Meteor.call( + "getLogs", + { foo: "bar" }, + expect(function(err, res) { + test.isFalse(err); + test.length(res, 1); - const logItem = res[0]; + const logItem = res[0]; - test.isTrue(logItem.foo); - test.isTrue(logItem._userId); - test.isTrue(logItem._groupId); - return test.isTrue(logItem._timestamp); - }) - ) + test.isTrue(logItem.foo); + test.isTrue(logItem._userId); + test.isTrue(logItem._groupId); + return test.isTrue(logItem._timestamp); + }) + ) ]); } diff --git a/tests/timer_tests.js b/tests/timer_tests.js index dcdee4f..8353cc1 100644 --- a/tests/timer_tests.js +++ b/tests/timer_tests.js @@ -19,125 +19,149 @@ const withCleanup = TestUtils.getCleanupWrapper({ } }); -Tinytest.addAsync("timers - expiration callback", withCleanup((test, next) => Partitioner.bindGroup(testGroup, function() { - const now = new Date; - - TurkServer.Timers.onRoundEnd(function(type) { - test.equal(type, TurkServer.Timers.ROUND_END_TIMEOUT); - // Cancel group binding - return Partitioner._currentGroup.withValue(null, next); - }); - - return TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); -})) +Tinytest.addAsync( + "timers - expiration callback", + withCleanup((test, next) => + Partitioner.bindGroup(testGroup, function() { + const now = new Date(); + + TurkServer.Timers.onRoundEnd(function(type) { + test.equal(type, TurkServer.Timers.ROUND_END_TIMEOUT); + // Cancel group binding + return Partitioner._currentGroup.withValue(null, next); + }); + + return TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); + }) + ) ); -Tinytest.addAsync("timers - start multiple rounds", withCleanup((test, next) => Partitioner.bindGroup(testGroup, function() { - const now = new Date; +Tinytest.addAsync( + "timers - start multiple rounds", + withCleanup((test, next) => + Partitioner.bindGroup(testGroup, function() { + const now = new Date(); - TurkServer.Timers.onRoundEnd(function(type) { - test.equal(type, TurkServer.Timers.ROUND_END_NEWROUND); + TurkServer.Timers.onRoundEnd(function(type) { + test.equal(type, TurkServer.Timers.ROUND_END_NEWROUND); - return Partitioner._currentGroup.withValue(null, next); - }); + return Partitioner._currentGroup.withValue(null, next); + }); - TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); - return TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); -})) + TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); + return TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); + }) + ) ); -Tinytest.addAsync("timers - end and start new rounds", withCleanup((test, next) => Partitioner.bindGroup(testGroup, function() { - TurkServer.Timers.onRoundEnd(type => test.equal(type, TurkServer.Timers.ROUND_END_MANUAL)); +Tinytest.addAsync( + "timers - end and start new rounds", + withCleanup((test, next) => + Partitioner.bindGroup(testGroup, function() { + TurkServer.Timers.onRoundEnd(type => test.equal(type, TurkServer.Timers.ROUND_END_MANUAL)); - const nRounds = 10; + const nRounds = 10; - for (let i = 1, end = nRounds, asc = 1 <= end; asc ? i <= end : i >= end; asc ? i++ : i--) { - const now = new Date; - TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); - TurkServer.Timers.endCurrentRound(); - } + for (let i = 1, end = nRounds, asc = 1 <= end; asc ? i <= end : i >= end; asc ? i++ : i--) { + const now = new Date(); + TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); + TurkServer.Timers.endCurrentRound(); + } - // Make sure there are the right number of rounds - test.length(RoundTimers.find().fetch(), nRounds); + // Make sure there are the right number of rounds + test.length(RoundTimers.find().fetch(), nRounds); - return Partitioner._currentGroup.withValue(null, next); -})) + return Partitioner._currentGroup.withValue(null, next); + }) + ) ); -Tinytest.addAsync("timers - early expiration", withCleanup((test, next) => Partitioner.bindGroup(testGroup, function() { - const now = new Date; - - let count = 0; - const types = {}; - - TurkServer.Timers.onRoundEnd(function(type) { - count++; - if (types[type] == null) { types[type] = 0; } - return types[type]++; - }); - - TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); - - TurkServer.Timers.endCurrentRound(); - - // round end callback should only have been called once - return Meteor.setTimeout(function() { - test.equal(count, 1); - test.equal(types[TurkServer.Timers.ROUND_END_MANUAL], 1); - // Cancel group binding - return Partitioner._currentGroup.withValue(null, next); - } - , 150); -})) +Tinytest.addAsync( + "timers - early expiration", + withCleanup((test, next) => + Partitioner.bindGroup(testGroup, function() { + const now = new Date(); + + let count = 0; + const types = {}; + + TurkServer.Timers.onRoundEnd(function(type) { + count++; + if (types[type] == null) { + types[type] = 0; + } + return types[type]++; + }); + + TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); + + TurkServer.Timers.endCurrentRound(); + + // round end callback should only have been called once + return Meteor.setTimeout(function() { + test.equal(count, 1); + test.equal(types[TurkServer.Timers.ROUND_END_MANUAL], 1); + // Cancel group binding + return Partitioner._currentGroup.withValue(null, next); + }, 150); + }) + ) ); -Tinytest.addAsync("timers - robustness to multiple calls", withCleanup((test, next) => Partitioner.bindGroup(testGroup, function() { - - const now = new Date; - const end = new Date(now.getTime() + 300); - - let errors = 0; - - const testFunc = function() { - try { - return TurkServer.Timers.startNewRound(now, end); - } catch (e) { - // We should get at least one error here. - console.log(e); - return errors++; - } - }; - - // Make sure that running a bunch of these simultaneously doesn't bug out - for (let _ = 1; _ <= 10; _++) { Meteor.defer(testFunc); } - - return Meteor.setTimeout(function() { - // TODO: do something smarter with RoundTimers.find().fetch() - test.isTrue(errors > 0); - test.isTrue(errors < 10); - - return next(); - } - , 500); -})) +Tinytest.addAsync( + "timers - robustness to multiple calls", + withCleanup((test, next) => + Partitioner.bindGroup(testGroup, function() { + const now = new Date(); + const end = new Date(now.getTime() + 300); + + let errors = 0; + + const testFunc = function() { + try { + return TurkServer.Timers.startNewRound(now, end); + } catch (e) { + // We should get at least one error here. + console.log(e); + return errors++; + } + }; + + // Make sure that running a bunch of these simultaneously doesn't bug out + for (let _ = 1; _ <= 10; _++) { + Meteor.defer(testFunc); + } + + return Meteor.setTimeout(function() { + // TODO: do something smarter with RoundTimers.find().fetch() + test.isTrue(errors > 0); + test.isTrue(errors < 10); + + return next(); + }, 500); + }) + ) ); -Tinytest.addAsync("timers - reschedule on server restart", withCleanup((test, next) => Partitioner.bindGroup(testGroup, function() { - const now = new Date; +Tinytest.addAsync( + "timers - reschedule on server restart", + withCleanup((test, next) => + Partitioner.bindGroup(testGroup, function() { + const now = new Date(); - TurkServer.Timers.onRoundEnd(function() { - test.ok(); - // Cancel group binding - return Partitioner._currentGroup.withValue(null, next); - }); + TurkServer.Timers.onRoundEnd(function() { + test.ok(); + // Cancel group binding + return Partitioner._currentGroup.withValue(null, next); + }); - TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); + TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100)); - // Prevent the normal timeout from being called - Meteor.clearTimeout(TestUtils.lastScheduledRound); + // Prevent the normal timeout from being called + Meteor.clearTimeout(TestUtils.lastScheduledRound); - // Pretend the server restarted - return TestUtils.scheduleOutstandingRounds(); -})) + // Pretend the server restarted + return TestUtils.scheduleOutstandingRounds(); + }) + ) ); - diff --git a/tests/utils.js b/tests/utils.js index 8609e71..b6c099b 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -9,8 +9,11 @@ if (Meteor.isClient) { // Prevent router from complaining about missing path Router.map(function() { - return this.route("/", - {onBeforeAction() { return this.render(null); }}); + return this.route("/", { + onBeforeAction() { + return this.render(null); + } + }); }); } @@ -30,51 +33,50 @@ if (Meteor.isServer) { TurkServer.mturk = function(op, params) { TestUtils.mturkAPI.op = op; TestUtils.mturkAPI.params = params; - return (typeof TestUtils.mturkAPI.handler === 'function' ? TestUtils.mturkAPI.handler(op, params) : undefined); + return typeof TestUtils.mturkAPI.handler === "function" + ? TestUtils.mturkAPI.handler(op, params) + : undefined; }; } // Get a wrapper that runs a before and after function wrapping some test function. TestUtils.getCleanupWrapper = function(settings) { - const { - before - } = settings; - const { - after - } = settings; + const { before } = settings; + const { after } = settings; // Take a function... - return fn => // Return a function that, when called, executes the hooks around the function. - (function() { - const next = arguments[1]; - if (typeof before === 'function') { - before(); - } - - if (next == null) { - // Synchronous version - Tinytest.add - try { - return fn.apply(this, arguments); - } catch (error) { - throw error; + return ( + fn // Return a function that, when called, executes the hooks around the function. + ) => + function() { + const next = arguments[1]; + if (typeof before === "function") { + before(); } - finally { - if (typeof after === 'function') { - after(); + + if (next == null) { + // Synchronous version - Tinytest.add + try { + return fn.apply(this, arguments); + } catch (error) { + throw error; + } finally { + if (typeof after === "function") { + after(); + } } + } else { + // Asynchronous version - Tinytest.addAsync + const hookedNext = function() { + if (typeof after === "function") { + after(); + } + return next(); + }; + return fn.call(this, arguments[0], hookedNext); } - } else { - // Asynchronous version - Tinytest.addAsync - const hookedNext = function() { - if (typeof after === 'function') { - after(); - } - return next(); - }; - return fn.call(this, arguments[0], hookedNext); - } - }); + }; }; -TestUtils.sleep = Meteor.wrapAsync((time, cb) => Meteor.setTimeout((() => cb(undefined)), time)); +TestUtils.sleep = Meteor.wrapAsync((time, cb) => Meteor.setTimeout(() => cb(undefined), time)); TestUtils.blockingCall = Meteor.wrapAsync(Meteor.call); From 4523bfc13fbe7e18b5d24e002ac2216718d789ef Mon Sep 17 00:00:00 2001 From: Andrew Mao Date: Mon, 4 Nov 2019 18:54:31 -0500 Subject: [PATCH 7/7] fix tests and add notes --- Contributing.md | 16 ++++++++++------ History.md | 2 ++ lib/common.js | 3 ++- package.js | 1 + 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Contributing.md b/Contributing.md index dee9217..9aabbaf 100644 --- a/Contributing.md +++ b/Contributing.md @@ -1,12 +1,16 @@ ## Code -Initial parts of this codebase were written in Coffeescript. However, any -updates and refactoring should be done in ES6, which implements many useful -functions from Coffeescript but allows more people to read the code and -contribute. -Generally, follow AirBnb's [Javascript style guide](https://github.com/airbnb/javascript). +This repo is written in ES6 with a `.prettierrc` to auto-format code. -More information to come. +Initial parts of this codebase were written in Coffeescript. However, ES6 +implements many useful functions from Coffeescript while allowing more people to +read the code and contribute. We [decaffeinated] the repo and in the future may +convert fully to TypeScript. + +[decaffeinated]: https://github.com/TurkServer/turkserver-meteor/pull/99 + +TODO: set up format or lint hooks with something like AirBnb's [Javascript style +guide](https://github.com/airbnb/javascript). ## Testing diff --git a/History.md b/History.md index ebdf341..575beef 100644 --- a/History.md +++ b/History.md @@ -1,5 +1,7 @@ ## vNEXT +- Convert all Coffeescript to ES6 and support up to Meteor 1.4. + ## v0.5.0 - First release on Atmosphere (Meteor's package system). diff --git a/lib/common.js b/lib/common.js index f279851..27527ad 100644 --- a/lib/common.js +++ b/lib/common.js @@ -33,7 +33,8 @@ this.Qualifications = new Mongo.Collection("ts.qualifications"); this.HITTypes = new Mongo.Collection("ts.hittypes"); this.HITs = new Mongo.Collection("ts.hits"); -const ErrMsg = { +// Need a global here for export to test code, after updating to Meteor 1.4. +ErrMsg = { // authentication unexpectedBatch: "This HIT is not recognized.", batchInactive: "This task is currently not accepting new assignments.", diff --git a/package.js b/package.js index 952b94f..7c0779a 100644 --- a/package.js +++ b/package.js @@ -136,6 +136,7 @@ Package.onTest(function(api) { "accounts-password", "check", "deps", + "ecmascript", "mongo", "random", "ui",