Esempio n. 1
0
    def test_user_has_not_logged_in_last_n_days(self):
        user_query_1_id = user_query_services.save_new_user_query(
            self.submitter_id, has_not_logged_in_for_n_days=6)
        self._run_one_off_job(user_query_1_id)

        query_1 = user_models.UserQueryModel.get(user_query_1_id)

        # List of users who has not logged_in in last 6 days.
        self.assertItemsEqual(query_1.user_ids, [self.user_e_id])

        user_query_2_id = user_query_services.save_new_user_query(
            self.submitter_id, has_not_logged_in_for_n_days=2)
        self._run_one_off_job(user_query_2_id)

        query_2 = user_models.UserQueryModel.get(user_query_2_id)

        # List of users logged_in in last 2 days.
        qualifying_user_ids = [self.user_a_id, self.user_e_id]
        self.assertItemsEqual(query_2.user_ids, qualifying_user_ids)

        # Test for legacy user.
        user_settings = user_services.get_user_settings(self.user_a_id)
        user_services.update_last_logged_in(user_settings, None)

        user_query_3_id = user_query_services.save_new_user_query(
            self.submitter_id, has_not_logged_in_for_n_days=6)
        self._run_one_off_job(user_query_3_id)

        query = user_models.UserQueryModel.get(user_query_3_id)

        # Make sure that legacy user is included in qualified user's list.
        self.assertItemsEqual(query.user_ids, [self.user_a_id, self.user_e_id])
Esempio n. 2
0
    def test_combination_of_query_params(self):
        user_query_1_id = user_query_services.save_new_user_query(
            self.submitter_id, {
                'created_at_least_n_exps': 1,
                'used_logic_proof_interaction': False,
                'created_collection': False
            })
        self._run_one_off_job(user_query_1_id)

        user_query_2_id = user_query_services.save_new_user_query(
            self.submitter_id, {
                'edited_at_least_n_exps': 1,
                'used_logic_proof_interaction': False,
                'created_collection': False
            })
        self._run_one_off_job(user_query_2_id)

        user_query_3_id = user_query_services.save_new_user_query(
            self.submitter_id, {
                'created_at_least_n_exps': 1,
                'edited_at_least_n_exps': 1,
                'used_logic_proof_interaction': False,
                'created_collection': False,
            })
        self._run_one_off_job(user_query_3_id)

        qualifying_user_ids_a = [
            self.user_b_id, self.user_d_id, self.user_e_id
        ]
        qualifying_user_ids_b = ([
            self.user_b_id, self.user_c_id, self.user_d_id, self.user_e_id
        ])
        qualifying_user_ids_combined = ([
            self.user_b_id, self.user_d_id, self.user_e_id
        ])

        query_1 = user_models.UserQueryModel.get(user_query_1_id)
        query_2 = user_models.UserQueryModel.get(user_query_2_id)
        query_combined = user_models.UserQueryModel.get(user_query_3_id)

        self.assertEqual(len(query_1.user_ids), 3)
        self.assertEqual(sorted(query_1.user_ids),
                         sorted(qualifying_user_ids_a))

        self.assertEqual(len(query_2.user_ids), 4)
        self.assertEqual(sorted(query_2.user_ids),
                         sorted(qualifying_user_ids_b))

        self.assertEqual(len(query_combined.user_ids), 3)
        self.assertEqual(sorted(query_combined.user_ids),
                         sorted(qualifying_user_ids_combined))
Esempio n. 3
0
    def test_that_user_unsubscribed_from_emails_is_skipped(self):
        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id, created_at_least_n_exps=1)
        self._run_one_off_job(user_query_id)

        query = user_models.UserQueryModel.get(user_query_id)
        self.assertNotIn(self.user_f_id, query.user_ids)
Esempio n. 4
0
    def test_email_dashboard_data_handler(self):
        self.login(self.SUBMITTER_EMAIL)

        response = self.get_json('/emaildashboarddatahandler',
                                 params={'num_queries_to_fetch': 1})
        self.assertEqual(response['recent_queries'], [])

        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id,
            inactive_in_last_n_days=10,
            created_at_least_n_exps=5,
            has_not_logged_in_for_n_days=30)

        response = self.get_json('/emaildashboarddatahandler',
                                 params={'num_queries_to_fetch': 1})

        self.assertEqual(len(response['recent_queries']), 1)

        recent_query = response['recent_queries'][0]

        self.assertEqual(recent_query['id'], user_query_id)
        self.assertEqual(recent_query['status'],
                         feconf.USER_QUERY_STATUS_PROCESSING)
        self.assertNotIn('submitter_id', recent_query)

        self.logout()
Esempio n. 5
0
    def test_bulk_email_handler_with_invalid_query_id_raises_400(self):
        self.login(self.SUBMITTER_EMAIL, is_super_admin=True)

        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id, self.SAMPLE_QUERY_PARAM)

        job_id = user_query_jobs_one_off.UserQueryOneOffJob.create_new()
        user_query_jobs_one_off.UserQueryOneOffJob.enqueue(
            job_id, additional_job_params={'query_id': user_query_id})

        self.assertEqual(
            self.count_jobs_in_mapreduce_taskqueue(
                taskqueue_services.QUEUE_NAME_ONE_OFF_JOBS), 1)
        # Complete execution of query.
        with self.swap(feconf, 'CAN_SEND_EMAILS', True):
            self.process_and_flush_pending_mapreduce_tasks()

        self.assertEqual(
            self.count_jobs_in_mapreduce_taskqueue(
                taskqueue_services.QUEUE_NAME_ONE_OFF_JOBS), 0)

        csrf_token = self.get_new_csrf_token()

        response = self.post_json('/emaildashboardtestbulkemailhandler/%s' %
                                  'invalid_query_id', {},
                                  csrf_token=csrf_token,
                                  expected_status_int=400)
        self.assertEqual(response['error'], '400 Invalid query id.')
        self.logout()
    def test_email_dashboard_data_handler(self):
        self.login(self.SUBMITTER_EMAIL, is_super_admin=True)

        response = self.get_json(
            '/emaildashboarddatahandler',
            params={'num_queries_to_fetch': 1})
        self.assertEqual(response['recent_queries'], [])

        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id, self.SAMPLE_QUERY_PARAM)

        response = self.get_json(
            '/emaildashboarddatahandler',
            params={'num_queries_to_fetch': 1})

        self.assertEqual(len(response['recent_queries']), 1)

        recent_query = response['recent_queries'][0]

        self.assertEqual(recent_query['id'], user_query_id)
        self.assertEqual(
            recent_query['status'], feconf.USER_QUERY_STATUS_PROCESSING)
        self.assertNotIn('submitter_id', recent_query)

        self.logout()
Esempio n. 7
0
    def test_user_has_edited_fewer_than_n_exps(self):
        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id, edited_fewer_than_n_exps=1)
        self._run_one_off_job(user_query_id)

        query = user_models.UserQueryModel.get(user_query_id)
        self.assertItemsEqual(query.user_ids, [self.user_a_id])
Esempio n. 8
0
    def test_cancel_email_handler_with_invalid_query_id_raises_400(self):
        self.login(self.SUBMITTER_EMAIL)

        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id,
            inactive_in_last_n_days=10,
            created_at_least_n_exps=5,
            has_not_logged_in_for_n_days=30)

        job_id = user_query_jobs_one_off.UserQueryOneOffJob.create_new()
        user_query_jobs_one_off.UserQueryOneOffJob.enqueue(
            job_id, additional_job_params={'query_id': user_query_id})

        self.assertEqual(
            self.count_jobs_in_mapreduce_taskqueue(
                taskqueue_services.QUEUE_NAME_ONE_OFF_JOBS), 1)
        # Complete execution of query.
        with self.swap(feconf, 'CAN_SEND_EMAILS', True):
            self.process_and_flush_pending_mapreduce_tasks()

        self.assertEqual(
            self.count_jobs_in_mapreduce_taskqueue(
                taskqueue_services.QUEUE_NAME_ONE_OFF_JOBS), 0)

        csrf_token = self.get_new_csrf_token()

        response = self.post_json('/emaildashboardcancelresult/%s' %
                                  'invalid_query_id', {},
                                  csrf_token=csrf_token,
                                  expected_status_int=400)
        self.assertEqual(response['error'], '400 Invalid query id.')
        self.logout()
Esempio n. 9
0
    def test_email_dashboard_result_page_with_mismatch_of_query_id_raises_401(
            self):
        self.login(self.SUBMITTER_EMAIL)

        user_query_1_id = user_query_services.save_new_user_query(
            self.submitter_id,
            inactive_in_last_n_days=10,
            created_at_least_n_exps=5,
            has_not_logged_in_for_n_days=30)

        user_query_2_id = user_query_services.save_new_user_query(
            self.new_submitter_id,
            inactive_in_last_n_days=10,
            created_at_least_n_exps=5,
            has_not_logged_in_for_n_days=30)

        job_id_1 = user_query_jobs_one_off.UserQueryOneOffJob.create_new()
        user_query_jobs_one_off.UserQueryOneOffJob.enqueue(
            job_id_1, additional_job_params={'query_id': user_query_1_id})

        job_id_2 = user_query_jobs_one_off.UserQueryOneOffJob.create_new()
        user_query_jobs_one_off.UserQueryOneOffJob.enqueue(
            job_id_2, additional_job_params={'query_id': user_query_2_id})

        self.assertEqual(
            self.count_jobs_in_mapreduce_taskqueue(
                taskqueue_services.QUEUE_NAME_ONE_OFF_JOBS), 2)
        # Complete execution of query.
        with self.swap(feconf, 'CAN_SEND_EMAILS', True):
            self.process_and_flush_pending_mapreduce_tasks()

        self.assertEqual(
            self.count_jobs_in_mapreduce_taskqueue(
                taskqueue_services.QUEUE_NAME_ONE_OFF_JOBS), 0)

        csrf_token = self.get_new_csrf_token()

        # Raises authorization error when passing a query id whose associated
        # query model is not created by the logged in user.
        response = self.post_json('/emaildashboardresult/%s' % user_query_2_id,
                                  {},
                                  csrf_token=csrf_token,
                                  expected_status_int=401)
        self.assertEqual(
            response['error'], '%s is not an authorized user for this query.' %
            (self.submitter_id))
        self.logout()
Esempio n. 10
0
    def test_user_has_created_at_least_n_exps(self):
        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id, created_at_least_n_exps=1)
        self._run_one_off_job(user_query_id)

        query = user_models.UserQueryModel.get(user_query_id)
        self.assertItemsEqual(query.user_ids,
                              [self.user_b_id, self.user_d_id, self.user_e_id])
Esempio n. 11
0
    def test_user_is_inactive_in_last_n_days(self):
        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id, inactive_in_last_n_days=3)
        self._run_one_off_job(user_query_id)

        query = user_models.UserQueryModel.get(user_query_id)

        # List of users who were not active in last 3 days.
        self.assertItemsEqual(query.user_ids, [self.user_e_id])
Esempio n. 12
0
    def test_that_user_without_user_contribution_model_is_skipped(self):
        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id, {
                'used_logic_proof_interaction': True,
                'created_collection': False
            })
        self._run_one_off_job(user_query_id)

        query = user_models.UserQueryModel.get(user_query_id)
        self.assertNotIn(self.user_g_id, query.user_ids)
Esempio n. 13
0
    def test_user_has_used_logic_proof_interaction(self):
        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id, {
                'used_logic_proof_interaction': True,
                'created_collection': False
            })
        self._run_one_off_job(user_query_id)

        query = user_models.UserQueryModel.get(user_query_id)
        self.assertItemsEqual(query.user_ids, [self.user_e_id])
Esempio n. 14
0
    def test_user_has_edited_at_least_n_exps(self):
        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id, {
                'edited_at_least_n_exps': 1
            })
        self._run_one_off_job(user_query_id)

        query = user_models.UserQueryModel.get(user_query_id)
        qualifying_user_ids = [
            self.user_b_id, self.user_c_id, self.user_d_id, self.user_e_id]
        self.assertItemsEqual(query.user_ids, qualifying_user_ids)
Esempio n. 15
0
    def test_that_user_unsubscribed_from_emails_is_skipped(self):
        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id, {
                'created_at_least_n_exps': 1,
                'used_logic_proof_interaction': False,
                'created_collection': False
            })
        self._run_one_off_job(user_query_id)

        query = user_models.UserQueryModel.get(user_query_id)
        self.assertNotIn(self.user_f_id, query.user_ids)
Esempio n. 16
0
    def test_combination_of_query_params(self):
        user_query_1_id = user_query_services.save_new_user_query(
            self.submitter_id, created_at_least_n_exps=1)
        self._run_one_off_job(user_query_1_id)

        user_query_2_id = user_query_services.save_new_user_query(
            self.submitter_id, edited_at_least_n_exps=1)
        self._run_one_off_job(user_query_2_id)

        user_query_3_id = user_query_services.save_new_user_query(
            self.submitter_id,
            created_at_least_n_exps=1,
            edited_at_least_n_exps=1)
        self._run_one_off_job(user_query_3_id)

        qualifying_user_ids_a = [
            self.user_b_id, self.user_d_id, self.user_e_id
        ]
        qualifying_user_ids_b = ([
            self.user_b_id, self.user_c_id, self.user_d_id, self.user_e_id
        ])
        qualifying_user_ids_combined = ([
            self.user_b_id, self.user_d_id, self.user_e_id
        ])

        query_1 = user_models.UserQueryModel.get(user_query_1_id)
        query_2 = user_models.UserQueryModel.get(user_query_2_id)
        query_combined = user_models.UserQueryModel.get(user_query_3_id)

        self.assertEqual(len(query_1.user_ids), 3)
        self.assertEqual(sorted(query_1.user_ids),
                         sorted(qualifying_user_ids_a))

        self.assertEqual(len(query_2.user_ids), 4)
        self.assertEqual(sorted(query_2.user_ids),
                         sorted(qualifying_user_ids_b))

        self.assertEqual(len(query_combined.user_ids), 3)
        self.assertEqual(sorted(query_combined.user_ids),
                         sorted(qualifying_user_ids_combined))
Esempio n. 17
0
    def post(self):
        """Post handler for query."""
        data = self.normalized_payload.get('data')
        kwargs = {key: data[key] for key in data if data[key] is not None}

        user_query_id = user_query_services.save_new_user_query(
            self.user_id, kwargs)

        # Start MR job in background.
        user_query = (user_query_services.get_user_query(user_query_id,
                                                         strict=True))
        data = {'query': _generate_user_query_dicts([user_query])[0]}
        self.render_json(data)
Esempio n. 18
0
    def test_user_has_edited_at_least_n_exps(self):
        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id, {
                'edited_at_least_n_exps': 1,
                'used_logic_proof_interaction': False,
                'created_collection': False
            })
        self._run_one_off_job(user_query_id)

        query = user_models.UserQueryModel.get(user_query_id)
        qualifying_user_ids = [
            self.user_b_id, self.user_c_id, self.user_d_id, self.user_e_id]
        self.assertItemsEqual(query.user_ids, qualifying_user_ids)
Esempio n. 19
0
    def test_that_correct_email_is_sent_upon_completion(self):
        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id, {
                'edited_fewer_than_n_exps': 1,
                'used_logic_proof_interaction': False,
                'created_collection': False
            })

        self._run_one_off_job(user_query_id)
        query = user_models.UserQueryModel.get(user_query_id)
        self.assertEqual(
            query.query_status, feconf.USER_QUERY_STATUS_COMPLETED)

        expected_email_html_body = (
            'Hi submit,<br>'
            'Your query with id %s has succesfully completed its '
            'execution. Visit the result page '
            '<a href="https://www.oppia.org/emaildashboardresult/%s">'
            'here</a> '
            'to see result of your query.<br><br>'
            'Thanks!<br>'
            '<br>'
            'Best wishes,<br>'
            'The Oppia Team<br>'
            '<br>'
            'You can change your email preferences via the '
            '<a href="http://localhost:8181/preferences">Preferences</a> page.'
        ) % (user_query_id, user_query_id)

        expected_email_text_body = (
            'Hi submit,\n'
            'Your query with id %s has succesfully completed its '
            'execution. Visit the result page here '
            'to see result of your query.\n\n'
            'Thanks!\n'
            '\n'
            'Best wishes,\n'
            'The Oppia Team\n'
            '\n'
            'You can change your email preferences via the '
            'Preferences page.'
        ) % user_query_id

        messages = self._get_sent_email_messages(
            self.USER_SUBMITTER_EMAIL)
        self.assertEqual(
            messages[0].html.decode(), expected_email_html_body)
        self.assertEqual(
            messages[0].body.decode(), expected_email_text_body)
Esempio n. 20
0
    def test_user_is_inactive_in_last_n_days(self):
        number_of_days = 3
        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id, {
                'inactive_in_last_n_days': number_of_days
            })
        self._run_one_off_job(user_query_id)

        query = user_models.UserQueryModel.get(user_query_id)

        # user_d has created an exploration 10 days ago but edited an
        # exploration 2 days ago.
        self.assertNotIn(self.user_d_id, query.user_ids)
        # List of users who were not active in last 3 days.
        self.assertItemsEqual(query.user_ids, [self.user_e_id])
Esempio n. 21
0
    def test_that_correct_email_is_sent_upon_failure(self):
        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id, {
                'edited_fewer_than_n_exps': 1,
                'used_logic_proof_interaction': False,
                'created_collection': False
            })

        self._run_one_off_job_resulting_in_failure(user_query_id)
        query = user_models.UserQueryModel.get(user_query_id)

        self.assertEqual(
            query.query_status, feconf.USER_QUERY_STATUS_FAILED)

        expected_email_html_body = (
            'Hi submit,<br>'
            'Your query with id %s has failed due to error '
            'during execution. '
            'Please check the query parameters and submit query again.<br><br>'
            'Thanks!<br>'
            '<br>'
            'Best wishes,<br>'
            'The Oppia Team<br>'
            '<br>'
            'You can change your email preferences via the '
            '<a href="http://localhost:8181/preferences">Preferences</a> page.'
        ) % user_query_id

        expected_email_text_body = (
            'Hi submit,\n'
            'Your query with id %s has failed due to error '
            'during execution. '
            'Please check the query parameters and submit query again.\n\n'
            'Thanks!\n'
            '\n'
            'Best wishes,\n'
            'The Oppia Team\n'
            '\n'
            'You can change your email preferences via the Preferences page.'
        ) % user_query_id

        messages = self._get_sent_email_messages(
            self.USER_SUBMITTER_EMAIL)
        self.assertEqual(
            messages[0].html.decode(), expected_email_html_body)
        self.assertEqual(
            messages[0].body.decode(), expected_email_text_body)
Esempio n. 22
0
    def test_query_status_check_handler(self):
        self.login(self.SUBMITTER_EMAIL, is_super_admin=True)

        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id, self.SAMPLE_QUERY_PARAM)

        query_data = self.get_json('/querystatuscheck',
                                   params={'query_id': user_query_id})['query']

        self.assertEqual(query_data['id'], user_query_id)
        self.assertEqual(query_data['status'],
                         feconf.USER_QUERY_STATUS_PROCESSING)
        self.assertEqual(query_data['submitter_username'],
                         self.SUBMITTER_USERNAME)
        self.assertNotIn('submitter_id', query_data)

        self.logout()
Esempio n. 23
0
    def post(self):
        """Post handler for query."""
        data = self.payload['data']
        kwargs = {key: data[key] for key in data if data[key] is not None}
        self._validate(kwargs)

        user_query_id = user_query_services.save_new_user_query(
            self.user_id, **kwargs)

        # Start MR job in background.
        job_id = user_query_jobs_one_off.UserQueryOneOffJob.create_new()
        params = {'query_id': user_query_id}
        user_query_jobs_one_off.UserQueryOneOffJob.enqueue(
            job_id, additional_job_params=params)

        user_query = (user_query_services.get_user_query(user_query_id,
                                                         strict=True))
        data = {'query': _generate_user_query_dicts([user_query])[0]}
        self.render_json(data)
Esempio n. 24
0
    def test_query_status_check_handler(self):
        self.login(self.SUBMITTER_EMAIL)

        user_query_id = user_query_services.save_new_user_query(
            self.submitter_id,
            inactive_in_last_n_days=10,
            created_at_least_n_exps=5,
            has_not_logged_in_for_n_days=30)

        query_data = self.get_json('/querystatuscheck',
                                   params={'query_id': user_query_id})['query']

        self.assertEqual(query_data['id'], user_query_id)
        self.assertEqual(query_data['status'],
                         feconf.USER_QUERY_STATUS_PROCESSING)
        self.assertEqual(query_data['submitter_username'],
                         self.SUBMITTER_USERNAME)
        self.assertNotIn('submitter_id', query_data)

        self.logout()
Esempio n. 25
0
    def test_save_new_query_model(self):
        query_param = {
            'inactive_in_last_n_days': 10,
            'created_at_least_n_exps': 5,
            'has_not_logged_in_for_n_days': 30
        }
        user_query_id = user_query_services.save_new_user_query(
            self.admin_user_id, query_param)

        query_model = user_models.UserQueryModel.get(user_query_id)

        self.assertEqual(query_model.submitter_id, self.admin_user_id)
        self.assertEqual(query_model.inactive_in_last_n_days,
                         query_param['inactive_in_last_n_days'])
        self.assertEqual(query_model.created_at_least_n_exps,
                         query_param['created_at_least_n_exps'])
        self.assertEqual(query_model.has_not_logged_in_for_n_days,
                         query_param['has_not_logged_in_for_n_days'])
        self.assertIsNone(query_model.created_fewer_than_n_exps)
        self.assertIsNone(query_model.edited_at_least_n_exps)
        self.assertIsNone(query_model.edited_fewer_than_n_exps)
    def test_save_new_query_model(self):
        inactive_in_last_n_days = 10
        created_at_least_n_exps = 5
        has_not_logged_in_for_n_days = 30
        user_query_id = user_query_services.save_new_user_query(
            self.admin_user_id,
            inactive_in_last_n_days=inactive_in_last_n_days,
            created_at_least_n_exps=created_at_least_n_exps,
            has_not_logged_in_for_n_days=has_not_logged_in_for_n_days)

        query_model = user_models.UserQueryModel.get(user_query_id)

        self.assertEqual(query_model.submitter_id, self.admin_user_id)
        self.assertEqual(query_model.inactive_in_last_n_days,
                         inactive_in_last_n_days)
        self.assertEqual(query_model.created_at_least_n_exps,
                         created_at_least_n_exps)
        self.assertEqual(query_model.has_not_logged_in_for_n_days,
                         has_not_logged_in_for_n_days)
        self.assertIsNone(query_model.created_fewer_than_n_exps)
        self.assertIsNone(query_model.edited_at_least_n_exps)
        self.assertIsNone(query_model.edited_fewer_than_n_exps)
Esempio n. 27
0
    def test_save_new_query_model(self) -> None:
        query_param = {
            'inactive_in_last_n_days': 10,
            'created_at_least_n_exps': 5,
            'has_not_logged_in_for_n_days': 30
        }
        user_query_id = user_query_services.save_new_user_query(
            self.admin_user_id, query_param)

        query_model = user_models.UserQueryModel.get(user_query_id)

        # Ruling out the possibility of None for mypy type checking.
        assert query_model is not None
        self.assertEqual(query_model.submitter_id, self.admin_user_id)
        self.assertEqual(query_model.inactive_in_last_n_days,
                         query_param['inactive_in_last_n_days'])
        self.assertEqual(query_model.created_at_least_n_exps,
                         query_param['created_at_least_n_exps'])
        self.assertEqual(query_model.has_not_logged_in_for_n_days,
                         query_param['has_not_logged_in_for_n_days'])
        self.assertIsNone(query_model.created_fewer_than_n_exps)
        self.assertIsNone(query_model.edited_at_least_n_exps)
        self.assertIsNone(query_model.edited_fewer_than_n_exps)