Ejemplo n.º 1
0
    def test_basic(self):
        data = [
            (None, None),
            ('', ''),
            ('http://example.com/', 'http://example.com/'),
            ('http://example.com/#foo', 'http://example.com/'),
            ('http://example.com/?foo=bar', 'http://example.com/'),
            ('http://example.com:8000/', 'http://example.com/'),
            ('ftp://foo.bar/', ''),
            ('chrome://something', 'chrome://something'),
            ('about:home', 'about:home'),
        ]

        for url, expected in data:
            eq_(clean_url(url), expected)
Ejemplo n.º 2
0
    def test_basic(self):
        data = [
            (None, None),
            ('', ''),
            ('http://example.com/', 'http://example.com/'),
            ('http://example.com/#foo', 'http://example.com/'),
            ('http://example.com/?foo=bar', 'http://example.com/'),
            ('http://example.com:8000/', 'http://example.com/'),
            ('ftp://foo.bar/', ''),
            ('chrome://something', 'chrome://something'),
            ('about:home', 'about:home'),
        ]

        for url, expected in data:
            eq_(clean_url(url), expected)
Ejemplo n.º 3
0
def _handle_feedback_post(request, locale=None, product=None,
                          version=None, channel=None):
    """Saves feedback post to db accounting for throttling

    :arg request: request we're handling the post for
    :arg locale: locale specified in the url
    :arg product: None or the Product
    :arg version: validated and sanitized version specified in the url
    :arg channel: validated and sanitized channel specified in the url

    """
    if getattr(request, 'limited', False):
        # If we're throttled, then return the thanks page, but don't
        # add the response to the db.
        return HttpResponseRedirect(reverse('thanks'))

    # Get the form and run is_valid() so it goes through the
    # validation and cleaning machinery. We don't really care if it's
    # valid, though, since we will take what we got and do the best we
    # can with it. Error validation is now in JS.
    form = ResponseForm(request.POST)
    form.is_valid()

    get_data = request.GET.copy()

    data = form.cleaned_data

    description = data.get('description', u'').strip()
    if not description:
        # If there's no description, then there's nothing to do here,
        # so thank the user and move on.
        return HttpResponseRedirect(reverse('thanks'))

    opinion = models.Response(
        # Data coming from the user
        happy=data['happy'],
        url=clean_url(data.get('url', u'').strip()),
        description=description,

        # Pulled from the form data or the url
        locale=data.get('locale', locale),

        # Data from mobile devices which is probably only
        # applicable to mobile devices
        manufacturer=data.get('manufacturer', ''),
        device=data.get('device', ''),
    )

    # Add user_agent and inferred data.
    user_agent = request.META.get('HTTP_USER_AGENT', '')
    if user_agent:
        browser = request.BROWSER

        opinion.user_agent = user_agent
        opinion.browser = browser.browser
        opinion.browser_version = browser.browser_version
        opinion.browser_platform = browser.platform
        if browser.platform == 'Windows':
            opinion.browser_platform += ' ' + browser.platform_version

    # source is src or utm_source
    source = (
        get_data.pop('src', [u''])[0] or
        get_data.pop('utm_source', [u''])[0]
    )
    if source:
        opinion.source = source[:100]

    campaign = get_data.pop('utm_campaign', [u''])[0]
    if campaign:
        opinion.campaign = campaign[:100]

    # If they sent "happy=1"/"happy=0" in the querystring, it will get
    # picked up by the javascript in the form and we can just drop it
    # here.
    get_data.pop('happy', None)

    platform = u''

    if product:
        # If we have a product at this point, then it came from the
        # url and it's a Product instance and we need to turn it into
        # the product.db_name which is a string.
        product_db_name = product.db_name
    else:
        # Check the POST data for the product.
        product_db_name = data.get('product', '')

    # For the version, we try the url data, then the POST data.
    version = version or data.get('version', '')

    # At this point, we have a bunch of values, but we might be
    # missing some values, too. We're going to cautiously infer data
    # from the user agent where we're very confident it's appropriate
    # to do so.
    if request.BROWSER != UNKNOWN:
        # If we don't have a product, try to infer that from the user
        # agent information.
        if not product_db_name:
            product_db_name = models.Response.infer_product(request.BROWSER)

        # If we have a product and it matches the user agent browser,
        # then we can infer the version and platform from the user
        # agent if they're missing.
        if product_db_name:
            product = models.Product.objects.get(db_name=product_db_name)
            if product.browser and product.browser == request.BROWSER.browser:
                if not version:
                    version = request.BROWSER.browser_version
                if not platform:
                    platform = models.Response.infer_platform(
                        product_db_name, request.BROWSER)

    # Make sure values are at least empty strings--no Nones.
    opinion.product = product_db_name or u''
    opinion.version = version or u''
    opinion.channel = channel or u''
    opinion.platform = platform or u''

    opinion.save()

    # If there was an email address, save that separately.
    if data.get('email_ok') and data.get('email'):
        e = models.ResponseEmail(email=data['email'], opinion=opinion)
        e.save()
        statsd.incr('feedback.emaildata.optin')

    # If there's browser data, save that separately.
    if data.get('browser_ok'):
        # This comes in as a JSON string. Because we're using
        # JSONObjectField, we need to convert it back to Python and
        # then save it. This is kind of silly, but it does guarantee
        # we have valid JSON.
        try:
            browser_data = data['browser_data']
            browser_data = json.loads(browser_data)

        except ValueError:
            # Handles empty string and any non-JSON value.
            statsd.incr('feedback.browserdata.badvalue')

        except KeyError:
            # Handles the case where it's missing from the data
            # dict. If it's missing, we don't want to do anything
            # including metrics.
            pass

        else:
            # If browser_data isn't an empty dict, then save it.
            if browser_data:
                rti = models.ResponsePI(
                    data=browser_data, opinion=opinion)
                rti.save()
                statsd.incr('feedback.browserdata.optin')

    if get_data:
        # There was extra context in the query string, so we grab that
        # with some restrictions and save it separately.
        slop = {}

        # We capture at most the first 20 key/val pairs
        get_data_items = sorted(get_data.items())[:20]

        for key, val in get_data_items:
            # Keys can be at most 20 characters long.
            key = key[:20]
            if len(val) == 1:
                val = val[0]

            # Values can be at most 20 characters long.
            val = val[:100]
            slop[key.encode('utf-8')] = val.encode('utf-8')

        context = models.ResponseContext(data=slop, opinion=opinion)
        context.save()
        statsd.incr('feedback.contextdata.optin')

    if data['happy']:
        statsd.incr('feedback.happy')
    else:
        statsd.incr('feedback.sad')

    request.session['response_id'] = opinion.id

    return HttpResponseRedirect(reverse('thanks'))
Ejemplo n.º 4
0
def _handle_feedback_post(request, locale=None, product=None, version=None, channel=None):
    if getattr(request, "limited", False):
        # If we're throttled, then return the thanks page, but don't
        # add the response to the db.
        return HttpResponseRedirect(reverse("thanks"))

    # Get the form and run is_valid() so it goes through the
    # validation and cleaning machinery. We don't really care if it's
    # valid, though, since we will take what we got and do the best we
    # can with it. Error validation is now in JS.
    form = ResponseForm(request.POST)
    form.is_valid()

    data = form.cleaned_data
    description = data.get("description", u"").strip()
    if not description:
        # If there's no description, then there's nothing to do here,
        # so thank the user and move on.
        return HttpResponseRedirect(reverse("thanks"))

    # Do some data validation of product, channel and version
    # coming from the url.
    product = models.Product.get_product_map().get(smart_str(product), u"")
    # FIXME - validate these better
    channel = smart_str(channel).lower()
    version = smart_str(version)

    # src, then source, then utm_source
    source = request.GET.get("src", u"")
    if not source:
        source = request.GET.get("utm_source", u"")

    campaign = request.GET.get("utm_campaign", u"")

    # Most platforms aren't different enough between versions to care.
    # Windows is.
    platform = request.BROWSER.platform
    if platform == "Windows":
        platform += " " + request.BROWSER.platform_version

    opinion = models.Response(
        # Data coming from the user
        happy=data["happy"],
        url=clean_url(data.get("url", u"")),
        description=data["description"].strip(),
        # Inferred data from user agent
        prodchan=_get_prodchan(request, product, channel),
        user_agent=request.META.get("HTTP_USER_AGENT", ""),
        browser=request.BROWSER.browser,
        browser_version=request.BROWSER.browser_version,
        platform=platform,
        # Pulled from the form data or the url
        locale=data.get("locale", locale),
        # Data from mobile devices which is probably only
        # applicable to mobile devices
        manufacturer=data.get("manufacturer", ""),
        device=data.get("device", ""),
    )

    if source:
        opinion.source = source[:100]

    if campaign:
        opinion.campaign = campaign[:100]

    if product:
        # If we picked up the product from the url, we use url
        # bits for everything.
        product = product or u""
        version = version or u""
        channel = channel or u""

    elif opinion.browser != UNKNOWN:
        # If we didn't pick up a product from the url, then we
        # infer as much as we can from the user agent.
        product = data.get("product", models.Response.infer_product(platform))
        version = data.get("version", request.BROWSER.browser_version)
        # Assume everything we don't know about is stable channel.
        channel = u"stable"

    else:
        product = channel = version = u""

    opinion.product = product or u""
    opinion.version = version or u""
    opinion.channel = channel or u""

    opinion.save()

    # If there was an email address, save that separately.
    if data.get("email_ok") and data.get("email"):
        e = models.ResponseEmail(email=data["email"], opinion=opinion)
        e.save()

    return HttpResponseRedirect(reverse("thanks"))
Ejemplo n.º 5
0
def _handle_feedback_post(request, locale=None, product=None,
                          version=None, channel=None):
    """Saves feedback post to db accounting for throttling

    :arg request: request we're handling the post for
    :arg locale: locale specified in the url
    :arg product: validated and sanitized product slug specified in
        the url
    :arg version: validated and sanitized version specified in the url
    :arg channel: validated and sanitized channel specified in the url

    """
    if getattr(request, 'limited', False):
        # If we're throttled, then return the thanks page, but don't
        # add the response to the db.
        return HttpResponseRedirect(reverse('thanks'))

    # Get the form and run is_valid() so it goes through the
    # validation and cleaning machinery. We don't really care if it's
    # valid, though, since we will take what we got and do the best we
    # can with it. Error validation is now in JS.
    form = ResponseForm(request.POST)
    form.is_valid()

    get_data = request.GET.copy()

    data = form.cleaned_data
    description = data.get('description', u'').strip()
    if not description:
        # If there's no description, then there's nothing to do here,
        # so thank the user and move on.
        return HttpResponseRedirect(reverse('thanks'))

    # Do some data validation of product, channel and version
    # coming from the url.
    if product:
        # If there was a product in the url, that's a product slug, so
        # we map it to a db_name which is what we want to save to the
        # db.
        product = models.Product.get_product_map()[product]

    # src, then source, then utm_source
    source = get_data.pop('src', [u''])[0]
    if not source:
        source = get_data.pop('utm_source', [u''])[0]

    campaign = get_data.pop('utm_campaign', [u''])[0]

    # If the product came in on the url, then we only want to populate
    # the platfrom from the user agent data iff the product specified
    # by the url is the same as the browser product.
    platform = u''
    if product is None or product == request.BROWSER.browser:
        # Most platforms aren't different enough between versions to care.
        # Windows is.
        platform = request.BROWSER.platform
        if platform == 'Windows':
            platform += ' ' + request.BROWSER.platform_version

    product = product or u''

    opinion = models.Response(
        # Data coming from the user
        happy=data['happy'],
        url=clean_url(data.get('url', u'')),
        description=data['description'].strip(),

        # Inferred data from user agent
        user_agent=request.META.get('HTTP_USER_AGENT', ''),
        browser=request.BROWSER.browser,
        browser_version=request.BROWSER.browser_version,
        platform=platform,

        # Pulled from the form data or the url
        locale=data.get('locale', locale),

        # Data from mobile devices which is probably only
        # applicable to mobile devices
        manufacturer=data.get('manufacturer', ''),
        device=data.get('device', ''),
    )

    if source:
        opinion.source = source[:100]

    if campaign:
        opinion.campaign = campaign[:100]

    if product:
        # If we picked up the product from the url, we use url
        # bits for everything.
        product = product or u''
        version = version or u''
        channel = channel or u''

    elif opinion.browser != UNKNOWN:
        # If we didn't pick up a product from the url, then we
        # infer as much as we can from the user agent.
        product = data.get(
            'product', models.Response.infer_product(platform))
        version = data.get(
            'version', request.BROWSER.browser_version)
        # Assume everything we don't know about is stable channel.
        channel = u'stable'

    else:
        product = channel = version = u''

    opinion.product = product or u''
    opinion.version = version or u''
    opinion.channel = channel or u''

    opinion.save()

    # If there was an email address, save that separately.
    if data.get('email_ok') and data.get('email'):
        e = models.ResponseEmail(email=data['email'], opinion=opinion)
        e.save()

    if get_data:
        # There was extra context in the query string, so we grab that
        # with some restrictions and save it separately.
        slop = {}

        # We capture at most the first 20 key/val pairs
        get_data_items = sorted(get_data.items())[:20]

        for key, val in get_data_items:
            # Keys can be at most 20 characters long.
            key = key[:20]
            if len(val) == 1:
                val = val[0]

            # Values can be at most 20 characters long.
            val = val[:100]
            slop[key.encode('utf-8')] = val.encode('utf-8')

        context = models.ResponseContext(data=slop, opinion=opinion)
        context.save()

    if data['happy']:
        statsd.incr('feedback.happy')
    else:
        statsd.incr('feedback.sad')

    return HttpResponseRedirect(reverse('thanks'))
Ejemplo n.º 6
0
def _handle_feedback_post(request,
                          locale=None,
                          product=None,
                          version=None,
                          channel=None):
    """Saves feedback post to db accounting for throttling

    :arg request: request we're handling the post for
    :arg locale: locale specified in the url
    :arg product: None or the Product
    :arg version: validated and sanitized version specified in the url
    :arg channel: validated and sanitized channel specified in the url

    """
    if getattr(request, 'limited', False):
        # If we're throttled, then return the thanks page, but don't
        # add the response to the db.
        return HttpResponseRedirect(reverse('thanks'))

    # Get the form and run is_valid() so it goes through the
    # validation and cleaning machinery. We don't really care if it's
    # valid, though, since we will take what we got and do the best we
    # can with it. Error validation is now in JS.
    form = ResponseForm(request.POST)
    form.is_valid()

    get_data = request.GET.copy()

    data = form.cleaned_data

    description = data.get('description', u'').strip()
    if not description:
        # If there's no description, then there's nothing to do here,
        # so thank the user and move on.
        return HttpResponseRedirect(reverse('thanks'))

    opinion = models.Response(
        # Data coming from the user
        happy=data['happy'],
        url=clean_url(data.get('url', u'').strip()),
        description=description,

        # Pulled from the form data or the url
        locale=data.get('locale', locale),

        # Data from mobile devices which is probably only
        # applicable to mobile devices
        manufacturer=data.get('manufacturer', ''),
        device=data.get('device', ''),
    )

    # Add user_agent and inferred data.
    user_agent = request.META.get('HTTP_USER_AGENT', '')
    if user_agent:
        browser = request.BROWSER

        opinion.user_agent = user_agent
        opinion.browser = browser.browser
        opinion.browser_version = browser.browser_version
        opinion.browser_platform = browser.platform
        if browser.platform == 'Windows':
            opinion.browser_platform += ' ' + browser.platform_version

    # source is src or utm_source
    source = (get_data.pop('src', [u''])[0]
              or get_data.pop('utm_source', [u''])[0])
    if source:
        opinion.source = source[:100]

    campaign = get_data.pop('utm_campaign', [u''])[0]
    if campaign:
        opinion.campaign = campaign[:100]

    platform = u''

    if product:
        # If we have a product at this point, then it came from the
        # url and it's a Product instance and we need to turn it into
        # the product.db_name which is a string.
        product_db_name = product.db_name
    else:
        # Check the POST data for the product.
        product_db_name = data.get('product', '')

    # For the version, we try the url data, then the POST data.
    version = version or data.get('version', '')

    # At this point, we have a bunch of values, but we might be
    # missing some values, too. We're going to cautiously infer data
    # from the user agent where we're very confident it's appropriate
    # to do so.
    if request.BROWSER != UNKNOWN:
        # If we don't have a product, try to infer that from the user
        # agent information.
        if not product_db_name:
            product_db_name = models.Response.infer_product(request.BROWSER)

        # If we have a product and it matches the user agent browser,
        # then we can infer the version and platform from the user
        # agent if they're missing.
        if product_db_name:
            product = models.Product.objects.get(db_name=product_db_name)
            if product.browser and product.browser == request.BROWSER.browser:
                if not version:
                    version = request.BROWSER.browser_version
                if not platform:
                    platform = models.Response.infer_platform(
                        product_db_name, request.BROWSER)

    # Make sure values are at least empty strings--no Nones.
    opinion.product = product_db_name or u''
    opinion.version = version or u''
    opinion.channel = channel or u''
    opinion.platform = platform or u''

    opinion.save()

    # If there was an email address, save that separately.
    if data.get('email_ok') and data.get('email'):
        e = models.ResponseEmail(email=data['email'], opinion=opinion)
        e.save()
        statsd.incr('feedback.emaildata.optin')

    # If there's browser data, save that separately.
    if data.get('browser_ok'):
        # This comes in as a JSON string. Because we're using
        # JSONObjectField, we need to convert it back to Python and
        # then save it. This is kind of silly, but it does guarantee
        # we have valid JSON.
        try:
            browser_data = data['browser_data']
            browser_data = json.loads(browser_data)

        except ValueError:
            # Handles empty string and any non-JSON value.
            statsd.incr('feedback.browserdata.badvalue')

        except KeyError:
            # Handles the case where it's missing from the data
            # dict. If it's missing, we don't want to do anything
            # including metrics.
            pass

        else:
            # If browser_data isn't an empty dict, then save it.
            if browser_data:
                rti = models.ResponsePI(data=browser_data, opinion=opinion)
                rti.save()
                statsd.incr('feedback.browserdata.optin')

    if get_data:
        # There was extra context in the query string, so we grab that
        # with some restrictions and save it separately.
        slop = {}

        # We capture at most the first 20 key/val pairs
        get_data_items = sorted(get_data.items())[:20]

        for key, val in get_data_items:
            # Keys can be at most 20 characters long.
            key = key[:20]
            if len(val) == 1:
                val = val[0]

            # Values can be at most 20 characters long.
            val = val[:100]
            slop[key.encode('utf-8')] = val.encode('utf-8')

        context = models.ResponseContext(data=slop, opinion=opinion)
        context.save()
        statsd.incr('feedback.contextdata.optin')

    if data['happy']:
        statsd.incr('feedback.happy')
    else:
        statsd.incr('feedback.sad')

    request.session['opinion_id'] = opinion.id

    return HttpResponseRedirect(reverse('thanks'))
Ejemplo n.º 7
0
def _handle_feedback_post(request, locale=None, product=None,
                          version=None, channel=None):
    """Saves feedback post to db accounting for throttling

    :arg request: request we're handling the post for
    :arg locale: locale specified in the url
    :arg product: validated and sanitized product slug specified in
        the url
    :arg version: validated and sanitized version specified in the url
    :arg channel: validated and sanitized channel specified in the url

    """
    if getattr(request, 'limited', False):
        # If we're throttled, then return the thanks page, but don't
        # add the response to the db.
        return HttpResponseRedirect(reverse('thanks'))

    # Get the form and run is_valid() so it goes through the
    # validation and cleaning machinery. We don't really care if it's
    # valid, though, since we will take what we got and do the best we
    # can with it. Error validation is now in JS.
    form = ResponseForm(request.POST)
    form.is_valid()

    data = form.cleaned_data
    description = data.get('description', u'').strip()
    if not description:
        # If there's no description, then there's nothing to do here,
        # so thank the user and move on.
        return HttpResponseRedirect(reverse('thanks'))

    # Do some data validation of product, channel and version
    # coming from the url.
    if product:
        # If there was a product in the url, that's a product slug, so
        # we map it to a db_name which is what we want to save to the
        # db.
        product = models.Product.get_product_map()[product]

    # src, then source, then utm_source
    source = request.GET.get('src', u'')
    if not source:
        source = request.GET.get('utm_source', u'')

    campaign = request.GET.get('utm_campaign', u'')

    # If the product came in on the url, then we only want to populate
    # the platfrom from the user agent data iff the product specified
    # by the url is the same as the browser product.
    platform = u''
    if product is None or product == request.BROWSER.browser:
        # Most platforms aren't different enough between versions to care.
        # Windows is.
        platform = request.BROWSER.platform
        if platform == 'Windows':
            platform += ' ' + request.BROWSER.platform_version

    product = product or u''

    opinion = models.Response(
        # Data coming from the user
        happy=data['happy'],
        url=clean_url(data.get('url', u'')),
        description=data['description'].strip(),

        # Inferred data from user agent
        prodchan=_get_prodchan(request, product, channel),
        user_agent=request.META.get('HTTP_USER_AGENT', ''),
        browser=request.BROWSER.browser,
        browser_version=request.BROWSER.browser_version,
        platform=platform,

        # Pulled from the form data or the url
        locale=data.get('locale', locale),

        # Data from mobile devices which is probably only
        # applicable to mobile devices
        manufacturer=data.get('manufacturer', ''),
        device=data.get('device', ''),
    )

    if source:
        opinion.source = source[:100]

    if campaign:
        opinion.campaign = campaign[:100]

    if product:
        # If we picked up the product from the url, we use url
        # bits for everything.
        product = product or u''
        version = version or u''
        channel = channel or u''

    elif opinion.browser != UNKNOWN:
        # If we didn't pick up a product from the url, then we
        # infer as much as we can from the user agent.
        product = data.get(
            'product', models.Response.infer_product(platform))
        version = data.get(
            'version', request.BROWSER.browser_version)
        # Assume everything we don't know about is stable channel.
        channel = u'stable'

    else:
        product = channel = version = u''

    opinion.product = product or u''
    opinion.version = version or u''
    opinion.channel = channel or u''

    opinion.save()

    # If there was an email address, save that separately.
    if data.get('email_ok') and data.get('email'):
        e = models.ResponseEmail(email=data['email'], opinion=opinion)
        e.save()

    if data['happy']:
        statsd.incr('feedback.happy')
    else:
        statsd.incr('feedback.sad')

    return HttpResponseRedirect(reverse('thanks'))
Ejemplo n.º 8
0
def _handle_feedback_post(request, locale=None, product=None,
                          version=None, channel=None):
    """Saves feedback post to db accounting for throttling

    :arg request: request we're handling the post for
    :arg locale: locale specified in the url
    :arg product: None or the Product
    :arg version: validated and sanitized version specified in the url
    :arg channel: validated and sanitized channel specified in the url

    """
    if getattr(request, 'limited', False):
        # If we're throttled, then return the thanks page, but don't
        # add the response to the db.
        return HttpResponseRedirect(reverse('thanks'))

    # Get the form and run is_valid() so it goes through the
    # validation and cleaning machinery. We don't really care if it's
    # valid, though, since we will take what we got and do the best we
    # can with it. Error validation is now in JS.
    form = ResponseForm(request.POST)
    form.is_valid()

    get_data = request.GET.copy()

    data = form.cleaned_data

    description = data.get('description', u'').strip()
    if not description:
        # If there's no description, then there's nothing to do here,
        # so thank the user and move on.
        return HttpResponseRedirect(reverse('thanks'))

    opinion = models.Response(
        # Data coming from the user
        happy=data['happy'],
        url=clean_url(data.get('url', u'')),
        description=description,

        # Pulled from the form data or the url
        locale=data.get('locale', locale),

        # Data from mobile devices which is probably only
        # applicable to mobile devices
        manufacturer=data.get('manufacturer', ''),
        device=data.get('device', ''),
    )

    # Add user_agent and inferred data.
    user_agent = request.META.get('HTTP_USER_AGENT', '')
    if user_agent:
        browser = request.BROWSER

        opinion.user_agent = user_agent
        opinion.browser = browser.browser
        opinion.browser_version = browser.browser_version
        opinion.browser_platform = browser.platform
        if browser.platform == 'Windows':
            opinion.browser_platform += ' ' + browser.platform_version

    # source is src or utm_source
    source = (
        get_data.pop('src', [u''])[0] or
        get_data.pop('utm_source', [u''])[0]
    )
    if source:
        opinion.source = source[:100]

    campaign = get_data.pop('utm_campaign', [u''])[0]
    if campaign:
        opinion.campaign = campaign[:100]

    platform = u''

    # Figure out product, version, channel and platform. We either get
    # them from the url, from the user_agent, or we don't set them at
    # all.
    if product:
        # If there was a product in the url, then we get a Product
        # instance as an argument and we want the db_name from that.
        product = product.db_name

        # FIXME: We should be able to "match" the product with the
        # user agent browser and if they're the same, then set
        # platform == browser_platform. However, it's tricky since
        # our "products" are "interesting".

    elif opinion.browser != UNKNOWN:
        # If we didn't pick up a product from the url, then we
        # infer as much as we can from the user agent.
        product = data.get(
            'product', models.Response.infer_product(opinion.browser_platform))
        version = data.get(
            'version', request.BROWSER.browser_version)
        # Assume everything we don't know about is stable channel.
        channel = u'stable'

    # Try to infer the platform from the product.
    if product and not platform:
        platform = models.Response.infer_platform(product, request.BROWSER)

    opinion.product = product or u''
    opinion.version = version or u''
    opinion.channel = channel or u''
    opinion.platform = platform or u''

    opinion.save()

    # If there was an email address, save that separately.
    if data.get('email_ok') and data.get('email'):
        e = models.ResponseEmail(email=data['email'], opinion=opinion)
        e.save()

    if get_data:
        # There was extra context in the query string, so we grab that
        # with some restrictions and save it separately.
        slop = {}

        # We capture at most the first 20 key/val pairs
        get_data_items = sorted(get_data.items())[:20]

        for key, val in get_data_items:
            # Keys can be at most 20 characters long.
            key = key[:20]
            if len(val) == 1:
                val = val[0]

            # Values can be at most 20 characters long.
            val = val[:100]
            slop[key.encode('utf-8')] = val.encode('utf-8')

        context = models.ResponseContext(data=slop, opinion=opinion)
        context.save()

    if data['happy']:
        statsd.incr('feedback.happy')
    else:
        statsd.incr('feedback.sad')

    return HttpResponseRedirect(reverse('thanks'))