Skip to content

Commit

Permalink
Added bonus column to dashboard.
Browse files Browse the repository at this point in the history
  • Loading branch information
evankirkiles committed Jan 4, 2022
1 parent 3b279be commit 1fb87c8
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 26 deletions.
34 changes: 34 additions & 0 deletions psiturk/amt_services.py
Expand Up @@ -221,6 +221,40 @@ def get_assignment(self, assignment_id):
'status': assignment['AssignmentStatus'],
}
return worker_data

@amt_service_response
def get_bonuses(self, hit_id=None, assignment_ids=None):
"""Get paid bonuses."""
bonuses = []
if hit_id:
paginator = self.mtc.get_paginator('list_bonus_payments')
args = dict(
HITId=hit_id,
PaginationConfig={'PageSize': 100}
)
for page in paginator.paginate(**args):
bonuses.extend(page['BonusPayments'])
elif assignment_ids:
if not isinstance(assignment_ids, list):
assignment_ids = [assignment_ids]
for assignment_id in assignment_ids:
paginator = self.mtc.get_paginator('list_bonus_payments')
args = dict(
AssignmentId=assignment_id,
PaginationConfig={'PageSize': 100}
)
for page in paginator.paginate(**args):
bonuses.extend(page['BonusPayments'])
bonus_data = [{
'workerId': bonus['WorkerId'],
'bonusAmount': bonus['BonusAmount'],
'assignmentId': bonus['AssignmentId'],
'reason': bonus['Reason'],
'grantTime': bonus['GrantTime']
} for bonus in bonuses]
print(bonus_data)
return bonus_data


@amt_service_response
def bonus_assignment(self, assignment_id, worker_id, amount, reason=""):
Expand Down
8 changes: 8 additions & 0 deletions psiturk/amt_services_wrapper.py
Expand Up @@ -230,6 +230,14 @@ def count_maybe_will_complete(self, hits=None):
elif status == 'Unassignable':
maybe_will_complete_count += hit.options['number_assignments_pending']
return maybe_will_complete_count

@amt_services_wrapper_response
def get_bonuses(self, hit_id=None, assignment_ids=None):
"""
Returns all bonuses if hit_id is set, or specific assignment bonuses.
"""
bonus_data = self.amt_services.get_bonuses(hit_id, assignment_ids).data
return {'bonuses': bonus_data}

@amt_services_wrapper_response
def get_assignments(self, hit_ids=None, assignment_status=None, all_studies=False):
Expand Down
16 changes: 16 additions & 0 deletions psiturk/api/__init__.py
Expand Up @@ -471,6 +471,19 @@ def post(self, action):
return _return


class BonusList(Resource):

# POST: Returns a list of bonuses for a HIT from MTurk
# hit_id: a specific hit to get all bonuses for (priority)
# assignment_ids: list of assignment ids to get bonuses for
def post(self):
hit_id = request.json['hit_id'] if 'hit_id' in request.json else None
assignment_ids = request.json['assignment_ids'] if 'assignment_ids' in request.json else None
_return = services_manager.amt_services_wrapper.get_bonuses(
hit_id=hit_id, assignment_ids=assignment_ids)
return _return.data


# --------------------------- DATA WRITING HELPERS --------------------------- #

# Writes data to an open zipfile
Expand Down Expand Up @@ -520,6 +533,9 @@ def get_datafile(participant, datatype):
api.add_resource(HitsList, '/hits', '/hits/')
api.add_resource(HitsAction, '/hits/action/<action>')

# Bonuses
api.add_resource(BonusList, '/bonuses', '/bonuses/')

# Assignments
api.add_resource(AssignmentsList, '/assignments', '/assignments/')
api.add_resource(AssignmentsAction, '/assignments/action/<action>')
Expand Down
55 changes: 50 additions & 5 deletions psiturk/dashboard/static/js/assignments.js
Expand Up @@ -6,6 +6,7 @@ var ASSIGNMENT_FIELDS = {
'workerId': {'title': 'Worker ID', 'type': 'string', 'style': {'width': '200px'}},
'assignmentId': {'title': 'Assignment ID', 'type': 'string', 'style': {'max-width': '150px'}},
'status': {'title': 'Status', 'type': 'string', 'style': {'width': '100px'}},
'bonused': {'title': 'Bonused', 'type': 'dollar', 'style': {'width': '100px'}},
'accept_time': {'title': 'Accepted On', 'type': 'date', 'style': {'width': '300px'}},
'submit_time': {'title': 'Submitted On', 'type': 'date', 'style': {'width': '300px'}},
};
Expand All @@ -32,6 +33,7 @@ if (HIT_LOCAL) {
'assignmentId': {'title': 'Assignment ID', 'type': 'string', 'style': {'max-width': '150px'}},
'status': {'title': 'Status', 'type': 'string', 'style': {'width': '100px'}},
'bonus': {'title': 'Bonus', 'type': 'dollar', 'style': {'width': '100px'}},
'bonused': {'title': 'Bonused', 'type': 'dollar', 'style': {'width': '100px'}},
'codeversion': {'title': 'Code#', 'type': 'string', 'style': {'width': '100px'}},
'accept_time': {'title': 'Accepted On', 'type': 'date', 'style': {'width': '300px'}},
'submit_time': {'title': 'Submitted On', 'type': 'date', 'style': {'width': '300px'}},
Expand Down Expand Up @@ -100,9 +102,7 @@ class AssignmentsDBDisplay {
'maintainSelected': false,
'index': 'assignmentId',
'callback': () => {
if (ASSIGNMENT_ID) {
$('#' + this.db.trPrefix + ASSIGNMENT_ID).click();
}
this._loadBonusesPaid(hitId);
}
});
}
Expand All @@ -114,6 +114,48 @@ class AssignmentsDBDisplay {
});
}

// Gets bonus information about the HIT
_loadBonusesPaid(hitId) {
$.ajax({
type: 'POST',
url: '/api/bonuses',
data: JSON.stringify({
hit_id: hitId
}),
contentType: 'application/json; charset=utf-8',
dataType: 'json',
success: (bonusData) => {
let bonuses = {}
bonusData.bonuses.forEach((bonus) => {
if (bonuses[bonus['assignmentId']] == undefined) {
bonuses[bonus['assignmentId']] = bonus['bonusAmount'];
} else {
bonuses[bonus['assignmentId']] += bonus['bonusAmount'];
}
});
let updatedData = this.db.data;
updatedData.forEach((el, i) => {
updatedData[i]['bonused'] = bonuses[el['assignmentId']];
});
this.db.updateData(updatedData, ASSIGNMENT_FIELDS, {
'rerender': true,
'resetFilter': false,
'maintainSelected': true,
'index': 'assignmentId',
'callback': () => {
if (ASSIGNMENT_ID) {
$('#' + this.db.trPrefix + ASSIGNMENT_ID).click();
}
}
});
},
error: function(errorMsg) {
alert(errorMsg);
console.log(errorMsg);
}
})
}

// Reloads specific assignment data
_reloadAssignments(assignment_ids) {
$.ajax({
Expand All @@ -136,7 +178,9 @@ class AssignmentsDBDisplay {
'resetFilter': false,
'maintainSelected': true,
'index': 'assignmentId',
'callback': () => {}
'callback': () => {
this._loadBonusesPaid(hitId);
}
});
},
error: function(errorMsg) {
Expand Down Expand Up @@ -184,8 +228,9 @@ class AssignmentsDBDisplay {
$('#downloadOneDataHref').removeAttr('href');
}
} else {
$('#assignmentInfo_bonus').text('No bonus data.');
$('#assignmentInfo_bonus').text('???');
}
$('#assignmentInfo_bonused').text(data['bonused'] ? '$' + data['bonused'] : undefined);

// Update the current HREF
history.pushState({id: 'hitpage'}, '', window.location.origin + '/dashboard/hits/' + HIT_ID + '/assignments/' + data['assignmentId']);
Expand Down
6 changes: 3 additions & 3 deletions psiturk/dashboard/static/js/dbfilter.js
Expand Up @@ -57,15 +57,15 @@ var FILTER_TYPES = {
'dollar': {
'greaterthan': {
'title': '>',
'comparator': (a, b) => a > parseInt(b)
'comparator': (a, b) => a > parseFloat(b)
},
'equals': {
'title': '=',
'comparator': (a, b) => a == parseInt(b)
'comparator': (a, b) => a == parseFloat(b)
},
'lessthan': {
'title': '<',
'comparator': (a, b) => a < parseInt(b)
'comparator': (a, b) => a < parseFloat(b)
}
}
}
Expand Down
38 changes: 21 additions & 17 deletions psiturk/dashboard/static/js/dbview.js
Expand Up @@ -112,8 +112,8 @@ export class DatabaseView {
th$.css(property, cssValue);
}
let sortLink$ = $('<a>' + value['title'] + '</a>')
sortLink$.on('click', () => {
this.sortByColumn(key);
sortLink$.on('click', () => {
this.sortByColumn(key);
});
headerTr$.append(th$.append(sortLink$));
}
Expand Down Expand Up @@ -167,7 +167,7 @@ export class DatabaseView {
this.selectedRowData = this.data[i];
this.selectedRowID = event.currentTarget.id;
this.selectedRowIndex = currentRow;
this.callbacks['onSelect'](this.selectedRowData);
if (this.callbacks['onSelect'] != undefined) this.callbacks['onSelect'](this.selectedRowData);
$(event.currentTarget).addClass('selected').siblings().removeClass('selected');
})
this.DOM$.tbody.append(row$);
Expand Down Expand Up @@ -225,6 +225,7 @@ export class DatabaseView {
sortByColumn(colTitle) {
var switching = true;
let byColIndex = Object.keys(this.fields).indexOf(colTitle) + 1;
let colType = this.fields[colTitle]['type'];
// Determine switch direction (swap from last)
// If the last sort was on this column, reverse the sort, otherwise just do forwards
let switchDirection = this.sort['last'] == byColIndex ? !this.sort['forwards'] : true;
Expand All @@ -236,24 +237,27 @@ export class DatabaseView {
for (let i = 0; i < (rows.length - 1) && !(shouldSwitch); i++) {
let first = $(rows[i]).find('td:eq(' + byColIndex + ')').html();
let second = $(rows[i+1]).find('td:eq(' + byColIndex + ')').html();
if (colType == 'date') {
first = new Date(first);
second = new Date(second);
} else if (colType == 'num') {
first = parseFloat(first);
second = parseFloat(second);
} else if (colType == 'dollar') {
first = parseFloat(first.replace(/\$/g, ''));
second = parseFloat(second.replace(/\$/g, ''));
first = isNaN(first) ? 0 : first;
second = isNaN(second) ? 0 : second;
} else {
first = first.toLowerCase();
second = second.toLowerCase();
}

// Determine comparator from column type, default is string compare
if (switchDirection) {
if (Object.entries(this.fields)[byColIndex][1]['type'] == 'date') {
shouldSwitch = new Date(first) < new Date(second);
} else if (Object.entries(this.fields)[byColIndex][1]['type'] == 'num') {
shouldSwitch = parseFloat(first) < parseFloat(second);
} else {
shouldSwitch = first.toLowerCase() < second.toLowerCase();
}
shouldSwitch = first < second;
} else {
if (Object.entries(this.fields)[byColIndex][1]['type'] == 'date') {
shouldSwitch = new Date(first) > new Date(second);
} else if (Object.entries(this.fields)[byColIndex][1]['type'] == 'num') {
shouldSwitch = parseFloat(first) > parseFloat(second);
} else {
shouldSwitch = first.toLowerCase() > second.toLowerCase();
}
shouldSwitch = first > second;
}

// If should switch matches switch direction, then go
Expand Down
4 changes: 3 additions & 1 deletion psiturk/dashboard/templates/dashboard/assignments.html
Expand Up @@ -99,7 +99,9 @@
<b class="noselect">Submitted: </b><span id="assignmentInfo_submitted">...</span>
</li>
<li class="list-group-item px-2 py-1" style="background-color: #eee">
<b class="noselect">HIT Reward: </b><span id="assignmentInfo_hitReward">${{ hit_info.options.reward }}</span>, <b class="noselect">Bonus: </b><span id="assignmentInfo_bonus">...</span>
<b class="noselect">HIT Reward: </b><span id="assignmentInfo_hitReward">${{ hit_info.options.reward }}</span>,
<b class="noselect">Bonus: </b><span id="assignmentInfo_bonus">...</span>,
<b class="noselect">Bonused: </b><span id="assignmentInfo_bonused">...</span>
</li>
</ul>
<div class="row no-gutters">
Expand Down

0 comments on commit 1fb87c8

Please sign in to comment.