def settings_plan(request): plan_id, format = get_many(request.params, ['plan_id'], optional=['format']) user = api.account.get_user(request) if not user: if format == 'redirect': request.session['plan_id'] = plan_id request.session.save() raise LoginRequired(next=request.route_path('settings')) if plan_id == 'enterprise': api.email.notify_admin( request, 'Enterprise plan inquery: [%s] %s' % (user.id, user.display_name), user.email_to) request.flash( 'The Enterprise plan is only available in limited access at the moment. We will reach out to you when a slot becomes available. You are welcome to choose another plan for now.' ) return old_plan_id = user.plan_id api.account.set_plan(user, plan_id=plan_id) request.flash('Plan updated.') api.email.notify_admin( request, 'Plan changed: [%s] %s' % (user.id, user.display_name), '%s -> %s' % (old_plan_id, user.plan_id))
def settings_payments(request): user = api.account.get_user(request, required=True) stripe_token, plan_id, ga_cid = get_many( request.params, optional=['stripe_token', 'plan_id', 'ga_cid']) payment_type = 'stripe' if user.payment and user.payment.id == 'namecheap': payment_type = 'namecheap' elif not stripe_token and not request.registry.settings.get('testing'): raise APIControllerError('Missing Stripe card token.') metadata = {} if ga_cid: metadata['ga_cid'] = ga_cid subject = 'Payment added: [%s] %s' if user.payment: subject = 'Payment updated: [%s] %s' api.account.set_payments(user, plan_id=plan_id, card_token=stripe_token, metadata=metadata, payment_type=payment_type) api.email.notify_admin(request, subject % (user.id, user.display_name), 'plan_id=%s' % user.plan_id) request.flash('Payment information is set.')
def report_create(request): remote_id, report_type, account_id, pace = get_many(request.params, optional=['remote_id', 'type', 'account_id', 'pace']) user = api.account.get_user(request, required=True, joinedload='accounts') user_id = user.id account = user.get_account(id=account_id) if not account: raise APIControllerError("Account [%s] does not exist for user: %s" % (account_id, user_id)) # Check limits num_sites = user.get_feature('num_sites') if num_sites: remote_ids = set(r.remote_id for r in account.reports) remote_ids.add(remote_id) if len(remote_ids) > num_sites: raise APIControllerError("Maximum number of sites limit reached, please consider upgrading your plan.") if not remote_id: remote_id = account.remote_id api_query = api.account.query_service(request, account=account) profile = api_query.get_profile(remote_id=remote_id) if not profile: raise APIControllerError("Profile does not belong to this account: %s" % remote_id) if profile.get('type') == u'APP': # TODO: Unhardcode this if report_type == 'mobile-week': pass elif report_type == 'week': report_type = 'mobile-week' elif report_type == 'activity-month': report_type = 'mobile-month' else: request.flash("This report type is not supported for mobile apps yet. Making a weekly report instead. Please contact support if you need a new report type.") api.email.notify_admin(request, "Attempted mobile report: %s" % report_type, str(user)) report_type = 'mobile-week' config = {} if pace: config['pace'] = pace report_type = report_type or api_query.oauth.default_report try: report = api.report.create(account_id=account.id, remote_data=profile, subscribe_user_id=user_id, type=report_type, remote_id=remote_id, config=config) except APIError as e: raise APIControllerError(e.message) preferred_hour = user.config.get('preferred_hour') if preferred_hour is not None: api.report.reschedule(report.id, hour=int(preferred_hour)) # Queue new report tasks.report.send.delay(report.id) request.flash("First %s report for %s has been queued. Please check your Spam folder if you don't see it in your Inbox in a few minutes." % (report.type, report.display_name)) return {'report': report}
def settings_feedback(request): user = api.account.get_user(request, required=True) body, subject, extend_trial = get_many( request.params, optional=['body', 'subject', 'extend_trial']) text = "From user_id=%s:\n%s\n%s\n\n===\n\n%s" % ( user.id, user.email_to, " [extend_trial=%d]" % user.num_remaining if extend_trial else "", body) api.email.notify_admin(request, subject, text=text) request.flash('Feedback submitted. We\'ll be in touch shortly!')
def extract_transaction(self, webhook_data, load_customer=True): if webhook_data['type'] != "invoice.payment_succeeded": return # Skip invoice = webhook_data['data']['object'] id, total, currency, metadata, lines, customer_id = get_many( invoice, ['id', 'total', 'currency', 'metadata', 'lines'], ['customer']) # TODO: Verify event user_id = client_id = None if load_customer and customer_id: r = self.get('https://api.stripe.com/v1/customers/%s' % customer_id) user_id = to_ga_key(r.get('metadata'), keys=GA_UID_KEYS) client_id = to_ga_key(r.get('metadata'), keys=GA_CID_KEYS) items = [] for line in (lines or {}).get('data', []): if not line: continue items.append({ 'hit_type': 'item', 'user_id': user_id, 'client_id': client_id, 'ti': id, 'ip': line['amount'] / 100.0, 'iq': line['quantity'], 'in': (line.get('plan') or {}).get('name') or line.get('description') or line['id'], 'cu': line['currency'].upper(), }) transaction = { 'hit_type': 'transaction', 'user_id': user_id, 'client_id': client_id, 'items': items, 'ti': id, 'tr': total / 100.0, 'cu': currency.upper(), } return transaction
def funnel_clear(request): user_id = api.account.get_user_id(request, required=True) account_id, = get_many(request.params, required=['account_id']) account = model.Account.get_by(id=account_id, user_id=user_id) if not account: raise APIControllerError("Invalid account: %s", account_id) account.config.pop('ga_funnels', None) account.config.changed() model.Session.commit() request.flash("Cleared event connections from %s account: %s" % (account.service, account.display_name))
def dry_run(request): api.account.get_admin(request) num_extra, filter_account, days_offset = get_many( request.params, optional=['num_extra', 'filter_account', 'days_offset']) num_extra = int(num_extra or 10) days_offset = int(days_offset or 14) tasks.report.dry_run.delay(num_extra=num_extra, filter_account=filter_account, days_offset=days_offset) request.flash( 'Dry run queued: num_extra=%s filter_account=%s days_offset=%s' % (num_extra, filter_account, days_offset))
def delete(self): # TODO: Move to API? user_id = api.account.get_user_id(self.request) token, is_confirmed, is_feedback, why, retention = get_many( self.request.params, optional=['token', 'confirmed', 'feedback', 'why', 'retention']) if not user_id and not token: raise LoginRequired(next=self.current_path) user = None if token: user = api.account.get(token=token) if not user and user_id: user = model.User.get(user_id) self.c.user = user self.c.token = token reasons = 'Why:\r\n%s\r\n\r\nRetention:\r\n%s' % (why, retention) if not user: return self._render('delete.mako') if is_confirmed: api.email.notify_admin(self.request, 'Account deleted: [%s] "%s" <%s>' % (user.id, user.display_name, user.email), text=reasons) api.account.delete(user_id=user.id) api.account.logout_user(self.request) self.request.session.flash('Good bye.') elif is_feedback: api.email.notify_admin(self.request, 'RETENTION: [%s] "%s" <%s>' % (user.id, user.display_name, user.email), text=reasons) self.request.session.flash( 'Feedback submitted. We\'ll be in touch shortly!') else: self.c.token = token return self._render('delete.mako') return self._redirect(location=self.request.route_path('index'))
def subscription_delete(request): # TODO: Remove report if it's the last subscriber? user_id = api.account.get_user_id(request, required=True) subscription_id, = get_many(request.params, required=['subscription_id']) sub = model.Session.query(model.Subscription).options(orm.joinedload_all('report.account'), orm.joinedload('user')).get(subscription_id) if not sub: raise APIControllerError("Invalid subscription id: %s" % subscription_id) report = sub.report if sub.user_id != user_id and report.account.user_id != user_id: raise APIControllerError("Subscription does not belong to you: %s" % subscription_id) email = sub.user.email sub.delete() model.Session.commit() request.flash("Removed subscriber [%s] to report for [%s]" % (email, report.display_name))
def explore_api(request): u = api.account.get_admin(request) a = u.get_account(service='google') report_id, dimensions, metrics, extra, date_start, date_end = get_many( request.params, ['report_id'], optional=['dimensions', 'metrics', 'extra', 'date_start', 'date_end']) report = model.Report.get_by(account_id=a.id, id=report_id) if not report: raise APIControllerError("Invalid report id: %s" % report_id) cache_keys = ('admin/explore_api', ) google_query = api.account.query_service(request, report.account, cache_keys=cache_keys) date_end = date_end or date.today() date_start = date_start or date_end - timedelta(days=7) params = { 'ids': 'ga:%s' % report.remote_id, 'start-date': date_start, 'end-date': date_end, } if metrics: metrics = [Column(m) for m in metrics.split(',')] if dimensions: dimensions = [Column(m) for m in dimensions.split(',')] if extra: params.update(part.split('=', 1) for part in extra.split('&')) try: r = google_query.get_table(params, metrics=metrics, dimensions=dimensions) except KeyError as e: raise APIControllerError("Invalid metric or dimension: %s" % e.args[0]) return {'table': r}
def index(self): user = api.account.get_user(self.request, required=True, joinedload='accounts') plan_id = self.request.session.pop('plan_id', None) if plan_id: api.account.set_plan(user, plan_id=plan_id) self.request.flash('Plan updated.') self.c.selected_plan = user.plan if user.plan.id != 'trial' else PLAN_DEFAULT self.c.user = user available_services = set(['google', 'stripe']) for a in user.accounts: available_services.discard(a.service) # TODO: Merge with plan level/features? self.c.extra_services = get_many(service_registry, available_services) return self._render('settings.mako')
def settings_branding(request): user = api.account.get_user(request, required=True) header_logo, header_text, from_name, reply_to = get_many( request.params, optional=['header_logo', 'header_text', 'from_name', 'reply_to']) if not user.get_feature('custom_branding'): raise APIControllerError( 'Your plan does not include custom branding. Please upgrade your plan.' ) if reply_to and '@' not in reply_to: raise APIControllerError( '"Reply To" field must be a valid email address.') base_dir = request.features.get('upload_logo') if base_dir and hasattr(header_logo, 'file'): prefix = '%s-' % user.id try: user.config['email_header_image'] = save_logo( fp=header_logo.file, base_dir=base_dir, replace_path=user.config.get('email_header_image'), prefix=prefix, pretend=request.registry.settings.get('testing'), ) except (ValueError, OSError) as e: log.error('settings.branding: Failed to save logo: %r' % e) raise APIControllerError( "Failed to read image '%s'. Please re-save the image as a PNG and try again." % header_logo.filename) user.config['email_intro_text'] = header_text user.config['reply_to'] = reply_to user.config['from_name'] = from_name model.Session.commit() request.flash('Branding updated.')
def disconnect(self): user = api.account.get_user(self.request, required=True, joinedload='accounts') account_id, = get_many(self.request.params, ['account_id']) account = user.get_account(id=account_id) if not account: raise APIControllerError('Invalid account id: %s' % account_id) if len(user.accounts) == 1: raise APIControllerError( 'Cannot disconnect last remaining service while retaining the account.' ) # TODO: Stop doing model stuff display_name = account.display_name model.Session.delete(account) model.Session.commit() self.request.flash('Account disconnected: %s' % display_name) return self._redirect(location='/settings')
def account_login(request): is_force, token, save_redirect, service = get_many( request.params, optional=['force', 'token', 'next', 'service']) service = service or request.matchdict.get('service', 'google') if service == 'stripe': # We don't do native auth with Stripe. if not api.account.get_user_id(request): # Not logged in? Force Google first. save_redirect = request.route_url('account_login', service=service, _query={'next': save_redirect}) service = 'google' else: is_force = True u = api.account.login_user(request, service=service, save_redirect=save_redirect, token=token, is_force=is_force) return {'user': u}
def account_connect(request): # TODO: Move /account/*/connect logic in here too? payload, service_id = get_many(request.params, ["payload", "service"]) service = service_registry[service_id] if service.protocol != 'openidconnect': raise APIControllerError('OpenID Connect not supported for service.') p = parse_qs(payload) id_token, = p['id_token'] decode_options = {} if request.registry.settings.get('testing'): decode_options = { 'verify_exp': False, } try: decoded = jwt.decode(id_token, service.config['sso_client_secret'], audience=service.config['sso_client_id'], options=decode_options) except jwt.DecodeError as e: raise APIControllerError('Failed to verify id token: %s' % e) except (jwt.InvalidTokenError, jwt.InvalidIssuedAtError) as e: raise APIControllerError('Invalid token: %s' % e) remote_id = decoded['sub'] account = model.Session.query(model.Account).filter_by( remote_id=remote_id, service=service_id).first() if not account: # TODO: Create on demand? raise APIControllerError('Account does not exist.') api.account.login_user_id(request, account.user_id) redirect_to = '/reports' return {'redirect': redirect_to, 'decoded': decoded}
def funnel_create(request): user_id = api.account.get_user_id(request, required=True) from_account_id, to_report_id, ga_tracking_id = get_many(request.params, required=['from_account_id'], optional=['to_report_id', 'ga_tracking_id']) from_account = model.Account.get_by(id=from_account_id, user_id=user_id) if not from_account: raise APIControllerError("Invalid account: %s" % from_account_id) to_display_name = ga_tracking_id if not ga_tracking_id: to_report = model.Session.query(model.Report).join(model.Account).filter(model.Report.id==to_report_id, model.Account.user_id==user_id).first() if to_report: ga_tracking_id = to_report.remote_data['webPropertyId'] to_display_name = to_report.display_name if not ga_tracking_id: raise APIControllerError("Failed to load Google Analytics tracking id from report: %s" % to_report_id) ga_funnels = from_account.config.get('ga_funnels') or [] ga_funnels.append(ga_tracking_id) from_account.config['ga_funnels'] = ga_funnels model.Session.commit() request.flash("Attached events from Stripe (%s) to Google Analytics property (%s) for new transactions." % (from_account.display_name, to_display_name))
def subscription_create(request): report_id, email, display_name = get_many(request.params, required=['report_id', 'email'], optional=['display_name']) if '@' not in email: raise APIControllerError('Invalid email address: %s' % email) user = api.account.get_user(request, required=True) report = model.Report.get(report_id) if not report: raise APIControllerError("Invalid report id: %s" % report_id) if report.account.user_id != user.id: raise APIControllerError("Account does not belong to user: %s" % user.id) num_recipients = user.get_feature('num_recipients') if num_recipients and len(report.subscriptions) >= num_recipients: raise APIControllerError("Maximum number of recipients limit reached, please consider upgrading your plan.") new_user = api.report.add_subscriber(report_id, email, display_name, invited_by_user_id=user.id) request.flash("Added subscriber [%s] to report for [%s]" % (new_user.email, report.display_name)) return {'user': new_user}
def send(request, report, since_time=None, pretend=False, session=model.Session): t = time.time() since_time = since_time or now() if not pretend and report.time_next and report.time_next > since_time: log.warning('send too early, skipping for report: %s' % report.id) return owner = report.account.user if not pretend and report.time_expire and report.time_expire < since_time: if owner.num_remaining is None: # Started paying, un-expire. report.time_expire = None else: log.info('Deleting expired report for %s: %s' % (owner, report)) session.delete(report) session.commit() return send_users = report.users if not send_users: log.warning('No recipients, skipping report: %s' % report.id) return try: report_context = fetch(request, report, since_time) except InvalidGrantError: subject = u"Problem with your Briefmetrics" html = api_email.render(request, 'email/error_auth.mako', Context({ 'report': report, })) message = api_email.create_message( request, to_email=owner.email, subject=subject, html=html, ) if not pretend: api_email.send_message(request, message) report.delete() session.commit() log.warning('Invalid token, removed report: %s' % report) return except APIError as e: if not 'User does not have sufficient permissions for this profile' in e.message: raise subject = u"Problem with your Briefmetrics" html = api_email.render(request, 'email/error_permission.mako', Context({ 'report': report, })) message = api_email.create_message( request, to_email=owner.email, subject=subject, html=html, ) if not pretend: api_email.send_message(request, message) report.delete() model.Session.commit() log.warning('Lost permission to profile, removed report: %s' % report) return messages, force_debug = check_trial(request, report, has_data=report_context.data, pretend=pretend) report_context.messages += messages subject = report_context.get_subject() template = report_context.template time_revive = None if not report_context.data: send_users = [report.account.user] template = 'email/error_empty.mako' time_revive = report_context.next_preferred(report_context.date_end + datetime.timedelta( days=30)) log.info('Sending report to [%d] subscribers: %s' % (len(send_users), report)) debug_sample = float(request.registry.settings.get('mail.debug_sample', 1)) debug_bcc = not report.time_next or random.random() < debug_sample if force_debug: debug_bcc = True email_kw = {} from_name, from_email, reply_to, api_mandrill_key, api_mailgun_key = get_many( owner.config, optional=[ 'from_name', 'from_email', 'reply_to', 'api_mandrill_key', 'api_mailgun_key' ]) if from_name: email_kw['from_name'] = from_name if from_email: email_kw['from_email'] = from_email if reply_to and reply_to != from_email: email_kw['reply_to'] = reply_to send_kw = {} if api_mandrill_key: send_kw['settings'] = { 'api.mandrill.key': api_mandrill_key, } email_kw['mailer'] = 'mandrill' if api_mailgun_key: send_kw['settings'] = { 'api.mailgun.key': api_mailgun_key, } email_kw['mailer'] = 'mailgun' for user in report.users: html = api_email.render( request, template, Context({ 'user': user, 'report': report_context, })) message = api_email.create_message(request, to_email=user.email, subject=subject, html=html, debug_bcc=debug_bcc, **email_kw) if pretend: continue api_email.send_message(request, message, **send_kw) model.ReportLog.create_from_report( report, body=html, subject=subject, seconds_elapsed=time.time() - t, time_sent=None if pretend else now(), ) if pretend: session.commit() return if report_context.data: if owner.num_remaining == 1: subject = u"Your Briefmetrics trial is over" html = api_email.render(request, 'email/error_trial_end.mako') message = api_email.create_message( request, to_email=owner.email, subject=subject, html=html, ) log.info('Sending trial expiration: %s' % owner) if not pretend: api_email.send_message(request, message) if owner.num_remaining: owner.num_remaining -= 1 report.time_last = now() report.time_next = time_revive or report_context.next_preferred( report_context.date_end + datetime.timedelta(days=7)) # XXX: Generalize if owner.num_remaining == 0: report.time_expire = report.time_next session.commit()
def _namecheap_subscription_create(request, data): nc_api = service_registry['namecheap'].instance event_id = data['event']['id'] return_uri = data['event'].get('returnURI') subscription_id = data['event']['subscription_id'] try: email, first_name, last_name, remote_id = get_many(data['event']['user'], ['email', 'first_name', 'last_name', 'username']) except KeyError as e: k = e.args[0] log.warning("namecheap webhook: Failing due to missing field '%s': %s" % (k, event_id)) return { 'type': 'subscription_create_resp', 'id': event_id, 'response': { 'state': 'failed', 'provider_id': '', 'message': 'missing field: %s' % k, } } display_name = ' '.join([first_name, last_name]) plan_id = data['event']['order'].get('pricing_plan_sku') user = api.account.get_or_create( email=email, service='namecheap', display_name=display_name, remote_id=remote_id, remote_data=data['event']['user'], plan_id=plan_id, ) if user.num_remaining != 10: log.warning('Resetting num_remaining from %s: %s' % (user.num_remaining, user)) user.num_remaining = 10 ack_message = 'Briefmetrics activation instructions sent to %s' % email ack_state = 'Active' if user.payment: ack_message = 'Failed to provision new Briefmetrics account for {email}. Account already exists with payment information.'.format(email=email) ack_state = 'Failed' log.info('namecheap webhook: Provision skipped %s' % user) else: user.set_payment('namecheap', subscription_id) if user.payment.auto_charge: user.time_next_payment = now() + user.payment.auto_charge model.Session.commit() log.info('namecheap webhook: Provisioned %s' % user) ack = { 'type': 'subscription_create_resp', 'id': event_id, 'response': { 'state': ack_state, 'provider_id': user.id, 'message': ack_message, } } if return_uri: # Confirm event, activate subscription r = nc_api.session.request('PUT', return_uri, json=ack) # Bypass our wrapper assert_response(r) if ack_state != 'Active': return ack subject = u"Welcome to Briefmetrics" html = api.email.render(request, 'email/welcome_namecheap.mako') message = api.email.create_message(request, to_email=email, subject=subject, html=html, ) api.email.send_message(request, message) return ack
def view(self): user = api.account.get_user(self.request, required=True, joinedload='accounts') report_id = self.request.matchdict['id'] q = model.Session.query(model.Report).filter_by(id=report_id) if not user.is_admin: q = q.join(model.Report.account).filter_by(user_id=user.id) report = q.first() if not report: raise httpexceptions.HTTPNotFound() config_options = ['pace', 'intro', 'ads', 'bcc'] config = dict((k, v) for k, v in self.request.params.items() if k in config_options and v) # Last Sunday since_time = now() report_context = api.report.fetch(self.request, report, since_time, config=config) template = report_context.template if not report_context.data: template = 'email/error_empty.mako' html = api.email.render(self.request, template, Context({ 'report': report_context, 'user': user, })) is_send = self.request.params.get('send') if is_send: owner = report_context.owner email_kw = {} debug_bcc = config.get('bcc') from_name, from_email, reply_to, api_mandrill_key = get_many(owner.config, optional=['from_name', 'from_email', 'reply_to', 'api_mandrill_key']) if from_name: email_kw['from_name'] = from_name if from_email: email_kw['from_email'] = from_email if reply_to and reply_to != from_email: email_kw['reply_to'] = reply_to if debug_bcc: email_kw['debug_bcc'] = debug_bcc send_kw = {} if api_mandrill_key: send_kw['settings'] = { 'api.mandrill.key': api_mandrill_key, } to_email = user.email message = api.email.create_message(self.request, to_email=to_email, subject=report_context.get_subject(), html=html, **email_kw ) api.email.send_message(self.request, message, **send_kw) self.request.session.flash('Sent report for [%s] to: %s' % (report.display_name, to_email)) return self._redirect(self.request.route_path('reports')) return Response(html)