Skip to content

Commit

Permalink
Added bonusing from a CSV.
Browse files Browse the repository at this point in the history
  • Loading branch information
evankirkiles committed Jan 4, 2022
1 parent 1fb87c8 commit 3e26283
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 85 deletions.
2 changes: 0 additions & 2 deletions psiturk/amt_services.py
Expand Up @@ -79,7 +79,6 @@ def wrapper(*args, **kwargs):
response = func(*args, **kwargs)
return AmtServicesSuccessResponse(operation=func.__name__, data=response)
except Exception as e:
# print(e)
return AmtServicesErrorResponse(operation=func.__name__, exception=e)

return wrapper
Expand Down Expand Up @@ -252,7 +251,6 @@ def get_bonuses(self, hit_id=None, assignment_ids=None):
'reason': bonus['Reason'],
'grantTime': bonus['GrantTime']
} for bonus in bonuses]
print(bonus_data)
return bonus_data


Expand Down
224 changes: 144 additions & 80 deletions psiturk/dashboard/static/js/assignments.js
Expand Up @@ -22,6 +22,7 @@ var APPROVE_FIELDS = {
var BONUS_FIELDS = {
'workerId': {'title': 'Worker ID', 'type': 'string', 'style': {'width': '200px'}},
'bonus': {'title': 'Bonus', 'type': 'dollar', 'style': {'width': '100px'}},
'bonused': {'title': 'Bonused', 'type': 'dollar', 'style': {'width': '100px'}},
'status': {'title': 'Status', 'type': 'string', 'style': {'width': '100px'}},
'assignmentId': {'title': 'Assignment ID', 'type': 'string', 'style': {'width': '320px'}},
}
Expand Down Expand Up @@ -102,7 +103,7 @@ class AssignmentsDBDisplay {
'maintainSelected': false,
'index': 'assignmentId',
'callback': () => {
this._loadBonusesPaid(hitId);
this._loadBonusesPaid(HIT_ID);
}
});
}
Expand Down Expand Up @@ -179,7 +180,7 @@ class AssignmentsDBDisplay {
'maintainSelected': true,
'index': 'assignmentId',
'callback': () => {
this._loadBonusesPaid(hitId);
this._loadBonusesPaid(HIT_ID);
}
});
},
Expand Down Expand Up @@ -293,63 +294,64 @@ class AssignmentWorkerDataDBDisplay {
}

// Approves a list of assignment ids and then reloads them
function assignmentAPI(assignment_ids, endpoint, payload={}, callbacks={'success': () => {}, 'failure': () => {}}) {
$.ajax({
type: 'POST',
url: '/api/assignments/action/' + endpoint,
data: JSON.stringify({
'assignments': assignment_ids,
'all_studies': !HIT_LOCAL,
...payload
}),
contentType: 'application/json; charset=utf-8',
dataType: 'json',
success: function(data) {
if (data.every(el => !el.success)) {
callbacks['failure']();
} else {
mainDisp._reloadAssignments(assignment_ids);
callbacks['success']();
function assignmentAPI(assignment_ids, endpoint, payload={}, reload=true) {
return new Promise((resolve, reject) => {
$.ajax({
type: 'POST',
url: '/api/assignments/action/' + endpoint,
data: JSON.stringify({
'assignments': assignment_ids,
'all_studies': !HIT_LOCAL,
...payload
}),
contentType: 'application/json; charset=utf-8',
dataType: 'json',
success: function(data) {
if (data.every(el => !el.success)) {
reject();
} else {
if (reload) { mainDisp._reloadAssignments(assignment_ids); }
resolve();
}
},
error: function(errorMsg) {
console.log(errorMsg);
reject();
}
},
error: function(errorMsg) {
console.log(errorMsg);
callbacks['failure']();
}
});
});
})

}

// Approves a single individual in the database
function approveIndividualHandler() {
let assignment_id = $('#assignmentInfo_assignmentid').text();
$('#approveOne').prop('disabled', true);
$('#rejectOne').prop('disabled', true);
assignmentAPI([assignment_id], 'approve', {}, {
'success': () => {
alert('Approval successful!');
},
'failure': () => {
$('#approveOne').prop('disabled', false);
$('#rejectOne').prop('disabled', false);
alert('Approval unsuccessful');
}
assignmentAPI([assignment_id], 'approve', {})
.then(() =>{
alert('Approval successful!');
})
.catch(() => {
$('#approveOne').prop('disabled', false);
$('#rejectOne').prop('disabled', false);
alert('Approval unsuccessful');
});
}

// Approves all the individuals in the approval display
function approveAllHandler() {
let assignment_ids = approvalDispView.getDisplayedData().map((el) => el['assignmentId']);
$('#approval-submit').prop('disabled', true);
assignmentAPI(assignment_ids, 'approve', {}, {
'success': () => {
$('#approveModal').modal('hide');
$('#approval-submit').prop('disabled', false);
alert('Approval successful!');
},
'failure': () => {
$('#approval-submit').prop('disabled', false);
alert('Approval unsuccessful.');
}
assignmentAPI(assignment_ids, 'approve', {})
.then(() => {
$('#approveModal').modal('hide');
$('#approval-submit').prop('disabled', false);
alert('Approval successful!');
})
.catch(() => {
$('#approval-submit').prop('disabled', false);
alert('Approval unsuccessful.');
});
}

Expand All @@ -358,20 +360,21 @@ function rejectIndividualHandler() {
let assignment_id = $('#assignmentInfo_assignmentid').text();
$('#approveOne').prop('disabled', true);
$('#rejectOne').prop('disabled', true);
assignmentAPI([assignment_id], 'reject', {}, {
'success': () => {
alert('Rejection successful!');
},
'failure': () => {
$('#approveOne').prop('disabled', false);
$('#rejectOne').prop('disabled', false);
alert('Rejection unsuccessful');
}
assignmentAPI([assignment_id], 'reject', {})
.then(() => {
alert('Rejection successful!');
})
.catch(() => {
$('#approveOne').prop('disabled', false);
$('#rejectOne').prop('disabled', false);
alert('Rejection unsuccessful');
});
}

var bonusesUploaded;
function bonusAllHandler() {
let assignment_ids = bonusDispView.getDisplayedData().map((el) => el['assignmentId']);
let displayedData = bonusDispView.getDisplayedData();
let assignment_ids = displayedData.map((el) => el['assignmentId']);
let amount = parseFloat($('#bonus-value').val());
if ($('#bonus-autoToggle').hasClass('active')) { amount = 'auto'; }
let reason = $('#bonus-reason').val();
Expand All @@ -384,21 +387,51 @@ function bonusAllHandler() {
}
}
$('#bonus-submit').prop('disabled', true);
assignmentAPI(assignment_ids, 'bonus', {
'amount': amount,
'reason': reason
},
{
'success': () => {
// in case of custom bonus amounts
if (amount == 'auto' && bonusesUploaded != undefined) {
// bonus each worker, and wait for all to complete
assignment_ids = [];
let bonusPromises = displayedData.reduce((result, el) => {
if (bonusesUploaded[el['workerId']]) {
assignment_ids.push(el['assignmentId']);
result.push(
assignmentAPI(el['assignmentId'], 'bonus', {
amount: bonusesUploaded[el['workerId']],
reason: reason
}, false)
);
}
return result;
}, []);
// once all complete, notify how many succeeded and how many failed
Promise.allSettled(bonusPromises).then((results) => {
let statuses = results.reduce((acc, result) => {
acc[result.status == "fulfilled" ? 0 : 1] += 1;
return acc;
}, [0, 0]);
let status = [];
if (statuses[0] > 0) status.push(`Successfully bonused ${statuses[0]} workers!`);
if (statuses[1] > 0) status.push(`Failed to bonus ${statuses[1]} workers.`);
// update the assignments
mainDisp._reloadAssignments(assignment_ids);
// alert of status and close the modal
alert(status.join(' '));
$('#bonusModal').modal('hide');
$('#bonus-submit').prop('disabled', false);
})
// otherwise let the API handle everything
} else {
assignmentAPI(assignment_ids, 'bonus', {amount, reason})
.then(() => {
$('#bonusModal').modal('hide');
$('#bonus-submit').prop('disabled', false);
alert('Bonus successful!');
},
'failure': () => {
})
.catch(() => {
$('#bonus-submit').prop('disabled', false);
alert('Bonus unsuccessful');
}
})
});
}
}

// Opens the worker approval modal with the workers currently in the table
Expand Down Expand Up @@ -430,12 +463,13 @@ function approveWorkersModal() {
// Handler for bonus information changing
function bonusInfoChanged() {
let assignments = mainDisp.db.getDisplayedData();
$("#bonus-autoToggle").prop('disabled', (!HIT_LOCAL && !bonusesUploaded));

// Find base total
let total = assignments.length * $('#bonus-value').val();
let totalText = `${$('#bonus-value').val()} x ${assignments.length}`;
if ($('#bonus-autoToggle').hasClass('active')) {
total = bonusDispView.getDisplayedData().reduce((prevValue, el) => prevValue + el['bonus'], 0);
total = bonusDispView.getDisplayedData().reduce((prevValue, el) => prevValue + (el['bonus'] ? el['bonus'] : 0), 0);
totalText = total.toFixed(2);
}
$('#bonus-baseTotal').text(totalText);
Expand All @@ -456,11 +490,39 @@ function bonusWorkersModal() {
if (!bonusDispView) {
bonusDispView = new DatabaseView({display: $('#DBBonusTable')});
}
bonusesUploaded = undefined;
bonusDispView.updateData(assignments, BONUS_FIELDS);
$('#numWorkersBonusing').text(assignments.length);
bonusInfoChanged();
}

// Reads a CSV of worker bonuses into bonusesUploaded
function handleBonusFileUpload(event) {
const reader = new FileReader();
reader.onload = (event) => {
bonusesUploaded = {};
const lines = event.target.result.split('\n');
for (let i = 1; i < lines.length; i++) {
let cols = lines[i].split(',');
bonusesUploaded[cols[0]] = parseFloat(cols[1]);
}
let newAssignments = [...bonusDispView.getDisplayedData()];
for (let i = 0; i < newAssignments.length; i++) {
if (bonusesUploaded[newAssignments[i]['workerId']] != undefined) {
newAssignments[i] = {
...newAssignments[i],
'bonus': bonusesUploaded[newAssignments[i]['workerId']]
};
}
}
bonusDispView.updateData(newAssignments, BONUS_FIELDS);
$('#bonus-file').val('');
$("#bonus-autoToggle").click();
bonusInfoChanged();
};
reader.readAsText(event.target.files[0]);
}

// Opens the worker bonus modal with the single worker selected
function bonusOneWorkerModal() {
let assignment_id = $('#assignmentInfo_assignmentid').text();
Expand Down Expand Up @@ -517,21 +579,23 @@ $(window).on('load', function() {
$('input[name="dataRadioOptions"]').on('change', viewWorkerDataHandler);

// Approves/rejects/bonuses the currently selected assignment
if (!HIT_LOCAL) {
$("#bonus-autoToggle").prop('disabled', true);
} else {
$('#bonus-autoToggle').on('click', () => {
if ($('#bonus-autoToggle').hasClass('active')) {
$('#bonus-autoToggle').removeClass('active');
bonusInfoChanged();
$('#bonus-value').prop('disabled', false);
} else {
$('#bonus-autoToggle').addClass('active');
bonusInfoChanged();
$('#bonus-value').prop('disabled', true);
}
})
}
$('#bonus-autoToggle').on('click', () => {
if ($('#bonus-autoToggle').hasClass('active')) {
$('#bonus-autoToggle').removeClass('active');
bonusInfoChanged();
$('#bonus-value').prop('disabled', false);
} else {
$('#bonus-autoToggle').addClass('active');
bonusInfoChanged();
$('#bonus-value').prop('disabled', true);
}
});

// Input listeners
$('#bonus-value').on('change', bonusInfoChanged);
$('#bonus-file').on('change', handleBonusFileUpload);

// Button listeners
$('#approveOne').on('click', approveIndividualHandler);
$('#rejectOne').on('click', rejectIndividualHandler);
$('#bonusOne').on('click', bonusOneWorkerModal);
Expand Down
12 changes: 9 additions & 3 deletions psiturk/dashboard/templates/dashboard/assignments.html
Expand Up @@ -212,7 +212,7 @@ <h5 class="modal-title">Approve <b>workers</b>!</h5>

<!-- BONUSING MODAL -->
<div class="modal fade" id="bonusModal" tabindex="-1" role="dialog" aria-labelledby="assignmentsCreateModal" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Bonus <b>workers</b>!</h5>
Expand Down Expand Up @@ -250,12 +250,18 @@ <h5 class="modal-title">Bonus <b>workers</b>!</h5>
</div>
</div>
<div class="mb-1"><b>Total: $</b><span id="bonus-baseTotal">X.XX</span><b>, MTurk Fee: </b><span id="bonus-mturkfee">XX</span>%<b>, Cost: $</b><span id="bonus-total">X.XX</span></div>
<div style="font-size: small; color: gray;">
When <b><i>Auto</i></b> is on, bonus amounts will be loaded for each worker from the Bonus column of the database. (local HITs only)
<div style="font-size: small; color: gray; user-select: none;">
When <b><i>Auto</i></b> is on, bonus amounts will be loaded for each worker from the Bonus column of the database (local HITs only).
Alternatively, you can enable <b><i>Auto</i></b> for non-local HITs by filling the Bonus column from a CSV with the file browser below.
The CSV's first column must be the worker's ID, and the second column must be the bonus dollar amount.
</div>
</div>
</div>
<div class="modal-footer">
<div class="custom-file" style="margin-right: 20px; white-space: nowrap; overflow: hidden;">
<input type="file" class="custom-file-input" id="bonus-file">
<label class="custom-file-label" for="bonus-file">Choose bonus file...</label>
</div>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="bonus-submit">Bonus!</button>
</div>
Expand Down

0 comments on commit 3e26283

Please sign in to comment.