Example #1
0
def record_event(request):
    """Endpoint to record user events."""
    user_id = request.POST.get('userid')
    app_id = request.POST.get('appid')
    campaign_id = request.POST.get('campaignid')
    content_id = request.POST.get('contentid')
    content = request.POST.get('content', '')
    action_id = request.POST.get('actionid')
    event_type = request.POST.get('eventType')
    extend = request.POST.get('extend_token', False)

    (friends, friend_fbids) = ([], [])
    for uid in request.POST.getlist('friends[]'):
        if uid.isdigit():
            uid = int(uid)
            friend_fbids.append(uid)
        friends.append(uid)

    if campaign_id:
        try:
            campaign = relational.Campaign.objects.get(campaign_id=campaign_id)
        except (relational.Campaign.DoesNotExist, ValueError):
            return http.HttpResponseBadRequest("No such campaign: {}".format(campaign_id))
    else:
        campaign = None

    if event_type not in ALL_EVENTS:
        return http.HttpResponseForbidden("Ah, ah, ah. You didn't say the magic word")

    # Create event(s) according to record type #

    if event_type in SINGULAR_EVENTS:
        request.visit.events.first_or_create(
            event_type=event_type,
            defaults={
                'campaign_id': campaign_id,
                'client_content_id': content_id,
                'content': content,
                'activity_id': action_id,
            }
        )
    elif event_type in UPDATED_EVENTS:
        # This is (currently) just the 'heartbeat' event
        with transaction.atomic():
            (event, created) = relational.Event.objects.select_for_update().first_or_create(
                event_type=event_type,
                visit=request.visit,
                defaults={
                    'campaign_id': campaign_id,
                    'client_content_id': content_id,
                    'content': '1',
                }
            )
            if not created:
                if campaign_id and not event.campaign_id:
                    event.campaign_id = campaign_id
                if content_id and not event.client_content_id:
                    event.client_content_id = content_id
                event.content = F('content') + 1
                event.save()
    elif friends:
        db.bulk_create.delay([
            relational.Event(
                visit_id=request.visit.visit_id,
                campaign_id=campaign_id,
                client_content_id=content_id,
                friend_fbid=friend if isinstance(friend, int) else None,
                content=content,
                activity_id=action_id,
                event_type=event_type,
            )
            for friend in friends
        ])
    else:
        db.delayed_save.delay(
            relational.Event(
                visit_id=request.visit.visit_id,
                campaign_id=campaign_id,
                client_content_id=content_id,
                content=content[:1028],
                activity_id=action_id,
                event_type=event_type,
            )
        )

    # Additional, event_type-specific handling #

    if event_type == 'authorized':
        try:
            fbid = int(user_id)
            appid = int(app_id)
            token_string = request.POST['token']
            api = request.POST['api']
        except (KeyError, ValueError, TypeError):
            fbid = appid = token_string = api = None

        if not all([fbid, appid, token_string, campaign, api]):
            msg = ("Cannot write authorization for fbid %r, appid %r, api %r "
                   "and token %r under campaign %r")
            args = (user_id, app_id, request.POST.get('api'),
                    request.POST.get('token'), campaign_id)
            LOG.warning(msg, *args, extra={'request': request})
            return http.HttpResponseBadRequest(msg % args)

        campaign.client.userclients.get_or_create(fbid=fbid)
        if extend:
            extend_token.delay(fbid, appid, token_string, api)

    elif event_type == 'shared':
        root_campaign = relational.Campaign.objects.get(rootcampaign_properties__campaign=campaign_id)

        # Write friends with whom we shared to the exclusions table,
        # so we don't show them for the same content/campaign again.
        # Rather than wait on as many as 10 get+inserts, background the task.
        exclusions = [
            {
                'fbid': user_id,
                'campaign_id': root_campaign.campaign_id,
                'content_id': content_id,
                'friend_fbid': friend,
                'defaults': {
                    'reason': 'shared',
                }
            } for friend in friend_fbids
        ]
        if exclusions:
            db.get_or_create.delay(relational.FaceExclusion, *exclusions)

        # Attempt to avoid race condition between subsequent reload of faces
        # page and above record of exclusions by storing these in session:
        faces_exclusions_key = PENDING_EXCLUSIONS_KEY.format(campaign_id=root_campaign.campaign_id,
                                                             content_id=content_id,
                                                             fbid=user_id)
        request.session[faces_exclusions_key] = friend_fbids

    # Additional handling #

    error_msg = request.POST.get('errorMsg[message]')
    if error_msg:
        # may want to push these to the DB at some point, but at least for now,
        # dump them to the logs to ensure we keep the data.
        LOG.warning(
            'Front-end error encountered for user %s in session %s: %s',
            user_id, request.session.session_key, error_msg,
            extra={'request': request}
        )

    share_msg = request.POST.get('shareMsg')
    if share_msg:
        db.delayed_save.delay(
            relational.ShareMessage(
                activity_id=action_id,
                fbid=user_id,
                campaign_id=campaign_id,
                content_id=content_id,
                message=share_msg,
            )
        )

    return http.HttpResponse()
Example #2
0
def faces(request):
    faces_form = forms.FacesForm(request.POST)
    if not faces_form.is_valid():
        return JsonHttpResponse(faces_form.errors, status=400)

    data = faces_form.cleaned_data
    campaign = root_campaign = data['campaign']
    content = data['content']
    client = campaign.client

    if not request.session.get('sessionverified', False):
        # Avoid spamming the workers with an agent who can't hold onto its session
        if data['last_call']:
            LOG.fatal("User agent failed cookie test. (Will return error to user.)",
                      extra={'request': request})
            return http.HttpResponseForbidden("Cookies are required.")

        # Agent failed test, but give it another shot
        LOG.warning("Suspicious session missing cookie test", extra={'request': request})
        request.session.set_test_cookie()
        return JsonHttpResponse({
            'status': 'waiting',
            'reason': "Cookies are required. Please try again.",
            'campaignid': campaign.pk,
            'contentid': content.pk,
        }, status=202)

    faces_tasks_key = FACES_TASKS_KEY.format(api=data['api'],
                                             campaign_id=campaign.pk,
                                             content_id=content.pk,
                                             fbid=data['fbid'])
    targeting_task_ids = request.session.get(faces_tasks_key)

    if targeting_task_ids:
        # Retrieve statuses of active targeting tasks #
        ranked_tasks = [(rank, celery.current_app.AsyncResult(task_id))
                        for (rank, task_id) in targeting_task_ids]
    else:
        # First request #
        token = datastructs.ShortToken(
            fbid=data['fbid'],
            appid=client.fb_app_id,
            token=data['token'],
            api=data['api'],
        )

        # Extend & store Token and record authorized UserClient:
        extend_token.delay(*token)
        db.get_or_create.delay(
            relational.UserClient,
            client_id=client.pk,
            fbid=data['fbid'],
        )

        # Initiate targeting tasks:
        ranked_tasks = request_targeting(
            visit=request.visit,
            token=token,
            api=data['api'],
            campaign=campaign,
            client_content=content,
            num_faces=data['num_face'],
        )
        targeting_task_ids = [(rank, task.id) for (rank, task) in ranked_tasks]
        request.session[faces_tasks_key] = targeting_task_ids

    (targeting_ranks, targeting_tasks) = zip(*ranked_tasks)

    # Check status #
    (primary_rank, primary_task) = ranked_tasks[0]
    if not all(task.ready() for task in targeting_tasks) and not primary_task.failed() and not data['last_call']:
        return JsonHttpResponse({
            'status': 'waiting',
            'reason': "Identifying friends.",
            'campaignid': campaign.pk,
            'contentid': content.pk,
        }, status=202)

    # Select results #
    ranked_results = [
        (rank, task.result if task.successful() else targeting.empty_filtering_result)
        for (rank, task) in ranked_tasks
    ]
    # Choose the filtered results from the highest-ranked results:
    for (result_rank, targeting_result) in reversed(ranked_results):
        if targeting_result.filtered:
            # Let's use this result set;
            # but, gather ranking data from the others as well.
            for (complement_rank, complement_result) in ranked_results:
                if complement_result is not targeting_result and complement_result.ranked:
                    if complement_rank > result_rank:
                        # Trust higher-ranked complement's results ranking:
                        targeting_result = targeting_result._replace(
                            ranked=complement_result.ranked,
                            filtered=targeting_result.filtered.reranked(complement_result.ranked,
                                                                        result_rank),
                        )
                    else:
                        # Our ranking is best;
                        # just include lower-ranked complement's scores (for reporting):
                        targeting_result = targeting_result._replace(
                            filtered=targeting_result.filtered.rescored(complement_result.ranked,
                                                                        complement_rank),
                        )

            # We're done
            break

    if not targeting_result.ranked or not targeting_result.filtered:
        if (
            primary_task.failed() and
            isinstance(primary_task.result, facebook.utils.OAuthPermissionDenied) and
            primary_task.result.requires_review
        ):
            return JsonHttpResponse({
                'status': 'failed',
                'reason': "This app has not been approved by Facebook, and is only accessible "
                          "to admins, developers and the app's test users.\n\n"
                          "If you are a Facebook App reviewer, please log in as "
                          '"Open Graph Test User" and try again.',
            }, status=403)
        elif primary_task.ready():
            return http.HttpResponseServerError('No friends were identified for you.')
        else:
            LOG.fatal("primary targeting task (px%s) failed to complete in the time allotted (%s)",
                      primary_rank or 0, primary_task.task_id, extra={'request': request})
            return http.HttpResponse('Response has taken too long, giving up', status=503)

    if targeting_result.campaign_id and targeting_result.campaign_id != campaign.pk:
        campaign = relational.Campaign.objects.get(pk=targeting_result.campaign_id)
    if targeting_result.content_id and targeting_result.content_id != content.pk:
        content = relational.ClientContent.objects.get(pk=targeting_result.content_id)

    # Apply campaign
    if data['efobjsrc']:
        fb_object = facebook.third_party.source_campaign_fbobject(campaign, data['efobjsrc'])
        db.delayed_save.delay(
            relational.Assignment.make_managed(
                visit_id=request.visit.pk,
                campaign_id=campaign.pk,
                content_id=content.pk,
                feature_row=fb_object,
                chosen_from_rows=None,
                manager=campaign.campaignfbobjects,
                random_assign=False,
            )
        )
    else:
        fb_object = campaign.campaignfbobjects.for_datetime().random_assign()
        db.delayed_save.delay(
            relational.Assignment.make_managed(
                visit_id=request.visit.pk,
                campaign_id=campaign.pk,
                content_id=content.pk,
                feature_row=fb_object,
                chosen_from_rows=campaign.campaignfbobjects.for_datetime(),
            )
        )

    fb_attrs = fb_object.fbobjectattribute_set.for_datetime().get()
    fb_object_url = 'https://%s%s?%s' % (
        request.get_host(),
        reverse('targetshare:objects', kwargs={
            'fb_object_id': fb_object.pk,
            'content_id': content.pk,
        }),
        urllib.urlencode({
            'cssslug': targeting_result.choice_set_slug,
            'campaign_id': campaign.pk,
        }),
    )
    fb_params = {
        'fb_action_type': fb_attrs.og_action,
        'fb_object_type': fb_attrs.og_type,
        'fb_object_url': fb_object_url,
        'fb_app_name': client.fb_app.name,
        'fb_app_id': client.fb_app_id,
        'fb_object_title': fb_attrs.og_title,
        'fb_object_image': fb_attrs.og_image,
        'fb_object_description': fb_attrs.og_description
    }

    # Record generation of suggestions (but only once per set of tasks):
    generated = []
    gen_count = 0
    for tier in targeting_result.filtered:
        for edge in itertools.islice(tier['edges'], MAX_FACES - gen_count):
            ranked_scores = (
                # ((pretty rank), (pretty score))
                (('' if rank is None else rank), ('N/A' if score is None else score))
                for (rank, score) in ((rank, edge.get_rank_score(rank)) for rank in targeting_ranks)
            )
            px_content = ', '.join(
                "px{}_score: {} ({})".format(rank, score, task.task_id)
                for ((rank, score), task) in zip(ranked_scores, targeting_tasks)
            )
            generated.append({
                'visit_id': request.visit.visit_id,
                'campaign_id': tier['campaign_id'],
                'client_content_id': tier['content_id'],
                'friend_fbid': edge.secondary.fbid,
                'event_type': 'generated',
                'content': u"{} : {}".format(px_content, edge.secondary.name),
                'defaults': {
                    'event_datetime': timezone.now(),
                },
            })

        gen_count = len(generated)
        if gen_count >= MAX_FACES:
            break

    db.get_or_create.delay(relational.Event, *generated)

    # Re-apply exclusions to pick up any shares and suppressions since results first generated:
    faces_exclusions_key = PENDING_EXCLUSIONS_KEY.format(campaign_id=root_campaign.pk,
                                                         content_id=content.pk,
                                                         fbid=data['fbid'])
    enqueued_exclusions = request.session.get(faces_exclusions_key, ())
    existing_exclusions = root_campaign.faceexclusion_set.filter(
        fbid=data['fbid'],
        content=content,
    ).values_list('friend_fbid', flat=True)
    all_exclusions = set(itertools.chain(enqueued_exclusions, existing_exclusions.iterator()))

    # Determine faces that can be shown (and record these):
    (face_friends, show_faces, shown) = ([], [], [])
    mapped_task_ids = dict(targeting_task_ids)
    eligible_edges = (edge for edge in targeting_result.filtered.iteredges()
                      if edge.secondary.fbid is None or edge.secondary.fbid not in all_exclusions)
    for (edge_index, edge) in enumerate(itertools.islice(eligible_edges, MAX_FACES)):
        if edge_index >= data['num_face']:
            face_friends.append(edge.secondary)
        else:
            show_faces.append(edge.secondary)

            try:
                (score_rank, score) = edge.get_score()
            except LookupError:
                score = 'N/A'
                score_rank = primary_rank

            px_content = "px{}_score: {} ({})".format(
                '' if score_rank is None else score_rank,
                score,
                mapped_task_ids[score_rank],
            )

            shown.append(
                relational.Event(
                    visit_id=request.visit.visit_id,
                    campaign_id=campaign.campaign_id,
                    client_content_id=content.content_id,
                    friend_fbid=edge.secondary.fbid,
                    content=u"{} : {}".format(px_content, edge.secondary.name),
                    event_type='shown',
                )
            )

    if not show_faces:
        LOG.fatal("No faces to show, (all suppressed). (Will return error to user.)",
                  extra={'request': request})
        return http.HttpResponseServerError("No friends remaining.")

    db.bulk_create.delay(shown)

    rendered_table = render_to_string('targetshare/faces_table.html', {
        'msg_params': {
            'sharing_prompt': fb_attrs.sharing_prompt,
            'sharing_sub_header': fb_attrs.sharing_sub_header,
            'sharing_button': fb_attrs.sharing_button,
            'msg1_pre': fb_attrs.msg1_pre,
            'msg1_post': fb_attrs.msg1_post,
            'msg2_pre': fb_attrs.msg2_pre,
            'msg2_post': fb_attrs.msg2_post,
        },
        'fb_params': fb_params,
        'all_friends': [edge.secondary for edge in targeting_result.ranked],
        'face_friends': face_friends,
        'show_faces': show_faces,
        'num_face': data['num_face'],
    }, context_instance=RequestContext(request))

    return JsonHttpResponse({
        'status': 'success',
        'campaignid': campaign.pk,
        'contentid': content.pk,
        'html': rendered_table,
    })