diff --git a/expdj/apps/experiments/forms.py b/expdj/apps/experiments/forms.py index f72180a..fea9378 100644 --- a/expdj/apps/experiments/forms.py +++ b/expdj/apps/experiments/forms.py @@ -57,8 +57,8 @@ def __init__(self, *args, **kwargs): class BatteryForm(ModelForm): class Meta: - exclude = ('owner','contributors','experiments','bonus_active' - 'blacklist_active','blacklist_threshold') + exclude = ('owner', 'contributors', 'experiments',' bonus_active' + 'blacklist_active', 'blacklist_threshold') model = Battery def clean(self): diff --git a/expdj/apps/experiments/models.py b/expdj/apps/experiments/models.py index cae337e..83a951f 100644 --- a/expdj/apps/experiments/models.py +++ b/expdj/apps/experiments/models.py @@ -1,14 +1,21 @@ +import collections +import operator + from guardian.shortcuts import assign_perm, get_users_with_perms, remove_perm -from django.core.validators import MaxValueValidator, MinValueValidator -from django.db.models.signals import m2m_changed +from jsonfield import JSONField from polymorphic.models import PolymorphicModel -from django.core.urlresolvers import reverse + from django.contrib.auth.models import User -from django.db.models import Q, DO_NOTHING -from jsonfield import JSONField +from django.core.exceptions import ObjectDoesNotExist +from django.core.urlresolvers import reverse +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models -import collections -import operator +from django.db.models import Q, DO_NOTHING +from django.db.models.signals import m2m_changed + +# trying to import Result object directly from models was giving an import +# error here, even though the import matched views.py exactly. +from expdj.apps import turk class CognitiveAtlasConcept(models.Model): name = models.CharField(max_length=1000, null=False, blank=False) @@ -133,6 +140,12 @@ def __str__(self): class Battery(models.Model): '''A battery is a collection of experiment templates''' + + ORDER_CHOICES = ( + ("random", "random"), + ("specified", "specified"), + ) + # Name must be unique because anonymous link is generated from hash name = models.CharField(max_length=200, unique = True, null=False, verbose_name="Name of battery") description = models.TextField(blank=True, null=True) @@ -150,19 +163,32 @@ class Battery(models.Model): active = models.BooleanField(choices=((False, 'Inactive'), (True, 'Active')), default=True,verbose_name="Active") - ORDER_CHOICES = ( - ("random", "random"), - ("specified", "specified"), - ) - presentation_order = models.CharField("order function for presentation of experiments",max_length=200,choices=ORDER_CHOICES,default="random",help_text="Select experiments randomly, or in a custom specified order.") - blacklist_active = models.BooleanField(choices=((False, 'Off'), - (True, 'On')), - default=False,verbose_name="Blacklist based on rejection criteria") + blacklist_active = models.BooleanField( + choices=((False, 'Off'), (True, 'On')), + default=False, + verbose_name="Blacklist based on rejection criteria" + ) blacklist_threshold = models.PositiveIntegerField(null=True,blank=True,default=10,help_text="Number of experiments to fail reject condition to add participant to blacklist",validators = [MinValueValidator(0.0)]) - bonus_active = models.BooleanField(choices=((False, 'Off'), - (True, 'On')), - default=False,verbose_name="Bonus based on reward criteria") + bonus_active = models.BooleanField( + choices=((False, 'Off'), (True, 'On')), + default=False, + verbose_name="Bonus based on reward criteria" + ) + required_batteries = models.ManyToManyField( + "Battery", + blank=True, + related_name='required_batteries_mtm', + help_text=("Batteries which must be completed for this battery to be " + "attempted") + ) + restricted_batteries = models.ManyToManyField( + "Battery", + blank=True, + related_name='restricted_batteries_mtm', + help_text=("Batteries that must not be completed in order for " + "this battery to be attempted") + ) def get_absolute_url(self): return_cid = self.id diff --git a/expdj/apps/experiments/templates/experiments/add_experiment.html b/expdj/apps/experiments/templates/experiments/add_experiment.html index 06882f9..20dab52 100644 --- a/expdj/apps/experiments/templates/experiments/add_experiment.html +++ b/expdj/apps/experiments/templates/experiments/add_experiment.html @@ -125,7 +125,7 @@

{{ change_type }} Experiment Amazon Mechanical Turk HITS

+ - {% for hit in hits %} + + + {{ hit.title }} {% else %} - + {{ hit.title }} {% endif %} + (Details) + + - {% endfor %} + {% endfor %}
titlecreation time keywords description reward +
{% if hit.sandbox = True %} -
{{ hit.title }}
{{ hit.title }}{{ hit.creation_time|date:"m/d/y G:H" }} {{ hit.keywords }} {{ hit.description }} {{ hit.reward }} {% if edit_permission %} Manage Hit + Clone Hit Expire Hit {% if hit.status = "D" %} @@ -185,7 +192,7 @@

Amazon Mechanical Turk HITS

{% endif %}
{% else %} @@ -261,7 +268,8 @@ ] }); $('#hits_table').dataTable({ - responsive: true + responsive: true, + "order": [[ 1, "desc" ]] }); $('#delete_experiment').click(function(e) { return confirm("This will remove the experiment from the battery, and not delete it from the application. Are you sure you want to do this?"); diff --git a/expdj/apps/experiments/templates/experiments/edit_battery.html b/expdj/apps/experiments/templates/experiments/edit_battery.html index 9acb059..b17f028 100644 --- a/expdj/apps/experiments/templates/experiments/edit_battery.html +++ b/expdj/apps/experiments/templates/experiments/edit_battery.html @@ -1,6 +1,8 @@ {% extends "main/base.html" %} +{% load static %} {% load crispy_forms_tags %} {% block head %} + {% endblock %} {% block content %} @@ -22,3 +24,10 @@

{{ header_text }}

{% endblock %} +{% block scripts %} + + +{% endblock %} diff --git a/expdj/apps/experiments/urls.py b/expdj/apps/experiments/urls.py index 44d58c5..18c8312 100644 --- a/expdj/apps/experiments/urls.py +++ b/expdj/apps/experiments/urls.py @@ -1,16 +1,20 @@ -from expdj.apps.experiments.views import experiments_view, edit_experiment_template, \ -delete_experiment_template, add_experiment_template, save_experiment_template, \ -view_experiment, preview_experiment, batteries_view, add_battery, \ -edit_battery, view_battery, delete_battery, remove_experiment, \ -add_experiment, edit_experiment, save_experiment, update_experiment_template, \ -remove_condition, preview_battery, serve_battery, serve_battery_anon, \ -generate_battery_user, sync, experiment_results_dashboard, \ -battery_results_dashboard, dummy_battery ,modify_experiment, intro_battery, \ -save_survey_template, add_survey_template, add_game_template, save_game_template, \ -enable_cookie_view, change_experiment_order, serve_battery_gmail, subject_management -from expdj import settings -from django.views.generic.base import TemplateView from django.conf.urls import patterns, url +from django.views.generic.base import TemplateView + +from expdj import settings +from expdj.apps.experiments.views import ( + experiments_view, edit_experiment_template, delete_experiment_template, + add_experiment_template, save_experiment_template, view_experiment, + preview_experiment, batteries_view, add_battery, edit_battery, + view_battery, delete_battery, remove_experiment, add_experiment, + edit_experiment, save_experiment, update_experiment_template, + remove_condition, preview_battery, serve_battery, serve_battery_anon, + generate_battery_user, sync, experiment_results_dashboard, + battery_results_dashboard, dummy_battery ,modify_experiment, intro_battery, + save_survey_template, add_survey_template, add_game_template, + save_game_template, enable_cookie_view, change_experiment_order, + serve_battery_gmail, subject_management +) urlpatterns = patterns('', @@ -66,11 +70,12 @@ url(r'^batteries/(?P\d+|[A-Z]{8})/serve/gmail$',serve_battery_gmail,name='serve_battery_gmail'), url(r'^local/(?P\d+|[A-Z]{8})/$',sync,name='local'), url(r'^local/$',sync,name='local'), # local sync of data - url(r'^cookie/$',enable_cookie_view,name='enable_cookie_view')) + url(r'^cookie/$',enable_cookie_view,name='enable_cookie_view') +) if settings.DEBUG: urlpatterns += patterns('', url(r'^static/(?P.*)$', 'django.views.static.serve', { 'document_root': settings.MEDIA_ROOT, }), - ) +) diff --git a/expdj/apps/experiments/views.py b/expdj/apps/experiments/views.py index ce3f2a0..9e61044 100644 --- a/expdj/apps/experiments/views.py +++ b/expdj/apps/experiments/views.py @@ -1,43 +1,57 @@ -from django.shortcuts import get_object_or_404, render_to_response, render, redirect -from expdj.apps.experiments.models import ExperimentTemplate, Experiment, Battery, \ - ExperimentVariable, CreditCondition -from expdj.apps.experiments.forms import ExperimentForm, ExperimentTemplateForm, BatteryForm, \ - BlacklistForm -from expdj.apps.turk.utils import get_worker_experiments -from expdj.apps.turk.tasks import assign_experiment_credit, update_assignments, check_blacklist, \ - experiment_reward -from expdj.apps.experiments.utils import get_experiment_selection, install_experiments, \ - update_credits, make_results_df, get_battery_results, get_experiment_type, remove_keys, \ - complete_survey_result, select_experiments -from expdj.settings import BASE_DIR,STATIC_ROOT,MEDIA_ROOT,DOMAIN_NAME -from django.http.response import HttpResponseRedirect, HttpResponseForbidden, Http404 -from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect -from django.core.exceptions import PermissionDenied, ValidationError +import datetime +import csv +import hashlib +import json +import numpy +import os +import pandas +import re +import shutil +import uuid + from expfactory.battery import get_load_static, get_experiment_run from expfactory.survey import generate_survey +from expfactory.experiment import load_experiment +from expfactory.views import embed_experiment + from django.contrib.auth.decorators import login_required -from expdj.apps.turk.models import HIT, Result, Assignment -from expdj.apps.turk.models import get_worker, Blacklist, Bonus +from django.core.exceptions import PermissionDenied, ValidationError +from django.forms.models import model_to_dict from django.http import HttpResponse, JsonResponse -from expfactory.experiment import load_experiment +from django.http.response import ( + HttpResponseRedirect, HttpResponseForbidden, Http404 +) +from django.shortcuts import ( + get_object_or_404, render_to_response, render, redirect +) +from django.utils import timezone +from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect + from expdj.apps.main.views import google_auth_view -from django.forms.models import model_to_dict -from expfactory.views import embed_experiment -from expdj.apps.users.models import User -from django.shortcuts import render +from expdj.apps.experiments.forms import ( + ExperimentForm, ExperimentTemplateForm, BatteryForm, BlacklistForm +) +from expdj.apps.experiments.models import ( + ExperimentTemplate, Experiment, Battery, ExperimentVariable, + CreditCondition +) +from expdj.apps.experiments.utils import ( + get_experiment_selection, install_experiments, update_credits, + make_results_df, get_battery_results, get_experiment_type, remove_keys, + complete_survey_result, select_experiments +) +from expdj.settings import BASE_DIR,STATIC_ROOT,MEDIA_ROOT,DOMAIN_NAME import expdj.settings as settings -from django.utils import timezone -import datetime -import uuid -import shutil -import hashlib -import numpy -import pandas -import uuid -import json -import csv -import re -import os +from expdj.apps.turk.models import ( + HIT, Result, Assignment, get_worker, Blacklist, Bonus +) +from expdj.apps.turk.tasks import ( + assign_experiment_credit, update_assignments, check_blacklist, + experiment_reward, check_battery_dependencies +) +from expdj.apps.turk.utils import get_worker_experiments +from expdj.apps.users.models import User + media_dir = os.path.join(BASE_DIR,MEDIA_ROOT) @@ -392,6 +406,15 @@ def serve_battery(request,bid,userid=None): if isinstance(worker,list): # no id means returning [] return render_to_response("turk/invalid_id_sorry.html") + missing_batteries, blocking_batteries = check_battery_dependencies(battery, userid) + if missing_batteries or blocking_batteries: + return render_to_response( + "turk/battery_requirements_not_met.html", + context={'missing_batteries': missing_batteries, + 'blocking_batteries': blocking_batteries} + ) + + # Try to get some info about browser, language, etc. browser = "%s,%s" %(request.user_agent.browser.family,request.user_agent.browser.version_string) platform = "%s,%s" %(request.user_agent.os.family,request.user_agent.os.version_string) @@ -399,7 +422,8 @@ def serve_battery(request,bid,userid=None): # Does the worker have experiments remaining? uncompleted_experiments = get_worker_experiments(worker,battery) - if len(uncompleted_experiments) == 0: + experiments_left = len(uncompleted_experiments) + if experiments_left == 0: # Thank you for your participation - no more experiments! return render_to_response("turk/worker_sorry.html") @@ -419,22 +443,27 @@ def serve_battery(request,bid,userid=None): "uniqueId":result.id} # If this is the last experiment, the finish button will link to a thank you page. - if len(uncompleted_experiments) == 1: + if experiments_left == 1: next_page = "/finished" # Determine template name based on template_type template = "%s/serve_battery.html" %(experiment_type) - return deploy_battery(deployment="docker-local", - battery=battery, - experiment_type=experiment_type, - context=context, - task_list=task_list, - template=template, - next_page=next_page, - result=result) - -def deploy_battery(deployment,battery,experiment_type,context,task_list,template,result,next_page=None,last_experiment=False): + return deploy_battery( + deployment="docker-local", + battery=battery, + experiment_type=experiment_type, + context=context, + task_list=task_list, + template=template, + next_page=next_page, + result=result, + experiments_left=experiments_left-1 + ) + +def deploy_battery(deployment, battery, experiment_type, context, task_list, + template, result, next_page=None, last_experiment=False, + experiments_left=None): '''deploy_battery is a general function for returning the final view to deploy a battery, either local or MTurk :param deployment: either "docker-mturk" or "docker-local" :param battery: models.Battery object @@ -445,6 +474,7 @@ def deploy_battery(deployment,battery,experiment_type,context,task_list,template :param template: html template to render :param result: the result object, turk.models.Result :param last_experiment: boolean if true will redirect the user to a page to submit the result (for surveys) + :param experiments_left: integer indicating how many experiments are left in battery. ''' if next_page == None: next_page = "javascript:window.location.reload();" @@ -471,6 +501,16 @@ def deploy_battery(deployment,battery,experiment_type,context,task_list,template if result != None: runcode = runcode.replace("{{result.id}}",str(result.id)) runcode = runcode.replace("{{next_page}}",next_page) + if experiments_left is not None: + total_experiments = battery.experiments.count() + expleft_msg = "

Experiments left in battery {0:d} out of {1:d}

" + expleft_msg = expleft_msg.format(experiments_left, total_experiments) + runcode = runcode.replace("

", expleft_msg) + if experiments_left == 0: + runcode = runcode.replace("

Experiment Complete

", "

All Experiments Complete

") + runcode = runcode.replace("You have completed the experiment", "You have completed all experiments") + runcode = runcode.replace("Click \"Next Experiment\" to keep your result, and progress to the next task", "Click \"Finised\" to keep your result.") + runcode = runcode.replace(">Next Experiment", ">Finished") elif experiment_type in ["games"]: experiment = load_experiment(experiment_folders[0]) runcode = experiment[0]["deployment_variables"]["run"] diff --git a/expdj/apps/turk/forms.py b/expdj/apps/turk/forms.py index eb35dad..c40dfb6 100644 --- a/expdj/apps/turk/forms.py +++ b/expdj/apps/turk/forms.py @@ -31,3 +31,13 @@ def __init__(self, *args, **kwargs): self.helper.layout = Layout() tab_holder = TabHolder() self.helper.add_input(Submit("submit", "Save")) + +class WorkerContactForm(forms.Form): + subject = forms.CharField(label="Subject") + message = forms.CharField(widget=forms.Textarea, label="Message") + + def __init__(self, *args, **kwargs): + super(WorkerContactForm, self).__init__(*args, **kwargs) + self.helper = FormHelper(self) + self.helper.layout = Layout() + self.helper.add_input(Submit("submit", "Send")) diff --git a/expdj/apps/turk/models.py b/expdj/apps/turk/models.py index 5319836..940da73 100644 --- a/expdj/apps/turk/models.py +++ b/expdj/apps/turk/models.py @@ -407,7 +407,6 @@ class Assignment(models.Model): (True, 'Completed')), default=False,verbose_name="participant completed the entire assignment") - def create(self): init_connection_callback(sender=self.hit) diff --git a/expdj/apps/turk/tasks.py b/expdj/apps/turk/tasks.py index 7a1d5e4..a44b7a2 100644 --- a/expdj/apps/turk/tasks.py +++ b/expdj/apps/turk/tasks.py @@ -1,14 +1,22 @@ from __future__ import absolute_import -from expdj.apps.turk.models import Result, Assignment, get_worker, HIT, Blacklist, Bonus -from expdj.apps.experiments.utils import get_experiment_type -from expdj.apps.experiments.models import ExperimentTemplate, Battery + +import numpy +import os + from boto.mturk.price import Price from celery import shared_task, Celery -from django.utils import timezone + from django.conf import settings +from django.utils import timezone + +from expdj.apps.experiments.models import ExperimentTemplate, Battery +from expdj.apps.experiments.utils import get_experiment_type +from expdj.apps.turk.models import Result, Assignment, get_worker, HIT, Blacklist, Bonus from expdj.settings import TURK -import numpy -import os + +# trying to import Result object directly from models was giving an import +# error here, even though the import matched views.py exactly. +from expdj.apps import turk os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'expdj.settings') app = Celery('expdj') @@ -119,7 +127,7 @@ def add_blacklist(blacklist,experiment,description): blacklist.flags[experiment.template.exp_id] = new_flag # If the blacklist count is greater than acceptable count, user is blacklisted - if len(blacklist.flags) > blacklist.battery.blacklist_threshold: + if len(blacklist.flags) >= blacklist.battery.blacklist_threshold: blacklist.active = True blacklist.blacklist_time = timezone.now() blacklist.save() @@ -283,3 +291,59 @@ def get_unique_variables(results): new_variables = [x for x in trial["trialdata"].keys() if x not in variables] variables = variables + new_variables return numpy.unique(variables).tolist() + + +def check_battery_dependencies(current_battery, worker_id): + ''' + check_battery_dependencies looks up all of a workers completed + experiments in a result object and places them in a dictionary + + organized by battery_id. Each of these buckets of results is + iterated through to check that every experiment in that battery has + been completed. In this way a list of batteries that a worker has + completed is built. This list is then compared to the lists of + required and restricted batteries to determine if the worker is + eligible to attempt the current battery. + ''' + worker_results = turk.models.Result.objects.filter( + worker_id = worker_id, + completed=True + ) + + worker_result_batteries = {} + for result in worker_results: + if worker_result_batteries.get(result.battery.id): + worker_result_batteries[result.battery.id].append(result) + else: + worker_result_batteries[result.battery.id] = [] + worker_result_batteries[result.battery.id].append(result) + + worker_completed_batteries = [] + for battery_id in worker_result_batteries: + result = worker_result_batteries[battery_id] + all_experiments_complete = True + result_experiment_list = [x.experiment_id for x in result] + try: + battery_experiments = Battery.objects.get(id=battery_id).experiments.all() + except ObjectDoesNotExist: + # battery may have been removed. + continue + for experiment in battery_experiments: + if experiment.template_id not in result_experiment_list: + all_experiments_complete = False + break + if all_experiments_complete: + worker_completed_batteries.append(battery_id) + continue + + missing_batteries = [] + for required_battery in current_battery.required_batteries.all(): + if required_battery.id not in worker_completed_batteries: + missing_batteries.append(required_battery) + + blocking_batteries = [] + for restricted_battery in current_battery.restricted_batteries.all(): + if restricted_battery.id in worker_completed_batteries: + blocking_batteries.append(restricted_battery) + + return missing_batteries, blocking_batteries diff --git a/expdj/apps/turk/templates/turk/battery_requirements_not_met.html b/expdj/apps/turk/templates/turk/battery_requirements_not_met.html new file mode 100644 index 0000000..4f5fde2 --- /dev/null +++ b/expdj/apps/turk/templates/turk/battery_requirements_not_met.html @@ -0,0 +1,49 @@ +{% load staticfiles %} + + + The Experiment Factory: Thank you + + + + + + + +
+

Some Requirements Have Not Been Met.

+ {% if missing_batteries %} + The following batteries need to be completed before the current one may be attempted:
+
    + {% for battery in missing_batteries %} +
  • + {{ battery.name }} +
  • + {% endfor %} +
+ {% endif %} + {% if blocking_batteries %} + The following batteries conflict with the current battery. Their completion is preventing the current battery from being attempted:
+
    + {% for battery in blocking_batteries %} +
  • + {{ battery.name }} +
  • + {% endfor %} +
+ {% endif %} +
+ + diff --git a/expdj/apps/turk/templates/turk/contact_worker_modal.html b/expdj/apps/turk/templates/turk/contact_worker_modal.html new file mode 100644 index 0000000..7fd4eae --- /dev/null +++ b/expdj/apps/turk/templates/turk/contact_worker_modal.html @@ -0,0 +1,19 @@ +{% load crispy_forms_tags %} + diff --git a/expdj/apps/turk/templates/turk/hit_detail.html b/expdj/apps/turk/templates/turk/hit_detail.html new file mode 100644 index 0000000..2bf4856 --- /dev/null +++ b/expdj/apps/turk/templates/turk/hit_detail.html @@ -0,0 +1,30 @@ +{% extends "main/base.html" %} +{% block title %} + Details for {{ hit.title }} +{% endblock %} +{% block content %} +Battery: {{ hit.battery }}
+Owner: {{ hit.owner }}
+Mturk ID: {{ hit.mturk_id }}
+HIT Type ID: {{ hit.hit_type_id }}
+Creation Time: {{ hit.creation_time }}
+Description: {{ hit.description }}
+Keywords: {{ hit.keywords }}
+Reward: {{ hit.reward }}
+Lifetime in Hours: {{ hit.lifetime_in_hours }}
+Assignment Duration in Hours: {{ hit.assignment_duration_in_hours }}
+Max Assignments: {{ hit.max_assignments }}
+Auto Approval Delay in Seconds: {{ hit.auto_approval_delay_in_seconds }}
+Requester Annotation: {{ hit.requester_annotation }}
+Number of Similar Hits: {{ hit.number_of_similar_hits }}
+Review Status: {{ hit.review_status }}
+Number of Pending Assignments: {{ hit.number_of_assignments_pending }}
+Number of Available Assignments: {{ hit.number_of_assignments_available }}
+Number of Completed Assignments: {{ hit.number_of_assignments_completed }}
+Qualification for Number of Approved HITs: {{ hit.qualification_number_hits_approved }}
+Qualification for Percent of Assignments Approved: {{ hit.qualification_percent_assignments_approved }}
+Qualification Adult: {{ hit.qualification_adult }}
+Qualification Locale: {{ hit.qualification_locale }}
+Qualification Custom: {{ hit.qualification_custom }}
+Sandbox Only: {{ hit.sandbox }}
+{% endblock %} diff --git a/expdj/apps/turk/templates/turk/manage_hit.html b/expdj/apps/turk/templates/turk/manage_hit.html index d6e7fde..9d1adba 100644 --- a/expdj/apps/turk/templates/turk/manage_hit.html +++ b/expdj/apps/turk/templates/turk/manage_hit.html @@ -45,6 +45,7 @@

Attention

Worker ID Status Accept Time + Contact @@ -54,6 +55,7 @@

Attention

{{ assignment.worker }} {{ assignment.status }} {{ assignment.accept_time | localize }} + Contact Worker {% endfor %} @@ -73,6 +75,7 @@

In Progress

Worker ID Status Accept Time + Contact @@ -82,6 +85,7 @@

In Progress

{{ assignment.worker }} {{ assignment.status }} {{ assignment.accept_time | localize }} + Contact Worker {% endfor %} @@ -100,7 +104,8 @@

In Rejected

Assignment ID Worker ID Status - Accept Time + Accept Time + Contact @@ -110,6 +115,7 @@

In Rejected

{{ assignment.worker }} {{ assignment.status }} {{ assignment.accept_time | localize }} + Contact Worker {% endfor %} @@ -128,7 +134,8 @@

Submit

Assignment ID Worker ID Status - Accept Time + Accept Time + Contact @@ -138,6 +145,7 @@

Submit

{{ assignment.worker }} {{ assignment.status }} {{ assignment.accept_time | localize }} + Contact Worker {% endfor %} @@ -158,6 +166,7 @@

Approved

Worker ID Status Accept Time + Contact @@ -167,6 +176,7 @@

Approved

{{ assignment.worker }} {{ assignment.status }} {{ assignment.accept_time | localize }} + Contact Worker {% endfor %} @@ -175,6 +185,8 @@

Approved

{% endif %} + {% endblock %} @@ -190,6 +202,11 @@

Approved

$('.collapse').collapse('hide'); }) +$('.contact_worker').on("click", function(e) { + e.preventDefault(); + $('#contact_modal').modal("show").load(this.href); +}); + }) {% endblock %} diff --git a/expdj/apps/turk/urls.py b/expdj/apps/turk/urls.py index 1bdf124..1ceab64 100644 --- a/expdj/apps/turk/urls.py +++ b/expdj/apps/turk/urls.py @@ -1,16 +1,47 @@ -from expdj.apps.turk.views import edit_hit, delete_hit, expire_hit, preview_hit, \ -serve_hit, multiple_new_hit, end_assignment, finished_view, not_consent_view, \ -survey_submit, manage_hit +from expdj.apps.turk.views import (edit_hit, delete_hit, expire_hit, + preview_hit, serve_hit, multiple_new_hit, end_assignment, finished_view, + not_consent_view, survey_submit, manage_hit, contact_worker) from expdj.apps.experiments.views import sync from django.views.generic.base import TemplateView from django.conf.urls import patterns, url +from django.views.generic.base import TemplateView + +from expdj.apps.experiments.views import sync +from expdj.apps.turk.views import ( + edit_hit, delete_hit, expire_hit, preview_hit, serve_hit, multiple_new_hit, + end_assignment, finished_view, not_consent_view, survey_submit, manage_hit, + clone_hit, hit_detail +) + urlpatterns = patterns('', # HITS url(r'^hits/(?P\d+|[A-Z]{8})/new$',edit_hit,name='new_hit'), - url(r'^hits/(?P\d+|[A-Z]{8})/(?P\d+|[A-Z]{8})/manage$',manage_hit,name='manage_hit'), - url(r'^hits/(?P\d+|[A-Z]{8})/multiple$',multiple_new_hit,name='multiple_new_hit'), - url(r'^hits/(?P\d+|[A-Z]{8})/(?P\d+|[A-Z]{8})/edit$',edit_hit,name='edit_hit'), + url( + r'^hits/(?P\d+|[A-Z]{8})/(?P\d+|[A-Z]{8})/manage$', + manage_hit, + name='manage_hit' + ), + url( + r'^hits/(?P\d+|[A-Z]{8})/multiple$', + multiple_new_hit, + name='multiple_new_hit' + ), + url( + r'^hits/(?P\d+|[A-Z]{8})/(?P\d+|[A-Z]{8})/edit$', + edit_hit, + name='edit_hit' + ), + url( + r'^hits/(?P\d+|[A-Z]{8})/(?P\d+|[A-Z]{8})/clone$', + clone_hit, + name='clone_hit' + ), + url( + r'hits/(?P\d+|[A-Z]{8})/detail$', + hit_detail, + name='hit_detail' + ), url(r'^hits/(?P\d+|[A-Z]{8})/delete$',delete_hit,name='delete_hit'), url(r'^hits/(?P\d+|[A-Z]{8})/expire$',expire_hit,name='expire_hit'), @@ -18,9 +49,18 @@ url(r'^accept/(?P\d+|[A-Z]{8})',serve_hit,name='serve_hit'), url(r'^turk/(?P\d+|[A-Z]{8})',preview_hit,name='preview_hit'), url(r'^turk/preview',not_consent_view,name='not_consent_view'), - url(r'^turk/end/(?P\d+|[A-Z]{8})',end_assignment,name='end_assignment'), - url(r'^surveys/(?P\d+|[A-Z]{8})/(?P[A-Za-z0-9]{30})/submit$',survey_submit,name='survey_submit'), + url( + r'^turk/end/(?P\d+|[A-Z]{8})', + end_assignment, + name='end_assignment' + ), + url( + r'^surveys/(?P\d+|[A-Z]{8})/(?P[A-Za-z0-9]{30})/submit$', + survey_submit, + name='survey_submit' + ), url(r'^sync/(?P\d+|[A-Z]{8})/$',sync,name='sync_data'), url(r'^sync/$',sync,name='sync_data'), - url(r'^finished$', finished_view, name="finished_view") + url(r'^finished$', finished_view, name="finished_view"), + url(r'^worker/contact/(?P\d+)',contact_worker,name='contact_worker') ) diff --git a/expdj/apps/turk/utils.py b/expdj/apps/turk/utils.py index ad525ee..b3abf03 100644 --- a/expdj/apps/turk/utils.py +++ b/expdj/apps/turk/utils.py @@ -1,16 +1,19 @@ -from expdj.apps.experiments.models import Experiment -from boto.mturk.connection import MTurkConnection -from expdj.settings import BASE_DIR, MTURK_ALLOW -from boto.mturk.question import ExternalQuestion -from boto.mturk.price import Price import ConfigParser import datetime -import pandas import json import os +from boto.mturk.connection import MTurkConnection +from boto.mturk.price import Price +from boto.mturk.question import ExternalQuestion +import pandas + from django.conf import settings +from expdj.apps.experiments.models import Experiment +from expdj.settings import BASE_DIR, MTURK_ALLOW + + # RESULTS UTILS def to_dict(input_ordered_dict): @@ -75,7 +78,7 @@ def is_sandbox(): def get_worker_url(): """Get proper URL depending upon sandbox settings""" - if is_sandbox(): + if settings.MTURK_ALLOW == False: return SANDBOX_WORKER_URL else: return PRODUCTION_WORKER_URL @@ -122,7 +125,8 @@ def get_worker_experiments(worker,battery,completed=False): experiment_selection = [e for e in battery_tags if e not in worker_tags] else: experiment_selection = [e for e in worker_tags if e in battery_tags] - return Experiment.objects.filter(template__exp_id__in=experiment_selection) + return Experiment.objects.filter(template__exp_id__in=experiment_selection, + battery_experiments__id=battery.id) def get_time_difference(d1,d2,format='%Y-%m-%d %H:%M:%S'): @@ -132,3 +136,6 @@ def get_time_difference(d1,d2,format='%Y-%m-%d %H:%M:%S'): if isinstance(d2,str): d2 = datetime.datetime.strptime(d2, format) return (d2 - d1).total_seconds() / 60 + + + diff --git a/expdj/apps/turk/views.py b/expdj/apps/turk/views.py index 6d12149..d546d06 100644 --- a/expdj/apps/turk/views.py +++ b/expdj/apps/turk/views.py @@ -1,25 +1,32 @@ -from expdj.apps.experiments.views import check_battery_edit_permission, check_mturk_access, \ -get_battery_intro, deploy_battery -from expdj.apps.turk.utils import get_connection, get_worker_url, get_host, get_worker_experiments -from django.http.response import HttpResponseRedirect, HttpResponseForbidden, HttpResponse, Http404 -from django.shortcuts import get_object_or_404, render_to_response, render, redirect -from expdj.apps.turk.tasks import assign_experiment_credit, get_unique_experiments -from expdj.apps.experiments.utils import get_experiment_type, select_experiments -from expdj.apps.turk.models import Worker, HIT, Assignment, Result, get_worker -from expdj.apps.experiments.models import Battery, ExperimentTemplate +from datetime import timedelta, datetime +import json +import os +import requests + from expfactory.battery import get_load_static, get_experiment_run -from expdj.settings import BASE_DIR,STATIC_ROOT,MEDIA_ROOT +from numpy.random import choice +from optparse import make_option + from django.contrib.auth.decorators import login_required from django.core.management.base import BaseCommand -from django.views.decorators.csrf import ensure_csrf_cookie -from expdj.apps.turk.forms import HITForm -from datetime import timedelta, datetime +from django.http.response import (HttpResponseRedirect, HttpResponseForbidden, + HttpResponse, Http404, HttpResponseNotAllowed) +from django.core.urlresolvers import reverse +from django.shortcuts import get_object_or_404, render_to_response, render, redirect from django.utils import timezone -from optparse import make_option -from numpy.random import choice -import requests -import json -import os +from django.views.decorators.csrf import ensure_csrf_cookie + +from expdj.apps.experiments.models import (Battery, ExperimentTemplate) +from expdj.apps.experiments.views import (check_battery_edit_permission, + check_mturk_access, get_battery_intro, deploy_battery) +from expdj.apps.experiments.utils import get_experiment_type, select_experiments +from expdj.apps.turk.forms import HITForm, WorkerContactForm +from expdj.apps.turk.models import Worker, HIT, Assignment, Result, get_worker +from expdj.apps.turk.tasks import (assign_experiment_credit, + get_unique_experiments, check_battery_dependencies) +from expdj.apps.turk.utils import (get_connection, get_credentials, get_host, + get_worker_url, get_worker_experiments) +from expdj.settings import BASE_DIR,STATIC_ROOT,MEDIA_ROOT media_dir = os.path.join(BASE_DIR,MEDIA_ROOT) @@ -127,6 +134,10 @@ def serve_hit(request,hid): # Get Experiment Factory objects for each worker = get_worker(aws["worker_id"]) + check_battery_response = check_battery_view(battery, aws["worker_id"]) + if (check_battery_response): + return check_battery_response + # This is the submit URL, either external or sandbox host = get_host(hit) @@ -152,13 +163,14 @@ def serve_hit(request,hid): # Does the worker have experiments remaining for the hit? uncompleted_experiments = get_worker_experiments(worker,hit.battery) - if len(uncompleted_experiments) == 0: + experiments_left = len(uncompleted_experiments) + if experiments_left == 0: # Thank you for your participation - no more experiments! return render_to_response("turk/worker_sorry.html") # if it's the last experiment, we will submit the result to amazon (only for surveys) last_experiment = False - if len(uncompleted_experiments) == 1: + if experiments_left == 1: last_experiment = True task_list = select_experiments(battery,uncompleted_experiments) @@ -180,18 +192,21 @@ def serve_hit(request,hid): aws["uniqueId"] = result.id # If this is the last experiment, the finish button will link to a thank you page. - if len(uncompleted_experiments) == 1: + if experiments_left == 1: next_page = "/finished" - return deploy_battery(deployment="docker-mturk", - battery=battery, - experiment_type=experiment_type, - context=aws, - task_list=task_list, - template=template, - next_page=None, - result=result, - last_experiment=last_experiment) + return deploy_battery( + deployment="docker-mturk", + battery=battery, + experiment_type=experiment_type, + context=aws, + task_list=task_list, + template=template, + next_page=None, + result=result, + last_experiment=last_experiment, + experiments_left=experiments_left-1 + ) else: return render_to_response("turk/error_sorry.html") @@ -208,6 +223,7 @@ def preview_hit(request,hid): hit = get_hit(hid,request) battery = hit.battery context = get_amazon_variables(request) + context["instruction_forms"] = get_battery_intro(battery) context["hit_uid"] = hid context["start_url"] = "/accept/%s/?assignmentId=%s&workerId=%s&turkSubmitTo=%s&hitId=%s" %(hid, @@ -289,6 +305,26 @@ def multiple_new_hit(request, bid): else: return HttpResponseForbidden() +@login_required +def clone_hit(request, bid, hid): + mturk_permission = check_mturk_access(request) + if mturk_permission != True: + return HttpResponseForbidden() + + new_hit = get_object_or_404(HIT, pk=hid) + new_hit.pk = None + form = HITForm(instance=new_hit) + form.helper.form_action = reverse('new_hit',args=[bid]) + + battery = Battery.objects.get(pk=bid) + header_text = "%s HIT" %(battery.name) + + context = {"form": form, + "is_owner": True, + "header_text":header_text} + + return render(request, "turk/new_hit.html", context) + @login_required def edit_hit(request, bid, hid=None): @@ -329,6 +365,42 @@ def edit_hit(request, bid, hid=None): else: return HttpResponseForbidden() +@login_required +def contact_worker(request, aid): + mturk_permission = check_mturk_access(request) + + if mturk_permission == False: + return HttpResponseForbidden() + + assignment = Assignment.objects.get(id=aid) + worker = assignment.worker + if request.method == "GET": + form = WorkerContactForm() + context = { + "form": form, + "worker": worker, + "assignment": assignment + } + return render(request, "turk/contact_worker_modal.html", context) + elif request.method == "POST": + form = WorkerContactForm(request.POST) + if form.is_valid(): + AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY_ID = get_credentials( + battery=assignment.hit.battery + ) + conn = get_connection( + AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY_ID, + hit=assignment.hit + ) + subject = form.cleaned_data['subject'] + message = form.cleaned_data['message'] + conn.notify_workers([worker.id], subject, message) + return redirect('manage_hit', bid=assignment.hit.battery.id, + hid=assignment.hit.id) + else: + return HttpResponseNotAllowed() + # Expire a hit @login_required def expire_hit(request, hid): @@ -366,6 +438,11 @@ def delete_hit(request, hid): else: return HttpResponseForbidden() +@login_required +def hit_detail(request, hid): + hit = get_object_or_404(HIT, pk=hid) + return render(request, "turk/hit_detail.html", {'hit': hit}) + def get_flagged_questions(number=None): """get_flagged_questions return questions that are flagged for curation @@ -378,3 +455,14 @@ def get_flagged_questions(number=None): if number == None: return questions return choice(questions,int(number)) + +def check_battery_view(battery, worker_id): + missing_batteries, blocking_batteries = check_battery_dependencies(battery, worker_id) + if missing_batteries or blocking_batteries: + return render_to_response( + "turk/battery_requirements_not_met.html", + context={'missing_batteries': missing_batteries, + 'blocking_batteries': blocking_batteries} + ) + else: + return None