diff --git a/src/meshapi/tests/sample_join_form_data.py b/src/meshapi/tests/sample_join_form_data.py index 301e197a..e3034810 100644 --- a/src/meshapi/tests/sample_join_form_data.py +++ b/src/meshapi/tests/sample_join_form_data.py @@ -1,17 +1,122 @@ valid_join_form_submission = { "first_name": "John", "last_name": "Smith", - "email": "jsmith@gmail.com", - "phone": "+1585-758-3425", # CSH's phone number :P + "email_address": "jsmith@gmail.com", + "phone_number": "+1 585-758-3425", # CSH's phone number :P + "parsed_phone": "+1 585-758-3425", # CSH's phone number :P + "street_address": "151 Broome Street", # Also covers New York County Test Case + "parsed_street_address": "151 Broome Street", + "city": "New York", + "state": "NY", + "zip_code": "10002", + "apartment": "", + "roof_access": True, + "referral": "Googled it or something IDK", + "ncl": True, + "trust_me_bro": True, + "dob_addr_response": { + "features": [ + { + "properties": { + "housenumber": "151", + "street": "Broome Street", + "borough": "New York", + "region_a": "NY", + "postalcode": "10002", + "addendum": {"pad": {"bin": 1077609}}, + }, + "geometry": {"coordinates": [0, 0]}, + } + ] + }, +} + +# Join form submission, but the phone and the address are going to be corrected +# by us and the member needs to confirm that +valid_join_form_submission_phone_needs_expansion = { + "first_name": "John", + "last_name": "Smith", + "email_address": "jsmith@gmail.com", + "phone_number": "+1585-758-3425", # CSH's phone number :P + "parsed_phone": "+1 585-758-3425", # CSH's phone number :P "street_address": "151 Broome St", # Also covers New York County Test Case "parsed_street_address": "151 Broome Street", "city": "New York", "state": "NY", - "zip": 10002, + "zip_code": "10002", + "apartment": "", + "roof_access": True, + "referral": "Googled it or something IDK", + "ncl": True, + "trust_me_bro": False, + "dob_addr_response": { + "features": [ + { + "properties": { + "housenumber": "151", + "street": "Broome Street", + "borough": "New York", + "region_a": "NY", + "postalcode": "10002", + "addendum": {"pad": {"bin": 1077609}}, + }, + "geometry": {"coordinates": [0, 0]}, + } + ] + }, +} + +valid_join_form_submission_city_needs_expansion = { + "first_name": "John", + "last_name": "Smith", + "email_address": "jsmith@gmail.com", + "phone_number": "+1 585-758-3425", # CSH's phone number :P + "parsed_phone": "+1 585-758-3425", # CSH's phone number :P + "street_address": "151 Broome Street", # Also covers New York County Test Case + "parsed_street_address": "151 Broome Street", + "city": "Manhattan", + "parsed_city": "New York", + "state": "NY", + "zip_code": "10002", + "apartment": "", + "roof_access": True, + "referral": "Googled it or something IDK", + "ncl": True, + "trust_me_bro": True, + "dob_addr_response": { + "features": [ + { + "properties": { + "housenumber": "151", + "street": "Broome Street", + "borough": "New York", + "region_a": "NY", + "postalcode": "10002", + "addendum": {"pad": {"bin": 1077609}}, + }, + "geometry": {"coordinates": [0, 0]}, + } + ] + }, +} + +valid_join_form_submission_street_needs_expansion = { + "first_name": "John", + "last_name": "Smith", + "email_address": "jsmith@gmail.com", + "phone_number": "+1 585-758-3425", # CSH's phone number :P + "parsed_phone": "+1 585-758-3425", # CSH's phone number :P + "street_address": "151 broome st", # Also covers New York County Test Case + "parsed_street_address": "151 Broome Street", + "city": "Manhattan", + "parsed_city": "New York", + "state": "NY", + "zip_code": "10002", "apartment": "", "roof_access": True, "referral": "Googled it or something IDK", "ncl": True, + "trust_me_bro": True, "dob_addr_response": { "features": [ { @@ -32,17 +137,52 @@ valid_join_form_submission_with_apartment_in_address = { "first_name": "John", "last_name": "Smith", - "email": "jsmith@gmail.com", - "phone": "+1585-758-3425", + "email_address": "jsmith@gmail.com", + "phone_number": "+1 585-758-3425", + "parsed_phone": "+1 585-758-3425", # CSH's phone number :P "street_address": "151 Broome St, Apt 1B", # Apt shouldn't be here, but this example tests robustness "parsed_street_address": "151 Broome Street", "city": "New York", "state": "NY", - "zip": 10002, + "zip_code": "10002", "apartment": "Apt 1B", "roof_access": True, "referral": "Googled it or something IDK", "ncl": True, + "trust_me_bro": True, + "dob_addr_response": { + "features": [ + { + "properties": { + "housenumber": "151", + "street": "Broome Street", + "borough": "New York", + "region_a": "NY", + "postalcode": "10002", + "addendum": {"pad": {"bin": 1077609}}, + }, + "geometry": {"coordinates": [0, 0]}, + } + ] + }, +} + +valid_join_form_submission_no_email = { + "first_name": "John", + "last_name": "Smith", + "email_address": None, + "phone_number": "+1 585-758-3425", # CSH's phone number :P + "parsed_phone": "+1 585-758-3425", # CSH's phone number :P + "street_address": "151 Broome St", # Also covers New York County Test Case + "parsed_street_address": "151 Broome Street", + "city": "New York", + "state": "NY", + "zip_code": "10002", + "apartment": "", + "roof_access": True, + "referral": "Googled it or something IDK", + "ncl": True, + "trust_me_bro": True, "dob_addr_response": { "features": [ { @@ -63,17 +203,19 @@ richmond_join_form_submission = { "first_name": "Maya", "last_name": "Viernes", - "email": "maya.viernes@gmail.com", - "phone": "+1585-758-3425", # CSH's phone number :P + "email_address": "maya.viernes@gmail.com", + "phone_number": "+1 585-758-3425", # CSH's phone number :P + "parsed_phone": "+1 585-758-3425", # CSH's phone number :P "street_address": "475 Seaview Ave", "parsed_street_address": "475 Seaview Avenue", "city": "Staten Island", "state": "NY", - "zip": 10305, + "zip_code": "10305", "apartment": "", "roof_access": True, "referral": "Googled it or something IDK", "ncl": True, + "trust_me_bro": True, "dob_addr_response": { "features": [ { @@ -94,17 +236,19 @@ kings_join_form_submission = { "first_name": "Anna", "last_name": "Edwards", - "email": "aedwards@gmail.com", - "phone": "+1585-758-3425", # CSH's phone number :P + "email_address": "aedwards@gmail.com", + "phone_number": "+1 585-758-3425", # CSH's phone number :P + "parsed_phone": "+1 585-758-3425", # CSH's phone number :P "street_address": "188 Prospect Park W", "parsed_street_address": "188 Prospect Park West", "city": "Brooklyn", "state": "NY", - "zip": 11215, + "zip_code": "11215", "apartment": "", "roof_access": True, "referral": "Googled it or something IDK", "ncl": True, + "trust_me_bro": True, "dob_addr_response": { "features": [ { @@ -125,17 +269,19 @@ queens_join_form_submission = { "first_name": "Lee", "last_name": "Cho", - "email": "lcho@gmail.com", - "phone": "+1585-758-3425", # CSH's phone number :P + "email_address": "lcho@gmail.com", + "phone_number": "+1 585-758-3425", # CSH's phone number :P + "parsed_phone": "+1 585-758-3425", # CSH's phone number :P "street_address": "36-01 35th Ave", "parsed_street_address": "36-01 35th Avenue", "city": "Queens", "state": "NY", - "zip": 11106, + "zip_code": "11106", "apartment": "", "roof_access": True, "referral": "Googled it or something IDK", "ncl": True, + "trust_me_bro": True, "dob_addr_response": { "features": [ { @@ -156,17 +302,19 @@ bronx_join_form_submission = { "first_name": "Richie", "last_name": "Smith", - "email": "rsmith@gmail.com", - "phone": "+1585-758-3425", # CSH's phone number :P + "email_address": "rsmith@gmail.com", + "phone_number": "+1 585-758-3425", # CSH's phone number :P + "parsed_phone": "+1 585-758-3425", # CSH's phone number :P "street_address": "250 Bedford Park Blvd W", "parsed_street_address": "250 Bedford Park Blvd West", "city": "Bronx", "state": "NY", - "zip": 10468, + "zip_code": "10468", "apartment": "", "roof_access": True, "referral": "Googled it or something IDK", "ncl": True, + "trust_me_bro": True, "dob_addr_response": { "features": [ { @@ -187,33 +335,55 @@ non_nyc_join_form_submission = { "first_name": "Jane", "last_name": "Doe", - "email": "jdoe@gmail.com", - "phone": "+1585-758-3425", + "email_address": "jdoe@gmail.com", + "phone_number": "+1 585-758-3425", + "parsed_phone": "+1 585-758-3425", # CSH's phone number :P "street_address": "480 E Broad St", "parsed_street_address": "480 East Broad Street", "city": "Columbus", "state": "OH", - "zip": 43215, + "zip_code": "43215", + "apartment": "", + "roof_access": True, + "referral": "Googled it or something IDK", + "ncl": True, + "trust_me_bro": True, + "dob_addr_response": {"features": []}, +} + +new_jersey_join_form_submission = { + "first_name": "Jane", + "last_name": "Doe", + "email_address": "jdoe@gmail.com", + "phone_number": "+1 585-758-3425", + "parsed_phone": "+1 585-758-3425", # CSH's phone number :P + "street_address": "711 Hudson St", + "parsed_street_address": "711 Hudson Street", + "city": "Hoboken", + "state": "NJ", + "zip_code": "07030", "apartment": "", "roof_access": True, "referral": "Googled it or something IDK", "ncl": True, + "trust_me_bro": True, "dob_addr_response": {"features": []}, } invalid_join_form_submission = { "first_name": 25, "last_name": 69, - "email": 420, - "phone": "eight", + "email_address": 420, + "phone_number": "eight", "street_address": False, "parsed_street_address": False, "city": True, "state": "NY", - "zip": 11215, + "zip_code": "11215", "apartment": 3, "roof_access": True, "ncl": "a duck", + "trust_me_bro": True, "dob_addr_response": {"features": []}, } @@ -221,17 +391,19 @@ jefferson_join_form_submission = { "first_name": "John", "last_name": "Smith", - "email": "jsmith@gmail.com", - "phone": "+1585-758-3425", + "email_address": "jsmith@gmail.com", + "phone_number": "+1 585-758-3425", + "parsed_phone": "+1 585-758-3425", # CSH's phone number :P "street_address": "476 Jefferson Street", "parsed_street_address": "476 Jefferson Street", "city": "Brooklyn", "state": "NY", - "zip": 11237, + "zip_code": "11237", "apartment": "27", "roof_access": True, "referral": "Googled it or something IDK", "ncl": True, + "trust_me_bro": True, "dob_addr_response": { "features": [ { diff --git a/src/meshapi/tests/test_install.py b/src/meshapi/tests/test_install.py index a81b4445..b79494f5 100644 --- a/src/meshapi/tests/test_install.py +++ b/src/meshapi/tests/test_install.py @@ -159,7 +159,7 @@ def test_install_number_readonly_on_create(self): f"status code incorrect. Should be {code}, but got {response.status_code}", ) response_obj = json.loads(response.content) - self.assertNotEquals(response_obj["install_number"], 123) + self.assertNotEqual(response_obj["install_number"], 123) response = self.client.post( "/api/v1/installs/", @@ -234,7 +234,7 @@ def test_cant_steal_taken_install_number(self): f"status code incorrect. Should be {code}, but got {response.status_code}", ) response_obj = json.loads(response.content) - self.assertNotEquals(response_obj["install_number"], 1) + self.assertNotEqual(response_obj["install_number"], 1) self.assertEqual(len(Install.objects.all()), 3) def test_get_install_by_id(self): diff --git a/src/meshapi/tests/test_join_form.py b/src/meshapi/tests/test_join_form.py index b1808669..e27d52f3 100644 --- a/src/meshapi/tests/test_join_form.py +++ b/src/meshapi/tests/test_join_form.py @@ -20,10 +20,14 @@ bronx_join_form_submission, jefferson_join_form_submission, kings_join_form_submission, + new_jersey_join_form_submission, non_nyc_join_form_submission, queens_join_form_submission, richmond_join_form_submission, valid_join_form_submission, + valid_join_form_submission_city_needs_expansion, + valid_join_form_submission_phone_needs_expansion, + valid_join_form_submission_street_needs_expansion, valid_join_form_submission_with_apartment_in_address, ) from .util import TestThread @@ -39,10 +43,10 @@ def validate_successful_join_form_submission(test_case, test_name, s, response, # Check if the member was created and that we see it when we # filter for it. existing_members = Member.objects.filter( - Q(phone_number=s.phone) - | Q(primary_email_address=s.email) - | Q(stripe_email_address=s.email) - | Q(additional_email_addresses__contains=[s.email]) + Q(phone_number=s.phone_number) + | Q(primary_email_address=s.email_address) + | Q(stripe_email_address=s.email_address) + | Q(additional_email_addresses__contains=[s.email_address]) ) test_case.assertEqual( @@ -57,7 +61,7 @@ def validate_successful_join_form_submission(test_case, test_name, s, response, street_address=s.street_address, city=s.city, state=s.state, - zip_code=s.zip, + zip_code=s.zip_code, ) length = 1 @@ -85,6 +89,9 @@ def pull_apart_join_form_submission(submission): request = submission.copy() del request["parsed_street_address"] del request["dob_addr_response"] + del request["parsed_phone"] + if "parsed_city" in request: + del request["parsed_city"] # Make sure that we get the right stuff out of the database afterwards s = JoinFormRequest(**request) @@ -92,8 +99,9 @@ def pull_apart_join_form_submission(submission): # Match the format from OSM. I did this to see how OSM would mutate the # raw request we get. s.street_address = submission["parsed_street_address"] - s.city = submission["city"] + s.city = submission["parsed_city"] if "parsed_city" in submission else submission["city"] s.state = submission["state"] + s.phone_number = submission["parsed_phone"] return request, s @@ -143,10 +151,55 @@ def test_valid_join_form(self, submission): ) validate_successful_join_form_submission(self, "Valid Join Form", s, response) + @parameterized.expand( + [ + [valid_join_form_submission_phone_needs_expansion], + [valid_join_form_submission_city_needs_expansion], + [valid_join_form_submission_street_needs_expansion], + [richmond_join_form_submission], + [kings_join_form_submission], + [queens_join_form_submission], + [bronx_join_form_submission], + [valid_join_form_submission_with_apartment_in_address], + ] + ) + def test_valid_join_form_with_member_confirmation(self, submission): + self.requests_mocker.get( + NYC_PLANNING_LABS_GEOCODE_URL, + json=submission["dob_addr_response"], + ) + + request, s = pull_apart_join_form_submission(submission) + + request["trust_me_bro"] = False + + response = self.c.post("/api/v1/join/", request, content_type="application/json") + code = 409 + self.assertEqual( + code, + response.status_code, + f"status code incorrect for Valid Join Form. Should be {code}, but got {response.status_code}.\n Response is: {response.content.decode('utf-8')}", + ) + + changed_info = response.data["changed_info"] + if changed_info: + for k, _ in request.items(): + if k in changed_info.keys(): + request[k] = changed_info[k] + + response = self.c.post("/api/v1/join/", request, content_type="application/json") + code = 201 + self.assertEqual( + code, + response.status_code, + f"status code incorrect for Valid Join Form. Should be {code}, but got {response.status_code}.\n Response is: {response.content.decode('utf-8')}", + ) + validate_successful_join_form_submission(self, "Valid Join Form", s, response) + def test_valid_join_form_aussie_intl_phone(self): request, s = pull_apart_join_form_submission(valid_join_form_submission) - request["phone"] = "+61 3 96 69491 6" # Australian bureau of meteorology (badly formatted) + request["phone_number"] = "+61 3 96 69491 6" # Australian bureau of meteorology (badly formatted) response = self.c.post("/api/v1/join/", request, content_type="application/json") code = 201 @@ -165,7 +218,7 @@ def test_valid_join_form_aussie_intl_phone(self): def test_valid_join_form_guatemala_intl_phone(self): request, s = pull_apart_join_form_submission(valid_join_form_submission) - request["phone"] = "+502 23 5 4 00 0 0" # US Embassy in Guatemala (badly formatted) + request["phone_number"] = "+502 23 5 4 00 0 0" # US Embassy in Guatemala (badly formatted) response = self.c.post("/api/v1/join/", request, content_type="application/json") code = 201 @@ -184,7 +237,7 @@ def test_valid_join_form_guatemala_intl_phone(self): def test_valid_join_form_no_country_code_us_phone(self): request, s = pull_apart_join_form_submission(valid_join_form_submission) - request["phone"] = "212 555 5555" + request["phone_number"] = "212 555 5555" response = self.c.post("/api/v1/join/", request, content_type="application/json") code = 201 @@ -221,8 +274,8 @@ def test_no_ncl(self): def test_no_phone_or_email(self): request, _ = pull_apart_join_form_submission(valid_join_form_submission) - request["email"] = None - request["phone"] = None + request["email_address"] = None + request["phone_number"] = None response = self.c.post("/api/v1/join/", request, content_type="application/json") code = 400 @@ -235,7 +288,7 @@ def test_no_phone_or_email(self): def test_invalid_email_valid_phone(self): request, _ = pull_apart_join_form_submission(valid_join_form_submission) - request["email"] = "aljksdafljkasfjldsaf" + request["email_address"] = "aljksdafljkasfjldsaf" response = self.c.post("/api/v1/join/", request, content_type="application/json") code = 400 @@ -262,6 +315,59 @@ def test_non_nyc_join_form(self): f"status code incorrect for Non NYC Join Form. Should be {code}, but got {response.status_code}.\n Response is: {response.content.decode('utf-8')}", ) + def test_new_jersey_join_form(self): + self.requests_mocker.get( + NYC_PLANNING_LABS_GEOCODE_URL, + json=new_jersey_join_form_submission["dob_addr_response"], + ) + + # Name, email, phone, location, apt, rooftop, referral + form, _ = pull_apart_join_form_submission(new_jersey_join_form_submission) + response = self.c.post("/api/v1/join/", form, content_type="application/json") + + code = 400 + self.assertEqual( + code, + response.status_code, + f"status code incorrect for Non NYC Join Form. Should be {code}, but got {response.status_code}.\n Response is: {response.content.decode('utf-8')}", + ) + + def test_new_jersey_but_nyc_zip_join_form(self): + self.requests_mocker.get( + NYC_PLANNING_LABS_GEOCODE_URL, + json=new_jersey_join_form_submission["dob_addr_response"], + ) + + # Name, email, phone, location, apt, rooftop, referral + form, _ = pull_apart_join_form_submission(new_jersey_join_form_submission) + form["zip_code"] = "10002" + response = self.c.post("/api/v1/join/", form, content_type="application/json") + + code = 400 + self.assertEqual( + code, + response.status_code, + f"status code incorrect for Non NYC Join Form. Should be {code}, but got {response.status_code}.\n Response is: {response.content.decode('utf-8')}", + ) + + def test_nyc_join_form_but_new_jersey_zip(self): + self.requests_mocker.get( + NYC_PLANNING_LABS_GEOCODE_URL, + json=valid_join_form_submission["dob_addr_response"], + ) + + # Name, email, phone, location, apt, rooftop, referral + form, _ = pull_apart_join_form_submission(valid_join_form_submission) + form["zip_code"] = "07030" + response = self.c.post("/api/v1/join/", form, content_type="application/json") + + code = 400 + self.assertEqual( + code, + response.status_code, + f"status code incorrect for Non NYC Join Form. Should be {code}, but got {response.status_code}.\n Response is: {response.content.decode('utf-8')}", + ) + def test_empty_join_form(self): self.requests_mocker.get( NYC_PLANNING_LABS_GEOCODE_URL, @@ -304,7 +410,7 @@ def test_bad_phone_join_form(self): # Name, email, phone, location, apt, rooftop, referral form, _ = pull_apart_join_form_submission(valid_join_form_submission) - form["phone"] = "555-555-55555" + form["phone_number"] = "555-555-55555" response = self.c.post("/api/v1/join/", form, content_type="application/json") code = 400 @@ -326,7 +432,7 @@ def test_bad_email_join_form(self): # Name, email, phone, location, apt, rooftop, referral form, _ = pull_apart_join_form_submission(valid_join_form_submission) - form["email"] = "notareal@email.meshmeshmeshmeshmesh" + form["email_address"] = "notareal@email.meshmeshmeshmeshmesh" response = self.c.post("/api/v1/join/", form, content_type="application/json") code = 400 @@ -365,7 +471,7 @@ def test_bad_address_join_form(self): con = json.loads(response.content.decode("utf-8")) self.assertEqual( - f"(NYC) Address '{form['street_address']}, {form['city']}, {form['state']} {form['zip']}' not found in geosearch.planninglabs.nyc.", + f"(NYC) Address '{form['street_address']}, {form['city']}, {form['state']} {form['zip_code']}' not found in geosearch.planninglabs.nyc.", con["detail"], f"Did not get correct response content for bad address join form: {response.content.decode('utf-8')}", ) @@ -448,7 +554,6 @@ def test_member_moved_join_form_but_somehow_duplicate_objects_already_exist_for_ # Now test that the member can "move" and still access the join form v_sub_2 = valid_join_form_submission.copy() v_sub_2["street_address"] = "152 Broome Street" - form, s = pull_apart_join_form_submission(v_sub_2) # Name, email, phone, location, apt, rooftop, referral @@ -582,7 +687,7 @@ def test_member_moved_and_used_additional_email_join_form(self): # Now test that the member can "move" and still access the join form, getting a new install # number and member object when this happens v_sub_2 = valid_join_form_submission.copy() - v_sub_2["email"] = "jsmith1234@yahoo.com" + v_sub_2["email_address"] = "jsmith1234@yahoo.com" v_sub_2["street_address"] = "152 Broome Street" form, s = pull_apart_join_form_submission(v_sub_2) @@ -635,7 +740,7 @@ def test_member_moved_and_used_a_new_phone_number_join_form(self): # Now test that the member can "move" and still access the join form # (even with a new phone number, so long as they use the same email) v_sub_2 = valid_join_form_submission.copy() - v_sub_2["phone"] = "+1 212-555-5555" + v_sub_2["phone_number"] = "+1 212-555-5555" v_sub_2["street_address"] = "152 Broome Street" form, s = pull_apart_join_form_submission(v_sub_2) @@ -673,7 +778,7 @@ def test_member_moved_and_used_a_new_phone_number_join_form(self): def test_no_email_join_form(self): no_email_submission = valid_join_form_submission.copy() - no_email_submission["email"] = None + no_email_submission["email_address"] = None # Name, email, phone, location, apt, rooftop, referral form, s = pull_apart_join_form_submission(no_email_submission) @@ -807,7 +912,7 @@ def test_member_moved_and_used_non_primary_email_join_form(self): # Now test that the member can move, use the stripe email address, # and we will NOT connect it to their old registration v_sub_2 = valid_join_form_submission.copy() - v_sub_2["email"] = "jsmith+stripe@gmail.com" + v_sub_2["email_address"] = "jsmith+stripe@gmail.com" v_sub_2["street_address"] = "152 Broome Street" v_sub_2["dob_addr_response"] = copy.deepcopy(valid_join_form_submission["dob_addr_response"]) v_sub_2["dob_addr_response"]["features"][0]["properties"]["housenumber"] = "152" @@ -851,7 +956,7 @@ def test_member_moved_and_used_non_primary_email_join_form(self): # Now test that the member can move again, use an additional email address, # and we will still not connect it to their old registration v_sub_3 = valid_join_form_submission.copy() - v_sub_3["email"] = "jsmith+other@gmail.com" + v_sub_3["email_address"] = "jsmith+other@gmail.com" v_sub_3["street_address"] = "178 Broome Street" v_sub_3["dob_addr_response"] = copy.deepcopy(valid_join_form_submission["dob_addr_response"]) v_sub_3["dob_addr_response"]["features"][0]["properties"]["housenumber"] = "178" @@ -874,7 +979,7 @@ def test_member_moved_and_used_non_primary_email_join_form(self): f"but got {response3.status_code}.\n Response is: {response3.content.decode('utf-8')}", ) - validate_successful_join_form_submission(self, "Valid Join Form", s, response3, expected_member_count=2) + validate_successful_join_form_submission(self, "Valid Join Form", s, response3, expected_member_count=3) self.assertNotEqual( str(member_object.id), @@ -975,11 +1080,11 @@ def test_valid_join_form(self): results = [] member1_submission = valid_join_form_submission.copy() - member1_submission["email"] = "member1@xyz.com" - member1_submission["phone"] = "+1 212 555 5555" + member1_submission["email_address"] = "member1@xyz.com" + member1_submission["phone_number"] = "+1 212 555 5555" member2_submission = valid_join_form_submission.copy() - member2_submission["email"] = "member2@xyz.com" - member1_submission["phone"] = "+1 212 555 2222" + member2_submission["email_address"] = "member2@xyz.com" + member1_submission["phone_number"] = "+1 212 555 2222" def invoke_join_form(submission, results): # Slow down the creation of the Install object to force a race condition diff --git a/src/meshapi/tests/test_validation.py b/src/meshapi/tests/test_validation.py index bcf297d5..56484ec0 100644 --- a/src/meshapi/tests/test_validation.py +++ b/src/meshapi/tests/test_validation.py @@ -11,7 +11,7 @@ class TestValidationNYCAddressInfo(TestCase): def test_invalid_state(self): with self.assertRaises(ValueError): - NYCAddressInfo("151 Broome St", "New York", "ny", 10002) + NYCAddressInfo("151 Broome St", "New York", "ny", "10002") @patch("meshapi.validation.requests.get") def test_validate_address_geosearch_unexpected_responses(self, mock_requests): @@ -29,12 +29,12 @@ def test_validate_address_geosearch_unexpected_responses(self, mock_requests): for test_case in test_cases: with self.assertRaises(test_case["exception"]): mock_requests.return_value = test_case["mock"] - NYCAddressInfo("151 Broome St", "New York", "NY", 10002) + NYCAddressInfo("151 Broome St", "New York", "NY", "10002") @patch("meshapi.validation.requests.get", side_effect=Exception("Pretend this is a network issue")) def test_validate_address_geosearch_network(self, mock_requests): with self.assertRaises(AddressAPIError): - NYCAddressInfo("151 Broome St", "New York", "NY", 10002) + NYCAddressInfo("151 Broome St", "New York", "NY", "10002") @patch("meshapi.validation.requests.get") def test_validate_address_good(self, mock_requests): @@ -49,13 +49,13 @@ def test_validate_address_good(self, mock_requests): mock_requests.side_effect = [mock_1, mock_2, mock_3] - nyc_addr_info = NYCAddressInfo("151 Broome St", "New York", "NY", 10002) + nyc_addr_info = NYCAddressInfo("151 Broome St", "New York", "NY", "10002") assert nyc_addr_info is not None assert nyc_addr_info.street_address == "151 Broome St" assert nyc_addr_info.city == "New York" assert nyc_addr_info.state == "NY" - assert nyc_addr_info.zip == 10002 + assert nyc_addr_info.zip == "10002" assert nyc_addr_info.longitude == -73.98492 assert nyc_addr_info.latitude == 40.716245 assert nyc_addr_info.altitude == 61.0 @@ -96,13 +96,13 @@ def test_validate_address_open_data_invalid_response(self, mock_requests): mock_requests.side_effect = [mock_1, mock_2, mock_test_case] - nyc_addr_info = NYCAddressInfo("151 Broome St", "New York", "NY", 10002) + nyc_addr_info = NYCAddressInfo("151 Broome St", "New York", "NY", "10002") assert nyc_addr_info is not None assert nyc_addr_info.street_address == "151 Broome St" assert nyc_addr_info.city == "New York" assert nyc_addr_info.state == "NY" - assert nyc_addr_info.zip == 10002 + assert nyc_addr_info.zip == "10002" assert nyc_addr_info.longitude == -73.98492 assert nyc_addr_info.latitude == 40.716245 assert nyc_addr_info.altitude is None diff --git a/src/meshapi/util/admin_notifications.py b/src/meshapi/util/admin_notifications.py index c81184d0..e833748c 100644 --- a/src/meshapi/util/admin_notifications.py +++ b/src/meshapi/util/admin_notifications.py @@ -40,7 +40,7 @@ def notify_administrators_of_data_issue( + ", ".join(f"<{get_admin_url(m, site_base_url)}|{m}>" for m in model_instances) + ". Please open the database admin UI using the provided links to correct this.\n\n" + "The current database state of these object(s) is: \n" - + f"```\n{json.dumps(serializer.data, indent=2)}\n```", + + f"```\n{json.dumps(serializer.data, indent=2, default=str)}\n```", } if not SLACK_ADMIN_NOTIFICATIONS_WEBHOOK_URL: diff --git a/src/meshapi/validation.py b/src/meshapi/validation.py index ec3f7655..da78f817 100644 --- a/src/meshapi/validation.py +++ b/src/meshapi/validation.py @@ -19,7 +19,7 @@ DOB_BUILDING_HEIGHT_API_URL = "https://data.cityofnewyork.us/resource/qb5r-6dgf.json" -def validate_email_address(email_address: str) -> bool: +def validate_email_address(email_address: str) -> Optional[bool]: return validate_email( email_address=email_address, check_format=True, @@ -56,16 +56,20 @@ class NYCAddressInfo: street_address: str city: str state: str - zip: int + zip: str longitude: float latitude: float altitude: float | None bin: int | None - def __init__(self, street_address: str, city: str, state: str, zip_code: int): + def __init__(self, street_address: str, city: str, state: str, zip_code: str): if state != "New York" and state != "NY": raise ValueError(f"(NYC) State '{state}' is not New York.") + # We only support the five boroughs of NYC at this time + if not NYCZipCodes.match_zip(zip_code): + raise ValueError(f"Non-NYC zip code detected: {zip_code}") + self.address = f"{street_address}, {city}, {state} {zip_code}" try: @@ -92,7 +96,8 @@ def __init__(self, street_address: str, city: str, state: str, zip_code: int): # the closest matching street address it can find, so check that # the ZIP of what we entered matches what we got. - found_zip = int(nyc_planning_resp["features"][0]["properties"]["postalcode"]) + # For some insane reason this is an integer, so we have to cast it to a string + found_zip = str(nyc_planning_resp["features"][0]["properties"]["postalcode"]) if found_zip != zip_code: raise AddressError( f"(NYC) Could not find address '{street_address}, {city}, {state} {zip_code}'. " @@ -106,7 +111,7 @@ def __init__(self, street_address: str, city: str, state: str, zip_code: int): self.city = addr_props["borough"].replace("Manhattan", "New York") self.state = addr_props["region_a"] - self.zip = int(addr_props["postalcode"]) + self.zip = str(addr_props["postalcode"]) if ( not addr_props.get("addendum", {}).get("pad", {}).get("bin") @@ -166,11 +171,7 @@ def validate_phone_number_field(phone_number: str) -> None: raise ValidationError(f"Invalid phone number: {phone_number}") -def geocode_nyc_address(street_address: str, city: str, state: str, zip_code: int) -> Optional[NYCAddressInfo]: - # We only support the five boroughs of NYC at this time - if not NYCZipCodes.match_zip(zip_code): - raise ValueError(f"Non-NYC zip code detected: {zip_code}") - +def geocode_nyc_address(street_address: str, city: str, state: str, zip_code: str) -> Optional[NYCAddressInfo]: attempts_remaining = 2 while attempts_remaining > 0: attempts_remaining -= 1 @@ -179,7 +180,7 @@ def geocode_nyc_address(street_address: str, city: str, state: str, zip_code: in return nyc_addr_info # If the user has given us an invalid address. Tell them to buzz # off. - except AddressError as e: + except (AddressError, ValueError) as e: logging.exception("AddressError when validating address") # Raise to next level raise e diff --git a/src/meshapi/views/forms.py b/src/meshapi/views/forms.py index 241ef876..943f91c6 100644 --- a/src/meshapi/views/forms.py +++ b/src/meshapi/views/forms.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from datetime import date from json.decoder import JSONDecodeError +from typing import Optional from django.db import IntegrityError, transaction from django.db.models import Q @@ -21,6 +22,7 @@ from meshapi.util.django_pglocks import advisory_lock from meshapi.util.network_number import NETWORK_NUMBER_MAX, NETWORK_NUMBER_MIN, get_next_available_network_number from meshapi.validation import ( + NYCAddressInfo, geocode_nyc_address, normalize_phone_number, validate_email_address, @@ -28,22 +30,25 @@ ) from meshdb.utils.spreadsheet_import.building.constants import AddressTruthSource +logging.basicConfig() + # Join Form @dataclass class JoinFormRequest: first_name: str last_name: str - email: str - phone: str + email_address: str + phone_number: str street_address: str city: str state: str - zip: int + zip_code: str apartment: str roof_access: bool referral: str ncl: bool + trust_me_bro: bool # Used to override member data correction class JoinFormRequestSerializer(DataclassSerializer): @@ -72,6 +77,7 @@ class Meta: "install_id": serializers.UUIDField(), "install_number": serializers.IntegerField(), "member_exists": serializers.BooleanField(), + "changed_info": serializers.DictField(), }, ), description="Request received, an install has been created (along with member and " @@ -102,25 +108,31 @@ def join_form(request: Request) -> Response: join_form_full_name = f"{r.first_name} {r.last_name}" - if not r.email: + if not r.email_address: return Response({"detail": "Must provide an email"}, status=status.HTTP_400_BAD_REQUEST) - if r.email and not validate_email_address(r.email): - return Response({"detail": f"{r.email} is not a valid email"}, status=status.HTTP_400_BAD_REQUEST) + if r.email_address and not validate_email_address(r.email_address): + return Response({"detail": f"{r.email_address} is not a valid email"}, status=status.HTTP_400_BAD_REQUEST) # Expects country code!!!! - if r.phone and not validate_phone_number(r.phone): - return Response({"detail": f"{r.phone} is not a valid phone number"}, status=status.HTTP_400_BAD_REQUEST) + if r.phone_number and not validate_phone_number(r.phone_number): + return Response({"detail": f"{r.phone_number} is not a valid phone number"}, status=status.HTTP_400_BAD_REQUEST) - formatted_phone_number = normalize_phone_number(r.phone) if r.phone else None + formatted_phone_number = normalize_phone_number(r.phone_number) if r.phone_number else None try: - nyc_addr_info = geocode_nyc_address(r.street_address, r.city, r.state, r.zip) + try: + nyc_addr_info: Optional[NYCAddressInfo] = geocode_nyc_address(r.street_address, r.city, r.state, r.zip_code) + except Exception as e: + # Ensure this gets logged + logging.exception(e) + raise e except ValueError: + logging.debug(r.street_address, r.city, r.state, r.zip_code) return Response( { "detail": "Non-NYC registrations are not supported at this time. Check back later, " - "or email support@nycmesh.net" + "or send an email to support@nycmesh.net" }, status=status.HTTP_400_BAD_REQUEST, ) @@ -132,6 +144,48 @@ def join_form(request: Request) -> Response: {"detail": "Your address could not be validated."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) + changed_info: dict[str, str | int] = {} + + if formatted_phone_number and r.phone_number != formatted_phone_number: + logging.warning(f"Changed phone_number: {formatted_phone_number} != {r.phone_number}") + changed_info["phone_number"] = formatted_phone_number + + if r.street_address != nyc_addr_info.street_address: + logging.warning(f"Changed street_address: {r.street_address} != {nyc_addr_info.street_address}") + changed_info["street_address"] = nyc_addr_info.street_address + + if r.city != nyc_addr_info.city: + logging.warning(f"Changed city: {r.city} != {nyc_addr_info.city}") + changed_info["city"] = nyc_addr_info.city + + # Let the member know we need to confirm some info with them. We'll send + # back a dictionary with the info that needs confirming. + # This is not a rejection. We expect another join form submission with all + # of this info in place for us. + if changed_info: + if r.trust_me_bro: + logging.warning( + "Got trust_me_bro, even though info was still updated " + f"(email: {r.email_address}, changed_info: {changed_info}). " + "Proceeding with install request submission." + ) + else: + logging.warning("Please confirm a few details") + return Response( + { + "detail": "Please confirm a few details.", + "building_id": None, + "member_id": None, + "install_id": None, + "install_number": None, + # If this is an existing member, then set a flag to let them know we have + # their information in case they need to update anything. + "member_exists": None, + "changed_info": changed_info, + }, + status=status.HTTP_409_CONFLICT, + ) + # A member can have multiple install requests, if they move apartments for example, so we # check if there's an existing member. Group members by matching only on primary email address # This is sublte but important. We do NOT want to dedupe on phone number, or even on additional @@ -142,21 +196,21 @@ def join_form(request: Request) -> Response: # where a couple uses one person's email address to fill out the join form, but signs up for # stripe payments with the other person's email address. If they then break up, and one person # moves out, we definitely do not want to send an email with their new home address to their ex - existing_members = list(Member.objects.filter(Q(primary_email_address=r.email))) + existing_members = list(Member.objects.filter(Q(primary_email_address=r.email_address))) join_form_member = ( existing_members[0] if len(existing_members) > 0 else Member( name=join_form_full_name, - primary_email_address=r.email, + primary_email_address=r.email_address, phone_number=formatted_phone_number, slack_handle=None, ) ) - if r.email not in join_form_member.all_email_addresses: - join_form_member.additional_email_addresses.append(r.email) + if r.email_address not in join_form_member.all_email_addresses: + join_form_member.additional_email_addresses.append(r.email_address) if formatted_phone_number not in join_form_member.all_phone_numbers: join_form_member.additional_phone_numbers.append(formatted_phone_number) @@ -285,10 +339,14 @@ def join_form(request: Request) -> Response: request, ) - logging.info( - f"JoinForm submission success. building_id: {join_form_building.id}, " - f"member_id: {join_form_member.id}, install_number: {join_form_install.install_number}" - ) + success_message = f"""JoinForm submission success {"(trust_me_bro)" if r.trust_me_bro else ""}. \ +building_id: {join_form_building.id}, member_id: {join_form_member.id}, \ +install_number: {join_form_install.install_number}""" + + if r.trust_me_bro: + logging.warning(success_message) + else: + logging.info(success_message) return Response( { @@ -300,6 +358,7 @@ def join_form(request: Request) -> Response: # If this is an existing member, then set a flag to let them know we have # their information in case they need to update anything. "member_exists": True if len(existing_members) > 0 else False, + "changed_info": {}, }, status=status.HTTP_201_CREATED, ) diff --git a/src/meshapi/views/geography.py b/src/meshapi/views/geography.py index aead7446..169bd3de 100644 --- a/src/meshapi/views/geography.py +++ b/src/meshapi/views/geography.py @@ -385,7 +385,7 @@ class GeocodeRequest: street_address: str city: str state: str - zip: int + zip: str class GeocodeSerializer(DataclassSerializer): diff --git a/src/meshapi/zips.py b/src/meshapi/zips.py index 822e5c03..56ffc671 100644 --- a/src/meshapi/zips.py +++ b/src/meshapi/zips.py @@ -1,207 +1,207 @@ bronx = [ - 10466, - 10468, - 10469, - 10465, - 10456, - 10451, - 10461, - 10471, - 10460, - 10454, - 10457, - 10473, - 10452, - 10474, - 10459, - 10467, - 10463, - 10472, - 10464, - 10455, - 10470, - 10462, - 10458, - 10453, - 10475, + "10466", + "10468", + "10469", + "10465", + "10456", + "10451", + "10461", + "10471", + "10460", + "10454", + "10457", + "10473", + "10452", + "10474", + "10459", + "10467", + "10463", + "10472", + "10464", + "10455", + "10470", + "10462", + "10458", + "10453", + "10475", ] new_york = [ - 10029, - 10043, - 10075, - 10024, - 10005, - 10016, - 10011, - 10018, - 10017, - 10009, - 10019, - 10002, - 10013, - 10034, - 10031, - 10006, - 10001, - 10003, - 10040, - 10004, - 10021, - 10007, - 10010, - 10032, - 10036, - 10022, - 10037, - 10060, - 10008, - 10069, - 10020, - 10027, - 10039, - 10012, - 10026, - 10033, - 10035, - 10038, - 10028, - 10044, - 10065, - 10081, - 10025, - 10080, - 10055, - 10023, - 10014, - 10041, - 10030, - 10045, + "10029", + "10043", + "10075", + "10024", + "10005", + "10016", + "10011", + "10018", + "10017", + "10009", + "10019", + "10002", + "10013", + "10034", + "10031", + "10006", + "10001", + "10003", + "10040", + "10004", + "10021", + "10007", + "10010", + "10032", + "10036", + "10022", + "10037", + "10060", + "10008", + "10069", + "10020", + "10027", + "10039", + "10012", + "10026", + "10033", + "10035", + "10038", + "10028", + "10044", + "10065", + "10081", + "10025", + "10080", + "10055", + "10023", + "10014", + "10041", + "10030", + "10045", ] kings = [ - 11234, - 11233, - 11221, - 11232, - 11243, - 11229, - 11256, - 11241, - 11224, - 11236, - 11247, - 11220, - 11225, - 11237, - 11206, - 11216, - 11214, - 11239, - 11245, - 11210, - 11207, - 11219, - 11249, - 11209, - 11223, - 11251, - 11238, - 11230, - 11211, - 11231, - 11228, - 11205, - 11203, - 11202, - 11212, - 11208, - 11416, - 11217, - 11201, - 11215, - 11218, - 11222, - 11235, - 11242, - 11213, - 11252, - 11204, - 11226, + "11234", + "11233", + "11221", + "11232", + "11243", + "11229", + "11256", + "11241", + "11224", + "11236", + "11247", + "11220", + "11225", + "11237", + "11206", + "11216", + "11214", + "11239", + "11245", + "11210", + "11207", + "11219", + "11249", + "11209", + "11223", + "11251", + "11238", + "11230", + "11211", + "11231", + "11228", + "11205", + "11203", + "11202", + "11212", + "11208", + "11416", + "11217", + "11201", + "11215", + "11218", + "11222", + "11235", + "11242", + "11213", + "11252", + "11204", + "11226", ] queens = [ - 11368, - 11412, - 11120, - 11361, - 11352, - 11386, - 11001, - 11104, - 11375, - 11357, - 11414, - 11364, - 11105, - 11377, - 11416, - 11411, - 11365, - 11208, - 11373, - 11415, - 11380, - 11354, - 11366, - 11372, - 11101, - 11351, - 11374, - 11040, - 11005, - 11363, - 11358, - 11106, - 11362, - 11370, - 11367, - 11004, - 11369, - 11385, - 11102, - 11360, - 11103, - 11355, - 11379, - 11359, - 11109, - 11413, - 11371, - 11405, - 11378, - 11356, + "11368", + "11412", + "11120", + "11361", + "11352", + "11386", + "11001", + "11104", + "11375", + "11357", + "11414", + "11364", + "11105", + "11377", + "11416", + "11411", + "11365", + "11208", + "11373", + "11415", + "11380", + "11354", + "11366", + "11372", + "11101", + "11351", + "11374", + "11040", + "11005", + "11363", + "11358", + "11106", + "11362", + "11370", + "11367", + "11004", + "11369", + "11385", + "11102", + "11360", + "11103", + "11355", + "11379", + "11359", + "11109", + "11413", + "11371", + "11405", + "11378", + "11356", ] richmond = [ - 10314, - 10313, - 10312, - 10302, - 10311, - 10301, - 10310, - 10307, - 10304, - 10308, - 10305, - 10303, - 10309, - 10306, + "10314", + "10313", + "10312", + "10302", + "10311", + "10301", + "10310", + "10307", + "10304", + "10308", + "10305", + "10303", + "10309", + "10306", ] class NYCZipCodes: @staticmethod - def match_zip(zip: int) -> bool: + def match_zip(zip: str) -> bool: return any(zip in a for a in [bronx, new_york, kings, queens, richmond]) diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 00000000..5fca3f84 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} \ No newline at end of file