@csrf_exempt
@lti_role_required(ADMINS)
def render_control_panel(request):
    canvas_modules = CanvasModules(request)
    course_id = get_lti_param(request, "custom_canvas_course_id")
    intervention_points = InterventionPoint.objects.filter(course_id=course_id)
    ip_display_mappings = {track_url.id: get_ip_open_where_display_index(track_url)
                           for ip in intervention_points for track_url in ip.track_urls()}
    experiments = Experiment.objects.filter(course_id=course_id)
    
    context = {
        "modules": canvas_modules.get_modules_with_items(),
        "intervention_points": intervention_points,
        "ip_display_mappings": ip_display_mappings,
        "uninstalled_intervention_points": canvas_modules.get_uninstalled_intervention_points(),
        "canvas_url": get_lti_param(request, "launch_presentation_return_url"),
        "experiments": experiments,
        "experiments_with_unassigned_students": experiments_with_unassigned_students(request, course_id),
        "deletable_experiment_ids": canvas_modules.get_deletable_experiment_ids(),
    }
    return render(request, "ab_tool/experiments_dashboard.html", context) def not_authorized(request):
    return HttpResponse(loader.render_to_string("ab_tool/not_authorized.html"), status=401) def tool_config(request):
    index_url = request.build_absolute_uri(reverse("ab_testing_tool_index"))
    resource_selection_url = request.build_absolute_uri(reverse("ab_testing_tool_resource_selection"))
    
    config = ToolConfig(
        title="A/B Testing Tool",
        launch_url=index_url,
        secure_launch_url=index_url,
    )
    # Tell Canvas that this tool provides a course navigation link:
    nav_params = {
        "enabled": "true",
        "url": index_url,
        "text": "A/B Testing Tool",
        "visibility": "admins",
    }
    config.set_ext_param("canvas.instructure.com", "privacy_level", "public")
    config.set_ext_param("canvas.instructure.com", "course_navigation",
                         nav_params)
    config.set_ext_param("canvas.instructure.com", "resource_selection",
                         {"enabled": "true", "url": resource_selection_url})
    config.set_ext_param("canvas.instructure.com", "selection_height", "800")
    config.set_ext_param("canvas.instructure.com", "selection_width", "800")
    config.set_ext_param("canvas.instructure.com", "tool_id", "ab_testing_tool")
    config.description = ("Tool to allow students in a course to " +
                          "get different content in a module item.")
    
    resp = HttpResponse(config.to_xml(), content_type="text/xml", status=200)
    return resp @lti_role_required(ADMINS)
def download_track_assignments(request, experiment_id):
    #TODO: change this to streaming
    course_id = get_lti_param(request, "custom_canvas_course_id")
    course_title = get_lti_param(request, "context_title")
    experiment = Experiment.get_or_404_check_course(experiment_id, course_id)
    file_title = "course_%s_experiment_%s_student_track_assignments.csv" % (
                                slugify(course_title), slugify(experiment.name))
    return get_student_list_csv(experiment, file_title) @lti_role_required(ADMINS)
def download_intervention_point_interactions(request, experiment_id):
    course_id = get_lti_param(request, "custom_canvas_course_id")
    course_title = get_lti_param(request, "context_title")
    experiment = Experiment.get_or_404_check_course(experiment_id, course_id)
    file_title = "course_%s_experiment_%s_intervention_point_interactions.csv" % (
                                slugify(course_title), slugify(experiment.name))
    return get_intervention_point_interactions_csv(experiment, file_title) @csrf_exempt
@lti_role_required(ADMINS)
def resource_selection(request):
    """ docs: https://canvas.instructure.com/doc/api/file.link_selection_tools.html """
    course_id = get_lti_param(request, "custom_canvas_course_id")
    canvas_modules = CanvasModules(request)
    ext_content_return_types = request.POST.getlist('ext_content_return_types')
    if ext_content_return_types != ['lti_launch_url']:
        raise MISSING_RETURN_TYPES_PARAM
    content_return_url = request.POST.get('ext_content_return_url')
    if not content_return_url:
        raise MISSING_RETURN_URL
    all_experiments = Experiment.objects.filter(course_id=course_id)
    context = {"content_return_url": content_return_url,
               "intervention_points": canvas_modules.get_uninstalled_intervention_points(),
               "tracks": Track.objects.filter(course_id=course_id),
               "experiments": all_experiments,
               }
    return render(request, "ab_tool/add_module_item.html", context) @lti_role_required(ADMINS)
def submit_selection(request):
    intervention_point_id = post_param(request, "intervention_point_id")
    course_id = get_lti_param(request, "custom_canvas_course_id")
    intervention_point = InterventionPoint.get_or_404_check_course(
            intervention_point_id, course_id)
    page_url = intervention_point_url(request, intervention_point_id)
    page_name = intervention_point.name
    content_return_url = post_param(request, "content_return_url")
    params = {"return_type": "lti_launch_url",
               "url": page_url,
               #"title": "Title",
               "text": page_name}
    return redirect("%s?%s" % (content_return_url, urlencode(params))) @lti_role_required(ADMINS)
def submit_create_intervention_point(request, experiment_id):
    """ Note: request will always be POST because Canvas fetches pages within iframe by POST
        TODO: use Django forms library to save instead of getting individual POST params """
    course_id = get_lti_param(request, "custom_canvas_course_id")
    name = validate_name(post_param(request, "name"))
    notes = post_param(request, "notes")
    # validate_format_url validates URLs using backend rules before InterventionPointUrl object creation
    intervention_pointurls = [(k,validate_format_url(v)) for (k,v) in request.POST.iteritems()
                              if INTERVENTION_POINT_URL_TAG in k and v]
    experiment = Experiment.get_or_404_check_course(experiment_id, course_id)
    if InterventionPoint.objects.filter(name=name,
        course_id=course_id, experiment=experiment).count() > 0:
        raise UNIQUE_NAME_ERROR
    intervention_point = InterventionPoint.objects.create(
            name=name, notes=notes, course_id=course_id, experiment=experiment)
    for (k,v) in intervention_pointurls:
        _, track_id = k.split(INTERVENTION_POINT_URL_TAG)
        deploy_option = post_param(request, DEPLOY_OPTION_TAG + track_id)
        is_canvas_page = bool(deploy_option == "canvasPage")
        open_as_tab = bool(deploy_option == "newTab")
        InterventionPointUrl.objects.create(
                url=v, intervention_point_id=intervention_point.id,
                track_id=track_id, is_canvas_page=is_canvas_page, open_as_tab=open_as_tab
        )
    return redirect(reverse("ab_testing_tool_index")) @require_POST
@lti_role_required(ADMINS)
def delete_intervention_point(request, intervention_point_id):
    """ Note: Installed intervention_points are not allowed to be deleted
        Note: attached InterventionPointUrls are deleted via cascading delete """
    course_id = get_lti_param(request, "custom_canvas_course_id")

    try:
        intervention_point = InterventionPoint.get_or_404_check_course(
                intervention_point_id, course_id)
        if intervention_point.experiment.tracks_finalized:
            raise DELETING_INTERVENTION_POINT_AFTER_FINALIZED
        canvas_modules = CanvasModules(request)
        if canvas_modules.intervention_point_is_installed(intervention_point):
            raise DELETING_INSTALLED_INTERVENTION_POINT
        intervention_point.delete()
    except Http404:
        pass

    return redirect(reverse("ab_testing_tool_index")) @csrf_exempt
def deploy_intervention_point(request, intervention_point_id):
    """
    Delivers randomly one of the urls in intervention_point if user is not an admin,
    or edit_intervention_point panel if admin.
    Note: Do not put @lti_role_required(ADMINS) here, as students can reach
    this page.
    TODO: Have admin able to preview intervention_points as a student would see them
    """
    course_id = get_lti_param(request, "custom_canvas_course_id")
    # TODO: replace the following code with verification.is_allowed
    # when that code makes it into django_auth_lti master
    user_roles = get_lti_param(request, "roles")
    
    # If user is an admin, let them edit the intervention_point
    if set(ADMINS) & set(user_roles):
        return redirect(reverse("ab_testing_tool_modules_page_view_intervention_point",
                                args=(intervention_point_id,)))
    
    intervention_point = InterventionPoint.get_or_404_check_course(
            intervention_point_id, course_id)
    
    experiment = intervention_point.experiment
    
    # Otherwise, user is a student.  Tracks for the course must be finalized
    # for a student to be able to access content from the ab_testing_tool
    if not experiment.tracks_finalized:
        raise EXPERIMENT_TRACKS_NOT_FINALIZED
    
    student_id = get_lti_param(request, "custom_canvas_user_login_id")
    
    # Get or create an object to track the student for this course
    try:
        student = ExperimentStudent.objects.get(student_id=student_id, experiment=experiment)
    except ExperimentStudent.DoesNotExist:
        # If this is a new student or the student doesn't yet have a track,
        # select a track before creating the student. This avoids a race condition of
        # a student existing but not having a track assigned (e.g. if the update to
        # a student database object fails)
        student_name = get_lti_param(request, "lis_person_name_full")
        student = assign_track_and_create_student(experiment, student_id, student_name)
    
    # Retrieve the url for the student's track at the current intervention point
    # Return an error page if there is no url configured.
    try:
        chosen_intervention_point_url = InterventionPointUrl.objects.get(intervention_point__pk=intervention_point_id, track=student.track)
    except InterventionPointUrl.DoesNotExist:
        raise NO_URL_FOR_TRACK
    if not chosen_intervention_point_url.url:
        raise NO_URL_FOR_TRACK
    
    log_intervention_point_interaction(course_id, student, intervention_point, experiment,
                                       student.track, chosen_intervention_point_url)
    
    if chosen_intervention_point_url.open_as_tab:
        return render_to_response("ab_tool/new_tab_redirect.html", {"url": chosen_intervention_point_url.url})
    if chosen_intervention_point_url.is_canvas_page:
        return render_to_response("ab_tool/window_redirect.html", {"url": chosen_intervention_point_url.url})
    return redirect(chosen_intervention_point_url.url) @csrf_exempt
@lti_role_required(ADMINS)
def submit_edit_intervention_point(request, intervention_point_id):
    edit_intervention_point_common(request, intervention_point_id)
    return redirect(reverse("ab_testing_tool_index")) @lti_role_required(ADMINS)
def modules_page_edit_intervention_point(request, intervention_point_id):
    context = intervention_point_context(request, intervention_point_id)
    return render(request, "ab_tool/edit_intervention_point_from_canvas.html", context) @lti_role_required(ADMINS)
def modules_page_view_intervention_point(request, intervention_point_id):
    context = intervention_point_context(request, intervention_point_id)
    return render_to_response("ab_tool/view_intervention_point_from_canvas.html", context) @lti_role_required(ADMINS)
def submit_edit_intervention_point_from_modules(request, intervention_point_id):
    edit_intervention_point_common(request, intervention_point_id)
    return redirect(reverse("ab_testing_tool_modules_page_view_intervention_point",
                            args=(intervention_point_id,))) @lti_role_required(ADMINS)
def create_experiment(request):
    context = {"create": True, "started": False}
    return render(request, "ab_tool/edit_experiment.html", context) @lti_role_required(ADMINS)
def submit_create_experiment(request):
    """ Expects a post parameter 'experiment' to be json of the form:
            {"name": name(str),
             "notes": notes(str),
             "uniformRandom": True/False,
             "csvUpload": True/False,
             "tracks": [{"id": None # This is none for all because all tracks are new
                         "weighting": track_weighting(int),
                         "name": track_name(str),
                        }]
            }
    """
    course_id = get_lti_param(request, "custom_canvas_course_id")
    experiment_dict = json.loads(request.body)

    # Unpack data from experiment_dict and update experiment
    name = validate_name(experiment_dict["name"])
    notes = experiment_dict["notes"]
    uniform_random = bool(experiment_dict["uniformRandom"])
    tracks = experiment_dict["tracks"]
    csv_upload = bool(experiment_dict["csvUpload"])

    # Validates using backend rules before any object creation
    for track_dict in tracks:
        validate_name(track_dict["name"])
        # added check for csv upload. if we are uploading a csv file
        # we don't want track weights
        if not uniform_random and not csv_upload:
            validate_weighting(track_dict["weighting"])

    if csv_upload:
        assignment_method = Experiment.CSV_UPLOAD
    elif uniform_random:
        assignment_method = Experiment.UNIFORM_RANDOM
    else:
        assignment_method = Experiment.WEIGHTED_PROBABILITY_RANDOM
    experiment = Experiment.objects.create(
            name=name, course_id=course_id, notes=notes,
            assignment_method=assignment_method
    )
    
    # Update existing tracks
    for track_dict in tracks:
        track = experiment.new_track(validate_name(track_dict["name"]))
        if not uniform_random and not csv_upload:
            track.set_weighting(track_dict["weighting"])
    return HttpResponse("success") @lti_role_required(ADMINS)
def edit_experiment(request, experiment_id):
    course_id = get_lti_param(request, "custom_canvas_course_id")
    experiment = Experiment.get_or_404_check_course(experiment_id, course_id)
    has_installed_intervention = CanvasModules(request).experiment_has_installed_intervention(experiment)
    context = {"experiment": experiment,
               "experiment_has_installed_intervention": has_installed_intervention,
               "create": False,
               "started": experiment.tracks_finalized}
    return render(request, "ab_tool/edit_experiment.html", context) @lti_role_required(ADMINS)
def submit_edit_experiment(request, experiment_id):
    """ Expects a post parameter 'experiment' to be json of the form:
            {"name": name(str),
             "notes": notes(str),
             "uniformRandom": True/False,
             "csvUpload": True/False,
             "tracks": [{"id": track_id(int), # this is None if track is new
                         "weighting": track_weighting(int),
                         "name": track_name(str),
                        }]
            }
    """
    course_id = get_lti_param(request, "custom_canvas_course_id")
    experiment = Experiment.get_or_404_check_course(experiment_id, course_id)
    experiment_dict = json.loads(request.body)
    # Unpack data from experiment_dict and update experiment
    name = validate_name(experiment_dict["name"])
    notes = experiment_dict["notes"]
    if experiment.tracks_finalized:
        # Only allow updating name, notes, and track names for started experiments
        experiment.update(name=name, notes=notes)
        existing_tracks = [i for i in experiment_dict["tracks"] if i["id"] is not None]
        for track_dict in existing_tracks:
            track = Track.get_or_404_check_course(track_dict["id"], course_id)
            track.update(name=validate_name(track_dict["name"]))
        return HttpResponse("success")
    
    uniform_random = experiment_dict["uniformRandom"]
    csv_upload = experiment_dict["csvUpload"]
    existing_tracks = [i for i in experiment_dict["tracks"] if i["id"] is not None]
    new_tracks = [i for i in experiment_dict["tracks"] if i["id"] is None]
    if csv_upload:
        assignment_method = Experiment.CSV_UPLOAD
    elif uniform_random:
        assignment_method = Experiment.UNIFORM_RANDOM
    else:
        assignment_method = Experiment.WEIGHTED_PROBABILITY_RANDOM
    experiment.update(name=name, notes=notes, assignment_method=assignment_method)
    
    # Update existing tracks
    for track_dict in existing_tracks:
        track = Track.get_or_404_check_course(track_dict["id"], course_id)
        track.update(name=validate_name(track_dict["name"]))
        if not uniform_random and not csv_upload:
            track.set_weighting(validate_weighting(track_dict["weighting"]))
    
    # Create new tracks
    for track_dict in new_tracks:
        track = experiment.new_track(validate_name(track_dict["name"]))
        if not uniform_random and not csv_upload:
            track.set_weighting(validate_weighting(track_dict["weighting"]))
    return HttpResponse("success") @require_POST
@lti_role_required(ADMINS)
def delete_experiment(request, experiment_id):
    """ If Http404 is raised, delete_experiment redirects regardless. This is by
        design, as multiple users may be deleting concurrently in the same course """
    course_id = get_lti_param(request, "custom_canvas_course_id")
    try:
        experiment = Experiment.get_or_404_check_course(experiment_id, course_id)
        experiment.assert_not_finalized()
        canvas_modules = CanvasModules(request)
        if canvas_modules.experiment_has_installed_intervention(experiment):
            raise INTERVENTION_POINTS_ARE_INSTALLED
        experiment.delete()
    except Http404:
        pass
    return redirect(reverse("ab_testing_tool_index")) @require_POST
@lti_role_required(ADMINS)
def finalize_tracks(request, experiment_id):
    course_id = get_lti_param(request, "custom_canvas_course_id")
    experiment = Experiment.get_or_404_check_course(experiment_id, course_id)
    if not experiment.tracks.count():
        raise NO_TRACKS_FOR_EXPERIMENT
    intervention_points = experiment.intervention_points.all()
    incomplete_intervention_points = get_incomplete_intervention_points(intervention_points)
    if incomplete_intervention_points:
        return HttpResponse("URLs missing for these tracks in these Intervention Points: %s"
                            % ", ".join(incomplete_intervention_points))
    missing_track_weights = get_missing_track_weights(experiment, course_id)
    if missing_track_weights:
        return HttpResponse("Track weightings missing for these tracks: %s"
                            % ", ".join(missing_track_weights))
    experiment.tracks_finalized = True
    experiment.save()
    return redirect(reverse("ab_testing_tool_index")) @require_POST
@lti_role_required(ADMINS)
def copy_experiment(request, experiment_id):
    course_id = get_lti_param(request, "custom_canvas_course_id")
    experiment = Experiment.get_or_404_check_course(experiment_id, course_id)
    experiments_with_prefix = set([e.name for e in Experiment.objects.filter(
            course_id=course_id, name__startswith=experiment.name)])
    for i in range(1,1000):
        new_name = "%s_copy%s" % (experiment.name, i)
        if new_name not in experiments_with_prefix:
            experiment.copy(new_name)
            return redirect(reverse("ab_testing_tool_index"))
    raise COPIES_EXCEEDS_LIMIT @require_POST
@lti_role_required(ADMINS)
def delete_track(request, track_id):
    """ If Http404 is raised, delete_track redirects regardless. This is by
        design, as multiple users may be deleting concurrently in the same course """
    course_id = get_lti_param(request, "custom_canvas_course_id")
    try:
        track = Track.get_or_404_check_course(track_id, course_id)
        track.delete()
    except Http404:
        pass
    return HttpResponse("success") @lti_role_required(ADMINS)
def track_selection_xlsx(request, experiment_id):
    course_id = get_lti_param(request, "custom_canvas_course_id")
    experiment = Experiment.get_or_404_check_course(experiment_id, course_id)
    return get_track_selection_xlsx(request, experiment, "track_selection.xlsx") @lti_role_required(ADMINS)
def track_selection_csv(request, experiment_id):
    course_id = get_lti_param(request, "custom_canvas_course_id")
    experiment = Experiment.get_or_404_check_course(experiment_id, course_id)
    return get_track_selection_csv(request, experiment, "track_selection.csv") @lti_role_required(ADMINS)
def upload_track_assignments(request, experiment_id):
    course_id = get_lti_param(request, "custom_canvas_course_id")
    experiment = Experiment.get_or_404_check_course(experiment_id, course_id)
    uploaded_file = request.FILES["track_assignments"]
    if (uploaded_file.size > int(settings.MAX_FILE_UPLOAD_SIZE)):
        raise FILE_TOO_LARGE
    uploaded_text = uploaded_file.read()
    unassigned_students = get_unassigned_students(request, experiment)
    students, errors = parse_uploaded_file(
            experiment, unassigned_students, uploaded_text, uploaded_file.name
    )
    if errors:
        return render_to_response("ab_tool/spreadsheetErrors.html", {"errors": errors})
    
    for student_id, track in students.items():
        # lis_person_sourcedid is not returned by SDK, so we set it to None
        ExperimentStudent.objects.create(
            student_id=student_id, course_id=experiment.course_id,
            track=track, student_name=unassigned_students[student_id],
            experiment=experiment
        )
    if not experiment.tracks_finalized:
        experiment.update(tracks_finalized=True)
    return redirect(reverse("ab_testing_tool_index"))
from django.conf.urls import patterns, url
from ab_tool.views.main_pages import (render_control_panel, not_authorized,
    tool_config, download_track_assignments, download_intervention_point_interactions)
from ab_tool.views.selection_pages import (resource_selection, submit_selection)
from ab_tool.views.intervention_point_pages import (submit_create_intervention_point,
    delete_intervention_point, deploy_intervention_point,
    submit_edit_intervention_point, modules_page_edit_intervention_point,
    modules_page_view_intervention_point,
    submit_edit_intervention_point_from_modules)
from ab_tool.views.experiment_pages import (create_experiment, submit_create_experiment,
    edit_experiment, submit_edit_experiment, delete_experiment, finalize_tracks,
    copy_experiment, delete_track, track_selection_xlsx, track_selection_csv,
    upload_track_assignments)



urlpatterns = patterns('',
    url(r'^$', render_control_panel, name='ab_testing_tool_index'),
    url(r'^not_authorized$', not_authorized, name='ab_testing_tool_not_authorized'),
    url(r'^download_track_assignments/(?P<experiment_id>\d+)$', download_track_assignments, name='ab_testing_tool_download_data'),
    url(r'^download_intervention_point_interactions/(?P<experiment_id>\d+)$', download_intervention_point_interactions,
        name='ab_testing_tool_download_intervention_point_interactions'),
    url(r'^tool_config$', tool_config, name='ab_testing_tool_tool_config'),
    
    url(r'^resource_selection$', resource_selection, name="ab_testing_tool_resource_selection"),
    url(r'^submit_selection$', submit_selection, name="ab_testing_tool_submit_selection"),
    # TODO: re-enable submit_selection_new_intervention_point() once it is available through the UI
    # url(r'^submit_selection_new_intervention_point$', submit_selection_new_intervention_point, name="ab_testing_tool_submit_selection_new_intervention_point"),
    
    url(r'^submit_create_intervention_point/(?P<experiment_id>\d+)$', submit_create_intervention_point, name='ab_testing_tool_submit_create_intervention_point'),
    url(r'^modules_page_view_intervention_point/(?P<intervention_point_id>\d+)$', modules_page_view_intervention_point, name='ab_testing_tool_modules_page_view_intervention_point'),
    url(r'^modules_page_edit_intervention_point/(?P<intervention_point_id>\d+)$', modules_page_edit_intervention_point, name='ab_testing_tool_modules_page_edit_intervention_point'),
    url(r'^submit_edit_intervention_point/(?P<intervention_point_id>\d+)$', submit_edit_intervention_point, name='ab_testing_tool_submit_edit_intervention_point'),
    url(r'^submit_edit_intervention_point_from_modules/(?P<intervention_point_id>\d+)$', submit_edit_intervention_point_from_modules, name='ab_testing_tool_submit_edit_intervention_point_from_modules'),
    url(r'^intervention_point/(?P<intervention_point_id>\d+)$', deploy_intervention_point, name='ab_testing_tool_deploy_intervention_point'),
    url(r'^delete_intervention_point/(?P<intervention_point_id>\d+)$', delete_intervention_point, name='ab_testing_tool_delete_intervention_point'),
    
    url(r'^create_experiment$', create_experiment, name='ab_testing_tool_create_experiment'),
    url(r'^submit_create_experiment$', submit_create_experiment, name='ab_testing_tool_submit_create_experiment'),
    url(r'^edit_experiment/(?P<experiment_id>\d+)$', edit_experiment, name='ab_testing_tool_edit_experiment'),
    url(r'^submit_edit_experiment/(?P<experiment_id>\d+)$', submit_edit_experiment, name='ab_testing_tool_submit_edit_experiment'),

    url(r'^delete_experiment/(?P<experiment_id>\d+)$', delete_experiment, name='ab_testing_tool_delete_experiment'),