def fxa_concert_rsvp(email, isFx): data = { 'email': email, 'is_firefox': 'Y' if isFx else 'N', 'campaign_id': 'firefox-concert-series-Q4-2018', # or something. just here in case there's another of these some day. 'api_key': settings.BASKET_API_KEY, } try: basket.request('post', 'fxa-concerts-rsvp', data=data) return True except: return False
def form_valid(self, form): try: basket.request('post', 'get-involved', self.get_basket_data(form)) except basket.BasketException as e: if e.code == basket.errors.BASKET_INVALID_EMAIL: msg = _(u'Whoops! Be sure to enter a valid email address.') field = 'email' else: msg = _(u'We apologize, but an error occurred in our system. ' u'Please try again later.') field = '__all__' form.errors[field] = form.error_class([msg]) return self.form_invalid(form) return super(ContributeTasksSurvey, self).form_valid(form)
def test_request_timeout(self): """ If requests times out, it's converted to a BasketException """ input_data = {'newsletters': ['one', 'two'], 'thing': 1} expected_input_data = input_data.copy() newsletters = ','.join(input_data['newsletters']) expected_input_data['newsletters'] = newsletters action, method, token = "ACTION", "METHOD", "TOKEN" with patch('basket.base.requests.request', autospec=True) \ as request_call: request_call.side_effect = Timeout with self.assertRaises(BasketException): request(method, action, data=input_data, token=token, params="PARAMS")
def test_request(self): """ request() calls requests.request() with the expected parms if everything is normal, and returns the expected result. """ response_data = {u'status': u'ok', u'foo': u'bar'} action, method, token = "ACTION", "METHOD", "TOKEN" url = basket_url(action, token) with patch('basket.base.requests.request', autospec=True) \ as request_call: request_call.return_value = Mock(status_code=200, content=json.dumps(response_data), content_type='application/json') result = request(method, action, data="DATA", token=token, params="PARAMS") request_call.assert_called_with(method, url, data="DATA", params="PARAMS", headers=None, timeout=ANY) self.assertEqual(response_data, result)
def test_request_newsletters_non_string(self): """ If request is passed newsletters as a non-string, newsletters is converted to a comma-separated string """ response_data = {u'status': u'ok', u'foo': u'bar'} input_data = {'newsletters': ['one', 'two'], 'thing': 1} expected_input_data = input_data.copy() newsletters = ','.join(input_data['newsletters']) expected_input_data['newsletters'] = newsletters action, method, token = "ACTION", "METHOD", "TOKEN" url = basket_url(action, token) with patch('basket.base.requests.request', autospec=True) \ as request_call: request_call.return_value = Mock(status_code=200, content=json.dumps(response_data), content_type='application/json') result = request(method, action, data=input_data, token=token, params="PARAMS") request_call.assert_called_with(method, url, data=expected_input_data, params="PARAMS", headers=None, timeout=ANY) self.assertEqual(response_data, result)
def test_request_newsletters_string(self): """ If request is passed newsletters as a string, newsletters is passed along unaltered. """ input_data = {'newsletters': 'one,two'} action, method, token = "ACTION", "METHOD", "TOKEN" url = basket_url(action, token) content = {'one': 100, 'two': 200} with patch('basket.base.requests.request', autospec=True) \ as request_call: request_call.return_value = Mock(status_code=200, content=json.dumps(content), content_type='application/json') result = request(method, action, data=input_data, token=token, params="PARAMS") request_call.assert_called_with(method, url, data=input_data, params="PARAMS", headers=None, timeout=ANY) self.assertEqual(content, result)
def custom_unsub_reason(token, reason): """Call basket. Pass along their reason for unsubscribing. This is calling a basket API that's custom to Mozilla, that's why there's not a helper in the basket-client package.""" data = { 'token': token, 'reason': reason, } return basket.request('post', 'custom_unsub_reason', data=data)
def set_country(request, token): """Allow a user to set their country""" initial = {} countrycode = get_geo_from_request(request) if countrycode: initial['country'] = countrycode.lower() form = CountrySelectForm('en-US', data=request.POST or None, initial=initial) if form.is_valid(): try: basket.request('post', 'user-meta', data=form.cleaned_data, token=token) except basket.BasketException: log.exception("Error updating user's country in basket") messages.add_message( request, messages.ERROR, general_error ) else: return redirect(reverse('newsletter.country_success')) return l10n_utils.render(request, 'newsletter/country.html', {'form': form})
def update_basket_task(instance_id): """Update Basket Task. This task subscribes a user to Basket, if not already subscribed and then updates his data on the Phonebook DataExtension. The task retries on failure at most BASKET_TASK_MAX_RETRIES times and if it finally doesn't complete successfully, it emails the settings.BASKET_MANAGERS with details. """ # This task is triggered by a post-save signal on UserProfile, so # we can't save() on UserProfile again while in here - if we were # running with CELERY_EAGER, we'd enter an infinite recursion until # Python died. from models import UserProfile instance = UserProfile.objects.get(pk=instance_id) if not BASKET_ENABLED or not instance.is_vouched: return email = instance.user.email token = instance.basket_token if not token: # no token yet, they're probably not subscribed, so subscribe them. # pass sync='Y' so we wait for it to complete and get back the token. try: retval = basket.subscribe( email, [settings.BASKET_NEWSLETTER], sync='Y', trigger_welcome='N' ) except (requests.exceptions.RequestException, basket.BasketException) as exception: try: update_basket_task.retry() except (MaxRetriesExceededError, basket.BasketException): _email_basket_managers('subscribe', instance.user.email, exception.message) return # Remember the token instance.basket_token = retval['token'] token = retval['token'] # Don't call .save() on a userprofile from here, it would invoke us again # via the post-save signal, which would be pointless. UserProfile.objects.filter(pk=instance.pk).update(basket_token=token) else: # They were already subscribed. See what email address they # have in exact target. If it has changed, we'll need to # unsubscribe the old address and subscribe the new one, # and save the new token. # This'll also return their subscriptions, so we can transfer them # to the new address if we need to. try: result = basket.lookup_user(token=token) except basket.BasketException as exception: try: update_basket_task.retry() except (MaxRetriesExceededError, basket.BasketException): msg = exception.message _email_basket_managers('update_phonebook', token, msg) return old_email = result['email'] if old_email != email: try: # We do the new subscribe first, then the unsubscribe, so we don't # risk losing their subscriptions if the subscribe fails. # Subscribe to all the same newsletters. # Pass sync='Y' so we get back the new token right away subscribe_result = basket.subscribe( email, [settings.BASKET_NEWSLETTER], sync='Y', trigger_welcome='N', ) # unsub all from the old token basket.unsubscribe(token=token, email=old_email, newsletters=[settings.BASKET_NEWSLETTER], optout='Y') except (requests.exceptions.RequestException, basket.BasketException) as exception: try: update_basket_task.retry() except (MaxRetriesExceededError, basket.BasketException): _email_basket_managers('subscribe', email, exception.message) return # FIXME: We should also remove their previous phonebook record from Exact Target, but # basket doesn't have a custom API to do that. (basket never deletes anything.) # That was all successful. Update the token. instance.basket_token = subscribe_result['token'] token = subscribe_result['token'] # Don't call .save() on a userprofile from here, it would invoke us again # via the post-save signal, which would be pointless. UserProfile.objects.filter(pk=instance.pk).update(basket_token=token) GroupMembership = get_model('groups', 'GroupMembership') Group = get_model('groups', 'Group') data = {} # What groups is the user in? user_group_pks = (instance.groups.filter(groupmembership__status=GroupMembership.MEMBER) .values_list('pk', flat=True)) for group in Group.objects.filter(functional_area=True): name = group.name.upper().replace(' ', '_') data[name] = 'Y' if group.id in user_group_pks else 'N' # User location if known if instance.geo_country: data['country'] = instance.geo_country.code if instance.geo_city: data['city'] = instance.geo_city.name # We have a token, proceed with the update try: basket.request('post', 'custom_update_phonebook', token=token, data=data) except (requests.exceptions.RequestException, basket.BasketException) as exception: try: update_basket_task.retry() except (MaxRetriesExceededError, basket.BasketException): _email_basket_managers('update_phonebook', email, exception.message)
def existing(request, token=None): """Manage subscriptions. If token is provided, user can manage their existing subscriptions, to subscribe, unsubscribe, change email or language preferences, etc. If no token is provided, user can fill in their email and language preferences and sign up for newsletters. @param HTTPRequest request: Django request object @param string token: A UUID that identifies this user to the backend. It's sent to users in each newsletter as part of a link to this page, so they can manage their subscriptions without needing an account somewhere with userid & password. """ locale = l10n_utils.get_locale(request) if not token: return redirect(reverse('newsletter.recovery')) if not UUID_REGEX.match(token): # Bad token messages.add_message(request, messages.ERROR, bad_token) # Redirect to the recovery page return redirect(reverse('newsletter.recovery')) if waffle.switch('newsletter-maintenance-mode'): return l10n_utils.render(request, 'newsletter/existing.html') unsub_parm = None # Example user: # # {u'lang': u'en', # u'format': u'H', # u'country': u'us', # u'newsletters': [u'firefox-tips', u'mobile'], # u'created-date': u'1/30/2013 12:46:05 PM', # u'token': u'some-uuid', # u'email': u'*****@*****.**' # } has_fxa = 'fxa' in request.GET user = None if token: try: # ask for fxa status if not passed in the URL params = None if has_fxa else {'fxa': 1} user = basket.request('get', 'user', token=token, params=params) except basket.BasketNetworkException: # Something wrong with basket backend, no point in continuing, # we'd probably fail to subscribe them anyway. log.exception("Basket timeout") messages.add_message(request, messages.ERROR, general_error) return l10n_utils.render(request, 'newsletter/existing.html') except basket.BasketException as e: log.exception("FAILED to get user from token (%s)", e.desc) if not user: # Bad or no token messages.add_message(request, messages.ERROR, bad_token) # Redirect to the recovery page return redirect(reverse('newsletter.recovery')) # if `has_fxa` not returned from basket, set it from the URL user.setdefault('has_fxa', has_fxa) # Get the newsletter data - it's a dictionary of dictionaries newsletter_data = utils.get_newsletters() # Figure out which newsletters to display, and whether to show them # as already subscribed. initial = [] for newsletter, data in newsletter_data.items(): # Only show a newsletter if it has ['active'] == True and # ['show'] == True or the user is already subscribed if not data.get('active', False): continue if (data.get('show', False) or newsletter in user['newsletters'] or (user['has_fxa'] and newsletter in settings.FXA_NEWSLETTERS and any( locale.startswith(l) for l in settings.FXA_NEWSLETTERS_LOCALES))): langs = data['languages'] nstrings = NEWSLETTER_STRINGS.get(newsletter) if nstrings: if newsletter == 'firefox-accounts-journey' and locale.startswith( 'en'): # alternate english title title = u'Firefox Account Tips' else: title = nstrings['title'] description = nstrings.get('description', u'') else: # Firefox Marketplace for Desktop/Android/Firefox OS should be # shorten in the titles title = _(data['title'].replace('Firefox Marketplace for ', '')) description = _(data['description']) form_data = { 'title': Markup(title), 'subscribed_radio': newsletter in user['newsletters'], 'subscribed_check': newsletter in user['newsletters'], 'newsletter': newsletter, 'description': Markup(description), 'english_only': len(langs) == 1 and langs[0].startswith('en'), 'indented': data.get('indent', False), } if 'order' in data: form_data['order'] = data['order'] initial.append(form_data) # Sort by 'order' field if we were given it; otherwise, by title if initial: keyfield = 'order' if 'order' in initial[0] else 'title' initial.sort(key=itemgetter(keyfield)) NewsletterFormSet = formset_factory(NewsletterForm, extra=0, max_num=len(initial)) if request.method == 'POST': form_kwargs = {} # Temporary form so we can see if they checked 'remove_all'. If # they did, no point in validating the newsletters formset and it would # look dumb to complain about it. form = ManageSubscriptionsForm(locale, data=request.POST, initial=user) remove_all = form.is_valid() and form.cleaned_data['remove_all'] formset_is_valid = False if remove_all: # We don't care about the newsletter formset formset_is_valid = True # Make an initialized one in case we fall through to the bottom formset = NewsletterFormSet(initial=initial) else: # We do need to validate the newsletter formset formset = NewsletterFormSet(request.POST, initial=initial) # Set `newsletters` to the list of newsletters they want. # After this, we don't need the formset anymore. newsletters = None if formset.is_valid(): formset_is_valid = True # What newsletters do they say they want to be subscribed to? newsletters = set([ subform.cleaned_data['newsletter'] for subform in formset if (subform.cleaned_data['subscribed_radio'] or subform.cleaned_data['subscribed_check']) ]) form_kwargs['newsletters'] = newsletters form = ManageSubscriptionsForm(locale, data=request.POST, initial=user, **form_kwargs) if formset_is_valid and form.is_valid(): data = form.cleaned_data # Update their format and locale information, if it has changed. # Also pass their updated list of newsletters they want to be # subscribed to, for basket to implement. kwargs = {} if settings.BASKET_API_KEY: kwargs['api_key'] = settings.BASKET_API_KEY for k in ['lang', 'format', 'country']: if user[k] != data[k]: kwargs[k] = data[k] if not remove_all: kwargs['newsletters'] = ",".join(newsletters) if kwargs: # always send lang so basket doesn't try to guess kwargs['lang'] = data['lang'] try: basket.update_user(token, **kwargs) except basket.BasketException: log.exception("Error updating user in basket") messages.add_message(request, messages.ERROR, general_error) return l10n_utils.render(request, 'newsletter/existing.html') # If they chose to remove all, tell basket that they've opted out if remove_all: try: basket.unsubscribe(token, user['email'], optout=True) except (basket.BasketException, requests.Timeout): log.exception("Error updating subscriptions in basket") messages.add_message(request, messages.ERROR, general_error) return l10n_utils.render(request, 'newsletter/existing.html') # We need to pass their token to the next view url = reverse('newsletter.updated') \ + "?unsub=%s&token=%s" % (UNSUB_UNSUBSCRIBED_ALL, token) return redirect(url) # We're going to redirect, so the only way to tell the next # view that we should display the welcome message in the # template is to modify the URL url = reverse('newsletter.updated') if unsub_parm: url += "?unsub=%s" % unsub_parm return redirect(url) # FALL THROUGH so page displays errors else: form = ManageSubscriptionsForm(locale, initial=user) formset = NewsletterFormSet(initial=initial) # For the template, we want a dictionary whose keys are language codes # and each value is the list of newsletter keys that are available in # that language code. newsletter_languages = defaultdict(list) for newsletter, data in newsletter_data.items(): for lang in data['languages']: newsletter_languages[lang].append(newsletter) newsletter_languages = mark_safe(json.dumps(newsletter_languages)) # We also want a list of the newsletters the user is already subscribed to already_subscribed = mark_safe(json.dumps(user['newsletters'])) context = { 'did_confirm': request.GET.get('confirm', None) == '1', 'form': form, 'formset': formset, 'newsletter_languages': newsletter_languages, 'newsletters_subscribed': already_subscribed, 'email': user['email'], } return l10n_utils.render(request, 'newsletter/existing.html', context)
def send_to_device_ajax(request): locale = l10n_utils.get_locale(request) phone_or_email = request.POST.get('phone-or-email') # ensure a value was entered in phone or email field if not phone_or_email: return JsonResponse({'success': False, 'errors': ['phone-or-email']}) # pull message set from POST (not part of form, so wont be in cleaned_data) message_set = request.POST.get('message-set', 'default') # begin collecting data to pass to form constructor data = {'platform': request.POST.get('platform')} # determine if email or phone number was submitted data_type = 'email' if '@' in phone_or_email else 'number' # populate data type in form data dict data[data_type] = phone_or_email # instantiate the form with processed POST data form = SendToDeviceWidgetForm(data) if form.is_valid(): phone_or_email = form.cleaned_data.get(data_type) platform = form.cleaned_data.get('platform') # if no platform specified, default to 'all' if not platform: platform = 'all' # ensure we have a valid message set. if not, fall back to default if message_set not in SEND_TO_DEVICE_MESSAGE_SETS: MESSAGES = SEND_TO_DEVICE_MESSAGE_SETS['default'] else: MESSAGES = SEND_TO_DEVICE_MESSAGE_SETS[message_set] if data_type == 'number': # for testing purposes return success if phone_or_email == '5555555555': return JsonResponse({'success': True}) if platform in MESSAGES['sms']: data = { 'mobile_number': phone_or_email, 'msg_name': MESSAGES['sms'][platform], 'lang': locale, } country = request.POST.get('country') if country and re.match(r'^[a-z]{2}$', country, flags=re.I): data['country'] = country try: basket.request('post', 'subscribe_sms', data=data) except basket.BasketException as e: if e.desc == 'mobile_number is invalid': return JsonResponse({ 'success': False, 'errors': ['number'] }) else: return JsonResponse( { 'success': False, 'errors': ['system'] }, status=400) else: return JsonResponse({'success': False, 'errors': ['platform']}) else: # email if platform in MESSAGES['email']: try: basket.subscribe( phone_or_email, MESSAGES['email'][platform], source_url=request.POST.get('source-url'), lang=locale, ) except basket.BasketException: return JsonResponse( { 'success': False, 'errors': ['system'] }, status=400) else: return JsonResponse({'success': False, 'errors': ['platform']}) resp_data = {'success': True} else: resp_data = {'success': False, 'errors': list(form.errors)} return JsonResponse(resp_data)
def send_to_device_ajax(request): locale = l10n_utils.get_locale(request) phone_or_email = request.POST.get('phone-or-email') # ensure a value was entered in phone or email field if not phone_or_email: return HttpResponseJSON({'success': False, 'errors': ['phone-or-email']}) # pull message set from POST (not part of form, so wont be in cleaned_data) message_set = request.POST.get('message-set', 'default') # begin collecting data to pass to form constructor data = { 'platform': request.POST.get('platform'), } # determine if email or phone number was submitted data_type = 'email' if '@' in phone_or_email else 'number' # populate data type in form data dict data[data_type] = phone_or_email # instantiate the form with processed POST data form = SendToDeviceWidgetForm(data) if form.is_valid(): phone_or_email = form.cleaned_data.get(data_type) platform = form.cleaned_data.get('platform') # if no platform specified, default to 'all' if not platform: platform = 'all' # ensure we have a valid message set. if not, fall back to default if message_set not in SEND_TO_DEVICE_MESSAGE_SETS: MESSAGES = SEND_TO_DEVICE_MESSAGE_SETS['default'] else: MESSAGES = SEND_TO_DEVICE_MESSAGE_SETS[message_set] if data_type == 'number': if platform in MESSAGES['sms']: data = { 'mobile_number': phone_or_email, 'msg_name': MESSAGES['sms'][platform], 'lang': locale, } country = request.POST.get('country') if country and re.match(r'^[a-z]{2}$', country, flags=re.I): data['country'] = country try: basket.request('post', 'subscribe_sms', data=data) except basket.BasketException as e: if e.desc == 'mobile_number is invalid': return HttpResponseJSON({'success': False, 'errors': ['number']}) else: return HttpResponseJSON({'success': False, 'errors': ['system']}, status=400) else: return HttpResponseJSON({'success': False, 'errors': ['platform']}) else: # email if platform in MESSAGES['email']: try: basket.subscribe(phone_or_email, MESSAGES['email'][platform], source_url=request.POST.get('source-url'), lang=locale) except basket.BasketException: return HttpResponseJSON({'success': False, 'errors': ['system']}, status=400) else: return HttpResponseJSON({'success': False, 'errors': ['platform']}) resp_data = {'success': True} else: resp_data = { 'success': False, 'errors': form.errors.keys(), } return HttpResponseJSON(resp_data)
def update_basket_task(instance_id): """Update Basket Task. This task subscribes a user to Basket, if not already subscribed and then updates his data on the Phonebook DataExtension. The task retries on failure at most BASKET_TASK_MAX_RETRIES times and if it finally doesn't complete successfully, it emails the settings.BASKET_MANAGERS with details. """ # This task is triggered by a post-save signal on UserProfile, so # we can't save() on UserProfile again while in here - if we were # running with CELERY_EAGER, we'd enter an infinite recursion until # Python died. from models import UserProfile instance = UserProfile.objects.get(pk=instance_id) if not BASKET_ENABLED or not instance.is_vouched: return email = instance.user.email token = instance.basket_token if not token: # no token yet, they're probably not subscribed, so subscribe them. # pass sync='Y' so we wait for it to complete and get back the token. try: retval = basket.subscribe(email, [settings.BASKET_NEWSLETTER], sync='Y', trigger_welcome='N') except (requests.exceptions.RequestException, basket.BasketException) as exception: try: update_basket_task.retry() except (MaxRetriesExceededError, basket.BasketException): _email_basket_managers('subscribe', instance.user.email, exception.message) return # Remember the token instance.basket_token = retval['token'] token = retval['token'] # Don't call .save() on a userprofile from here, it would invoke us again # via the post-save signal, which would be pointless. UserProfile.objects.filter(pk=instance.pk).update(basket_token=token) else: # They were already subscribed. See what email address they # have in exact target. If it has changed, we'll need to # unsubscribe the old address and subscribe the new one, # and save the new token. # This'll also return their subscriptions, so we can transfer them # to the new address if we need to. try: result = basket.lookup_user(token=token) except basket.BasketException as exception: try: update_basket_task.retry() except (MaxRetriesExceededError, basket.BasketException): msg = exception.message _email_basket_managers('update_phonebook', token, msg) return old_email = result['email'] if old_email != email: try: # We do the new subscribe first, then the unsubscribe, so we don't # risk losing their subscriptions if the subscribe fails. # Subscribe to all the same newsletters. # Pass sync='Y' so we get back the new token right away subscribe_result = basket.subscribe( email, [settings.BASKET_NEWSLETTER], sync='Y', trigger_welcome='N', ) # unsub all from the old token basket.unsubscribe(token=token, email=old_email, newsletters=[settings.BASKET_NEWSLETTER], optout='Y') except (requests.exceptions.RequestException, basket.BasketException) as exception: try: update_basket_task.retry() except (MaxRetriesExceededError, basket.BasketException): _email_basket_managers('subscribe', email, exception.message) return # FIXME: We should also remove their previous phonebook record from Exact Target, but # basket doesn't have a custom API to do that. (basket never deletes anything.) # That was all successful. Update the token. instance.basket_token = subscribe_result['token'] token = subscribe_result['token'] # Don't call .save() on a userprofile from here, it would invoke us again # via the post-save signal, which would be pointless. UserProfile.objects.filter(pk=instance.pk).update( basket_token=token) GroupMembership = get_model('groups', 'GroupMembership') Group = get_model('groups', 'Group') data = {} # What groups is the user in? user_group_pks = (instance.groups.filter( groupmembership__status=GroupMembership.MEMBER).values_list('pk', flat=True)) for group in Group.objects.filter(functional_area=True): name = group.name.upper().replace(' ', '_') data[name] = 'Y' if group.id in user_group_pks else 'N' # User location if known if instance.geo_country: data['country'] = instance.geo_country.code if instance.geo_city: data['city'] = instance.geo_city.name # We have a token, proceed with the update try: basket.request('post', 'custom_update_phonebook', token=token, data=data) except (requests.exceptions.RequestException, basket.BasketException) as exception: try: update_basket_task.retry() except (MaxRetriesExceededError, basket.BasketException): _email_basket_managers('update_phonebook', email, exception.message)