diff options
Diffstat (limited to 'tests/models/spectrum')
| -rw-r--r-- | tests/models/spectrum/__init__.py | 0 | ||||
| -rw-r--r-- | tests/models/spectrum/test_question.py | 216 | ||||
| -rw-r--r-- | tests/models/spectrum/test_survey.py | 413 | ||||
| -rw-r--r-- | tests/models/spectrum/test_survey_manager.py | 130 |
4 files changed, 759 insertions, 0 deletions
diff --git a/tests/models/spectrum/__init__.py b/tests/models/spectrum/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/models/spectrum/__init__.py diff --git a/tests/models/spectrum/test_question.py b/tests/models/spectrum/test_question.py new file mode 100644 index 0000000..ba118d7 --- /dev/null +++ b/tests/models/spectrum/test_question.py @@ -0,0 +1,216 @@ +from datetime import datetime, timezone + +from generalresearch.models import Source +from generalresearch.models.spectrum.question import ( + SpectrumQuestionOption, + SpectrumQuestion, + SpectrumQuestionType, + SpectrumQuestionClass, +) +from generalresearch.models.thl.profiling.upk_question import ( + UpkQuestion, + UpkQuestionSelectorMC, + UpkQuestionType, + UpkQuestionChoice, +) + + +class TestSpectrumQuestion: + + def test_parse_from_api_1(self): + + example_1 = { + "qualification_code": 213, + "text": "My household earns approximately $%%213%% per year", + "cat": None, + "desc": "Income", + "type": 5, + "class": 1, + "condition_codes": [], + "format": {"min": 0, "max": 999999, "regex": "/^([0-9]{1,6})$/i"}, + "crtd_on": 1502869927688, + "mod_on": 1706557247467, + } + q = SpectrumQuestion.from_api(example_1, "us", "eng") + + expected_q = SpectrumQuestion( + question_id="213", + country_iso="us", + language_iso="eng", + question_name="Income", + question_text="My household earns approximately $___ per year", + question_type=SpectrumQuestionType.TEXT_ENTRY, + tags=None, + options=None, + class_num=SpectrumQuestionClass.CORE, + created=datetime(2017, 8, 16, 7, 52, 7, 688000, tzinfo=timezone.utc), + is_live=True, + source=Source.SPECTRUM, + category_id=None, + ) + assert "My household earns approximately $___ per year" == q.question_text + assert "213" == q.question_id + assert expected_q == q + q.to_upk_question() + assert "s:213" == q.external_id + + def test_parse_from_api_2(self): + + example_2 = { + "qualification_code": 211, + "text": "I'm a %%211%%", + "cat": None, + "desc": "Gender", + "type": 1, + "class": 1, + "condition_codes": [ + {"id": "111", "text": "Male"}, + {"id": "112", "text": "Female"}, + ], + "format": {"min": None, "max": None, "regex": ""}, + "crtd_on": 1502869927688, + "mod_on": 1706557249817, + } + q = SpectrumQuestion.from_api(example_2, "us", "eng") + expected_q = SpectrumQuestion( + question_id="211", + country_iso="us", + language_iso="eng", + question_name="Gender", + question_text="I'm a", + question_type=SpectrumQuestionType.SINGLE_SELECT, + tags=None, + options=[ + SpectrumQuestionOption(id="111", text="Male", order=0), + SpectrumQuestionOption(id="112", text="Female", order=1), + ], + class_num=SpectrumQuestionClass.CORE, + created=datetime(2017, 8, 16, 7, 52, 7, 688000, tzinfo=timezone.utc), + is_live=True, + source=Source.SPECTRUM, + category_id=None, + ) + assert expected_q == q + q.to_upk_question() + + def test_parse_from_api_3(self): + + example_3 = { + "qualification_code": 220, + "text": "My child is a %%230%% %%221%% old %%220%%", + "cat": None, + "desc": "Child Dependent", + "type": 6, + "class": 4, + "condition_codes": [ + {"id": "111", "text": "Boy"}, + {"id": "112", "text": "Girl"}, + ], + "format": {"min": None, "max": None, "regex": ""}, + "crtd_on": 1502869927688, + "mod_on": 1706556781278, + } + q = SpectrumQuestion.from_api(example_3, "us", "eng") + # This fails because the text has variables from other questions in it + assert q is None + + def test_parse_from_api_4(self): + + example_4 = { + "qualification_code": 1039, + "text": "Do you suffer from any of the following ailments or medical conditions? (Select all that apply) " + " %%1039%%", + "cat": "Ailments, Illness", + "desc": "Standard Ailments", + "type": 3, + "class": 2, + "condition_codes": [ + {"id": "111", "text": "Allergies (Food, Nut, Skin)"}, + {"id": "999", "text": "None of the above"}, + {"id": "130", "text": "Other"}, + { + "id": "129", + "text": "Women's Health Conditions (Reproductive Issues)", + }, + ], + "format": {"min": None, "max": None, "regex": ""}, + "crtd_on": 1502869927688, + "mod_on": 1706557241693, + } + q = SpectrumQuestion.from_api(example_4, "us", "eng") + expected_q = SpectrumQuestion( + question_id="1039", + country_iso="us", + language_iso="eng", + question_name="Standard Ailments", + question_text="Do you suffer from any of the following ailments or medical conditions? (Select all that " + "apply)", + question_type=SpectrumQuestionType.MULTI_SELECT, + tags="Ailments, Illness", + options=[ + SpectrumQuestionOption( + id="111", text="Allergies (Food, Nut, Skin)", order=0 + ), + SpectrumQuestionOption( + id="129", + text="Women's Health Conditions (Reproductive Issues)", + order=1, + ), + SpectrumQuestionOption(id="130", text="Other", order=2), + SpectrumQuestionOption(id="999", text="None of the above", order=3), + ], + class_num=SpectrumQuestionClass.EXTENDED, + created=datetime(2017, 8, 16, 7, 52, 7, 688000, tzinfo=timezone.utc), + is_live=True, + source=Source.SPECTRUM, + category_id=None, + ) + assert expected_q == q + + # todo: we should have something that infers that if the choice text is "None of the above", + # then the choice is exclusive + u = UpkQuestion( + id=None, + ext_question_id="s:1039", + type=UpkQuestionType.MULTIPLE_CHOICE, + selector=UpkQuestionSelectorMC.MULTIPLE_ANSWER, + country_iso="us", + language_iso="eng", + text="Do you suffer from any of the following ailments or medical conditions? (Select all " + "that apply)", + choices=[ + UpkQuestionChoice( + id="111", + text="Allergies (Food, Nut, Skin)", + order=0, + group=None, + exclusive=False, + importance=None, + ), + UpkQuestionChoice( + id="129", + text="Women's Health Conditions (Reproductive Issues)", + order=1, + group=None, + exclusive=False, + importance=None, + ), + UpkQuestionChoice( + id="130", + text="Other", + order=2, + group=None, + exclusive=False, + importance=None, + ), + UpkQuestionChoice( + id="999", + text="None of the above", + order=3, + group=None, + exclusive=False, + importance=None, + ), + ], + ) + assert u == q.to_upk_question() diff --git a/tests/models/spectrum/test_survey.py b/tests/models/spectrum/test_survey.py new file mode 100644 index 0000000..65dec60 --- /dev/null +++ b/tests/models/spectrum/test_survey.py @@ -0,0 +1,413 @@ +from datetime import timezone, datetime +from decimal import Decimal + + +class TestSpectrumCondition: + + def test_condition_create(self): + from generalresearch.models import LogicalOperator + from generalresearch.models.spectrum.survey import ( + SpectrumCondition, + ) + from generalresearch.models.thl.survey.condition import ConditionValueType + + c = SpectrumCondition.from_api( + { + "qualification_code": 212, + "range_sets": [ + {"units": 311, "to": 28, "from": 25}, + {"units": 311, "to": 42, "from": 40}, + ], + } + ) + assert ( + SpectrumCondition( + question_id="212", + values=["25-28", "40-42"], + value_type=ConditionValueType.RANGE, + negate=False, + logical_operator=LogicalOperator.OR, + ) + == c + ) + + # These equal each other b/c age ranges get automatically converted + assert ( + SpectrumCondition( + question_id="212", + values=["25", "26", "27", "28", "40", "41", "42"], + value_type=ConditionValueType.LIST, + negate=False, + logical_operator=LogicalOperator.OR, + ) + == c + ) + + c = SpectrumCondition.from_api( + { + "condition_codes": ["111", "117", "112", "113", "118"], + "qualification_code": 1202, + } + ) + assert ( + SpectrumCondition( + question_id="1202", + values=["111", "112", "113", "117", "118"], + value_type=ConditionValueType.LIST, + negate=False, + logical_operator=LogicalOperator.OR, + ) + == c + ) + + +class TestSpectrumQuota: + + def test_quota_create(self): + from generalresearch.models.spectrum.survey import ( + SpectrumCondition, + SpectrumQuota, + ) + + d = { + "quota_id": "a846b545-4449-4d76-93a2-f8ebdf6e711e", + "quantities": {"currently_open": 57, "remaining": 57, "achieved": 0}, + "criteria": [{"qualification_code": 211, "condition_codes": ["111"]}], + "crtd_on": 1716227282077, + "mod_on": 1716227284146, + "last_complete_date": None, + } + criteria = [SpectrumCondition.from_api(q) for q in d["criteria"]] + d["condition_hashes"] = [x.criterion_hash for x in criteria] + q = SpectrumQuota.from_api(d) + assert SpectrumQuota(remaining_count=57, condition_hashes=["c23c0b9"]) == q + assert q.is_open + + def test_quota_passes(self): + from generalresearch.models.spectrum.survey import ( + SpectrumQuota, + ) + + q = SpectrumQuota(remaining_count=57, condition_hashes=["a"]) + assert q.passes({"a": True}) + assert not q.passes({"a": False}) + assert not q.passes({}) + + # We have to match all + q = SpectrumQuota(remaining_count=57, condition_hashes=["a", "b", "c"]) + assert not q.passes({"a": True, "b": False}) + assert q.passes({"a": True, "b": True, "c": True}) + + # Quota must be open, even if we match + q = SpectrumQuota(remaining_count=0, condition_hashes=["a"]) + assert not q.passes({"a": True}) + + def test_quota_passes_soft(self): + from generalresearch.models.spectrum.survey import ( + SpectrumQuota, + ) + + q = SpectrumQuota(remaining_count=57, condition_hashes=["a", "b", "c"]) + # Pass if we match all + assert (True, set()) == q.matches_soft({"a": True, "b": True, "c": True}) + # Fail if we don't match any + assert (False, set()) == q.matches_soft({"a": True, "b": False, "c": None}) + # Unknown if any are unknown AND we don't fail any + assert (None, {"c", "b"}) == q.matches_soft({"a": True, "b": None, "c": None}) + assert (None, {"a", "c", "b"}) == q.matches_soft( + {"a": None, "b": None, "c": None} + ) + assert (False, set()) == q.matches_soft({"a": None, "b": False, "c": None}) + + +class TestSpectrumSurvey: + def test_survey_create(self): + from generalresearch.models import ( + LogicalOperator, + Source, + TaskCalculationType, + ) + from generalresearch.models.spectrum import SpectrumStatus + from generalresearch.models.spectrum.survey import ( + SpectrumCondition, + SpectrumQuota, + SpectrumSurvey, + ) + from generalresearch.models.thl.survey.condition import ConditionValueType + + # Note: d is the raw response after calling SpectrumAPI.preprocess_survey() on it! + d = { + "survey_id": 29333264, + "survey_name": "Exciting New Survey #29333264", + "survey_status": 22, + "field_end_date": datetime(2024, 5, 23, 18, 18, 31, tzinfo=timezone.utc), + "category": "Exciting New", + "category_code": 232, + "crtd_on": datetime(2024, 5, 20, 17, 48, 13, tzinfo=timezone.utc), + "mod_on": datetime(2024, 5, 20, 18, 18, 31, tzinfo=timezone.utc), + "soft_launch": False, + "click_balancing": 0, + "price_type": 1, + "pii": False, + "buyer_message": "", + "buyer_id": 4726, + "incl_excl": 0, + "cpi": Decimal("1.20000"), + "last_complete_date": None, + "project_last_complete_date": None, + "survey_performance": { + "overall": {"ir": 40, "loi": 10}, + "last_block": {"ir": None, "loi": None}, + }, + "supplier_completes": { + "needed": 495, + "achieved": 0, + "remaining": 495, + "guaranteed_allocation": 0, + "guaranteed_allocation_remaining": 0, + }, + "pds": {"enabled": False, "buyer_name": None}, + "quotas": [ + { + "quota_id": "c2bc961e-4f26-4223-b409-ebe9165cfdf5", + "quantities": { + "currently_open": 491, + "remaining": 495, + "achieved": 0, + }, + "criteria": [ + { + "qualification_code": 212, + "range_sets": [{"units": 311, "to": 64, "from": 18}], + } + ], + "crtd_on": 1716227293496, + "mod_on": 1716229289847, + "last_complete_date": None, + } + ], + "qualifications": [ + { + "range_sets": [{"units": 311, "to": 64, "from": 18}], + "qualification_code": 212, + } + ], + "country_iso": "fr", + "language_iso": "fre", + "bid_ir": 0.4, + "bid_loi": 600, + "last_block_ir": None, + "last_block_loi": None, + "survey_exclusions": set(), + "exclusion_period": 0, + } + s = SpectrumSurvey.from_api(d) + expected_survey = SpectrumSurvey( + cpi=Decimal("1.20000"), + country_isos=["fr"], + language_isos=["fre"], + buyer_id="4726", + source=Source.SPECTRUM, + used_question_ids={"212"}, + survey_id="29333264", + survey_name="Exciting New Survey #29333264", + status=SpectrumStatus.LIVE, + field_end_date=datetime(2024, 5, 23, 18, 18, 31, tzinfo=timezone.utc), + category_code="232", + calculation_type=TaskCalculationType.COMPLETES, + requires_pii=False, + survey_exclusions=set(), + exclusion_period=0, + bid_ir=0.40, + bid_loi=600, + last_block_loi=None, + last_block_ir=None, + overall_loi=None, + overall_ir=None, + project_last_complete_date=None, + country_iso="fr", + language_iso="fre", + include_psids=None, + exclude_psids=None, + qualifications=["77f493d"], + quotas=[SpectrumQuota(remaining_count=491, condition_hashes=["77f493d"])], + conditions={ + "77f493d": SpectrumCondition( + logical_operator=LogicalOperator.OR, + value_type=ConditionValueType.RANGE, + negate=False, + question_id="212", + values=["18-64"], + ) + }, + created_api=datetime(2024, 5, 20, 17, 48, 13, tzinfo=timezone.utc), + modified_api=datetime(2024, 5, 20, 18, 18, 31, tzinfo=timezone.utc), + updated=None, + ) + assert expected_survey.model_dump_json() == s.model_dump_json() + + def test_survey_properties(self): + from generalresearch.models.spectrum.survey import ( + SpectrumSurvey, + ) + + d = { + "survey_id": 29333264, + "survey_name": "#29333264", + "survey_status": 22, + "field_end_date": datetime(2024, 5, 23, 18, 18, 31, tzinfo=timezone.utc), + "category": "Exciting New", + "category_code": 232, + "crtd_on": datetime(2024, 5, 20, 17, 48, 13, tzinfo=timezone.utc), + "mod_on": datetime(2024, 5, 20, 18, 18, 31, tzinfo=timezone.utc), + "soft_launch": False, + "click_balancing": 0, + "price_type": 1, + "pii": False, + "buyer_message": "", + "buyer_id": 4726, + "incl_excl": 0, + "cpi": Decimal("1.20000"), + "last_complete_date": None, + "project_last_complete_date": None, + "quotas": [ + { + "quota_id": "c2bc961e-4f26-4223-b409-ebe9165cfdf5", + "quantities": { + "currently_open": 491, + "remaining": 495, + "achieved": 0, + }, + "criteria": [ + { + "qualification_code": 214, + "range_sets": [{"units": 311, "to": 64, "from": 18}], + } + ], + } + ], + "qualifications": [ + { + "range_sets": [{"units": 311, "to": 64, "from": 18}], + "qualification_code": 212, + }, + {"condition_codes": ["111", "117", "112"], "qualification_code": 1202}, + ], + "country_iso": "fr", + "language_iso": "fre", + "overall_ir": 0.4, + "overall_loi": 600, + "last_block_ir": None, + "last_block_loi": None, + "survey_exclusions": set(), + "exclusion_period": 0, + } + s = SpectrumSurvey.from_api(d) + assert {"212", "1202", "214"} == s.used_question_ids + assert s.is_live + assert s.is_open + assert {"38cea5e", "83955ef", "77f493d"} == s.all_hashes + + def test_survey_eligibility(self): + from generalresearch.models.spectrum.survey import ( + SpectrumQuota, + SpectrumSurvey, + ) + + d = { + "survey_id": 29333264, + "survey_name": "#29333264", + "survey_status": 22, + "field_end_date": datetime(2024, 5, 23, 18, 18, 31, tzinfo=timezone.utc), + "category": "Exciting New", + "category_code": 232, + "crtd_on": datetime(2024, 5, 20, 17, 48, 13, tzinfo=timezone.utc), + "mod_on": datetime(2024, 5, 20, 18, 18, 31, tzinfo=timezone.utc), + "soft_launch": False, + "click_balancing": 0, + "price_type": 1, + "pii": False, + "buyer_message": "", + "buyer_id": 4726, + "incl_excl": 0, + "cpi": Decimal("1.20000"), + "last_complete_date": None, + "project_last_complete_date": None, + "quotas": [], + "qualifications": [], + "country_iso": "fr", + "language_iso": "fre", + "overall_ir": 0.4, + "overall_loi": 600, + "last_block_ir": None, + "last_block_loi": None, + "survey_exclusions": set(), + "exclusion_period": 0, + } + s = SpectrumSurvey.from_api(d) + s.qualifications = ["a", "b", "c"] + s.quotas = [ + SpectrumQuota(remaining_count=10, condition_hashes=["a", "b"]), + SpectrumQuota(remaining_count=0, condition_hashes=["d"]), + SpectrumQuota(remaining_count=10, condition_hashes=["e"]), + ] + + assert s.passes_qualifications({"a": True, "b": True, "c": True}) + assert not s.passes_qualifications({"a": True, "b": True, "c": False}) + + # we do NOT match a full quota, so we pass + assert s.passes_quotas({"a": True, "b": True, "d": False}) + # We dont pass any + assert not s.passes_quotas({}) + # we only pass a full quota + assert not s.passes_quotas({"d": True}) + # we only dont pass a full quota, but we haven't passed any open + assert not s.passes_quotas({"d": False}) + # we pass a quota, but also pass a full quota, so fail + assert not s.passes_quotas({"e": True, "d": True}) + # we pass a quota, but are unknown in a full quota, so fail + assert not s.passes_quotas({"e": True}) + + # # Soft Pair + assert (True, set()) == s.passes_qualifications_soft( + {"a": True, "b": True, "c": True} + ) + assert (False, set()) == s.passes_qualifications_soft( + {"a": True, "b": True, "c": False} + ) + assert (None, set("c")) == s.passes_qualifications_soft( + {"a": True, "b": True, "c": None} + ) + + # we do NOT match a full quota, so we pass + assert (True, set()) == s.passes_quotas_soft({"a": True, "b": True, "d": False}) + # We dont pass any + assert (None, {"a", "b", "d", "e"}) == s.passes_quotas_soft({}) + # we only pass a full quota + assert (False, set()) == s.passes_quotas_soft({"d": True}) + # we only dont pass a full quota, but we haven't passed any open + assert (None, {"a", "b", "e"}) == s.passes_quotas_soft({"d": False}) + # we pass a quota, but also pass a full quota, so fail + assert (False, set()) == s.passes_quotas_soft({"e": True, "d": True}) + # we pass a quota, but are unknown in a full quota, so fail + assert (None, {"d"}) == s.passes_quotas_soft({"e": True}) + + assert s.determine_eligibility({"a": True, "b": True, "c": True, "d": False}) + assert not s.determine_eligibility( + {"a": True, "b": True, "c": False, "d": False} + ) + assert not s.determine_eligibility( + {"a": True, "b": True, "c": None, "d": False} + ) + assert (True, set()) == s.determine_eligibility_soft( + {"a": True, "b": True, "c": True, "d": False} + ) + assert (False, set()) == s.determine_eligibility_soft( + {"a": True, "b": True, "c": False, "d": False} + ) + assert (None, set("c")) == s.determine_eligibility_soft( + {"a": True, "b": True, "c": None, "d": False} + ) + assert (None, {"c", "d"}) == s.determine_eligibility_soft( + {"a": True, "b": True, "c": None, "d": None} + ) diff --git a/tests/models/spectrum/test_survey_manager.py b/tests/models/spectrum/test_survey_manager.py new file mode 100644 index 0000000..582093c --- /dev/null +++ b/tests/models/spectrum/test_survey_manager.py @@ -0,0 +1,130 @@ +import copy +import logging +from datetime import timezone, datetime +from decimal import Decimal + +from pymysql import IntegrityError + + +logger = logging.getLogger() + +example_survey_api_response = { + "survey_id": 29333264, + "survey_name": "#29333264", + "survey_status": 22, + "field_end_date": datetime(2024, 5, 23, 18, 18, 31, tzinfo=timezone.utc), + "category": "Exciting New", + "category_code": 232, + "crtd_on": datetime(2024, 5, 20, 17, 48, 13, tzinfo=timezone.utc), + "mod_on": datetime(2024, 5, 20, 18, 18, 31, tzinfo=timezone.utc), + "soft_launch": False, + "click_balancing": 0, + "price_type": 1, + "pii": False, + "buyer_message": "", + "buyer_id": 4726, + "incl_excl": 0, + "cpi": Decimal("1.20"), + "last_complete_date": None, + "project_last_complete_date": None, + "quotas": [ + { + "quota_id": "c2bc961e-4f26-4223-b409-ebe9165cfdf5", + "quantities": {"currently_open": 491, "remaining": 495, "achieved": 0}, + "criteria": [ + { + "qualification_code": 214, + "range_sets": [{"units": 311, "to": 64, "from": 18}], + } + ], + } + ], + "qualifications": [ + { + "range_sets": [{"units": 311, "to": 64, "from": 18}], + "qualification_code": 212, + }, + {"condition_codes": ["111", "117", "112"], "qualification_code": 1202}, + ], + "country_iso": "fr", + "language_iso": "fre", + "bid_ir": 0.4, + "bid_loi": 600, + "overall_ir": None, + "overall_loi": None, + "last_block_ir": None, + "last_block_loi": None, + "survey_exclusions": set(), + "exclusion_period": 0, +} + + +class TestSpectrumSurvey: + + def test_survey_create(self, settings, spectrum_manager, spectrum_rw): + from generalresearch.models.spectrum.survey import SpectrumSurvey + + assert settings.debug, "CRITICAL: Do not run this on production." + + now = datetime.now(tz=timezone.utc) + spectrum_rw.execute_sql_query( + query=f""" + DELETE FROM `{spectrum_rw.db}`.spectrum_survey + WHERE survey_id = '29333264'""", + commit=True, + ) + + d = example_survey_api_response.copy() + s = SpectrumSurvey.from_api(d) + spectrum_manager.create(s) + + surveys = spectrum_manager.get_survey_library(updated_since=now) + assert len(surveys) == 1 + assert "29333264" == surveys[0].survey_id + assert s.is_unchanged(surveys[0]) + + try: + spectrum_manager.create(s) + except IntegrityError as e: + print(e.args) + + def test_survey_update(self, settings, spectrum_manager, spectrum_rw): + from generalresearch.models.spectrum.survey import SpectrumSurvey + + assert settings.debug, "CRITICAL: Do not run this on production." + + now = datetime.now(tz=timezone.utc) + spectrum_rw.execute_sql_query( + query=f""" + DELETE FROM `{spectrum_rw.db}`.spectrum_survey + WHERE survey_id = '29333264' + """, + commit=True, + ) + d = copy.deepcopy(example_survey_api_response) + s = SpectrumSurvey.from_api(d) + print(s) + + spectrum_manager.create(s) + s.cpi = Decimal("0.50") + spectrum_manager.update([s]) + surveys = spectrum_manager.get_survey_library(updated_since=now) + assert len(surveys) == 1 + assert "29333264" == surveys[0].survey_id + assert Decimal("0.50") == surveys[0].cpi + assert s.is_unchanged(surveys[0]) + + # --- Updating bid/overall/last block + assert 600 == s.bid_loi + assert s.overall_loi is None + assert s.last_block_loi is None + + # now the last block is set + s.bid_loi = None + s.overall_loi = 1000 + s.last_block_loi = 1000 + spectrum_manager.update([s]) + surveys = spectrum_manager.get_survey_library(updated_since=now) + assert 600 == surveys[0].bid_loi + assert 1000 == surveys[0].overall_loi + assert 1000 == surveys[0].last_block_loi |
