diff --git a/apis_v1/documentation_source/all_ballot_items_retrieve_doc.py b/apis_v1/documentation_source/all_ballot_items_retrieve_doc.py index 1511ac3eb..33c9eda84 100644 --- a/apis_v1/documentation_source/all_ballot_items_retrieve_doc.py +++ b/apis_v1/documentation_source/all_ballot_items_retrieve_doc.py @@ -87,6 +87,8 @@ def all_ballot_items_retrieve_doc_template_values(url_root): ' "measure_text": string (if kind_of_ballot_item is MEASURE)\n' \ ' "measure_url": string (if kind_of_ballot_item is MEASURE)\n' \ ' "no_vote_description": string (if kind_of_ballot_item is MEASURE)\n' \ + ' "referendum_con": string (if kind_of_ballot_item is MEASURE)\n' \ + ' "referendum_pro": string (if kind_of_ballot_item is MEASURE)\n' \ ' "state_code": string,\n' \ ' "yes_vote_description": string (if kind_of_ballot_item is MEASURE)\n' \ ' ],\n' \ diff --git a/apis_v1/documentation_source/ballot_item_options_retrieve_doc.py b/apis_v1/documentation_source/ballot_item_options_retrieve_doc.py index 1b99f7906..bca6d2fb2 100644 --- a/apis_v1/documentation_source/ballot_item_options_retrieve_doc.py +++ b/apis_v1/documentation_source/ballot_item_options_retrieve_doc.py @@ -106,6 +106,8 @@ def ballot_item_options_retrieve_doc_template_values(url_root): ' "measure_url": string,\n' \ ' "measure_we_vote_id": string,\n' \ ' "no_vote_description": string,\n' \ + ' "referendum_con": string,\n' \ + ' "referendum_pro": string,\n' \ ' "state_code": string,\n' \ ' "yes_vote_description": string,\n' \ ' ],\n' \ diff --git a/apis_v1/documentation_source/ballot_items_search_retrieve_doc.py b/apis_v1/documentation_source/ballot_items_search_retrieve_doc.py index 280b4c4b6..bd38101b6 100644 --- a/apis_v1/documentation_source/ballot_items_search_retrieve_doc.py +++ b/apis_v1/documentation_source/ballot_items_search_retrieve_doc.py @@ -87,6 +87,8 @@ def ballot_items_search_retrieve_doc_template_values(url_root): ' "measure_text": string (if kind_of_ballot_item is MEASURE)\n' \ ' "measure_url": string (if kind_of_ballot_item is MEASURE)\n' \ ' "no_vote_description": string (if kind_of_ballot_item is MEASURE)\n' \ + ' "referendum_con": string (if kind_of_ballot_item is MEASURE)\n' \ + ' "referendum_pro": string (if kind_of_ballot_item is MEASURE)\n' \ ' "state_code": string,\n' \ ' "yes_vote_description": string (if kind_of_ballot_item is MEASURE)\n' \ ' ],\n' \ diff --git a/apis_v1/documentation_source/ballot_items_sync_out_doc.py b/apis_v1/documentation_source/ballot_items_sync_out_doc.py index 385f5cdce..fa4d0ee3a 100644 --- a/apis_v1/documentation_source/ballot_items_sync_out_doc.py +++ b/apis_v1/documentation_source/ballot_items_sync_out_doc.py @@ -43,6 +43,8 @@ def ballot_items_sync_out_doc_template_values(url_root): ' "measure_url": string,\n' \ ' "no_vote_description": string,\n' \ ' "polling_location_we_vote_id": string,\n' \ + ' "referendum_con": string,\n' \ + ' "referendum_pro": string,\n' \ ' "yes_vote_description": string,\n' \ '}]' diff --git a/apis_v1/documentation_source/measure_retrieve_doc.py b/apis_v1/documentation_source/measure_retrieve_doc.py index edd88344a..8701c670b 100644 --- a/apis_v1/documentation_source/measure_retrieve_doc.py +++ b/apis_v1/documentation_source/measure_retrieve_doc.py @@ -76,6 +76,8 @@ def measure_retrieve_doc_template_values(url_root): ' "measure_url": string,\n' \ ' "no_vote_description": string,\n' \ ' "ocd_division_id": string,\n' \ + ' "referendum_con": string,\n' \ + ' "referendum_pro": string,\n' \ ' "regional_display_name": string,\n' \ ' "state_code": string,\n' \ ' "state_display_name": string,\n' \ diff --git a/apis_v1/documentation_source/measures_sync_out_doc.py b/apis_v1/documentation_source/measures_sync_out_doc.py index 617e63d6c..d5636c834 100644 --- a/apis_v1/documentation_source/measures_sync_out_doc.py +++ b/apis_v1/documentation_source/measures_sync_out_doc.py @@ -45,6 +45,8 @@ def measures_sync_out_doc_template_values(url_root): ' "measure_url": string,\n' \ ' "ocd_division_id": string,\n' \ ' "primary_party": string,\n' \ + ' "referendum_con": string,\n' \ + ' "referendum_pro": string,\n' \ ' "state_code": string,\n' \ ' "wikipedia_page_id": string,\n' \ ' "wikipedia_page_title": string,\n' \ diff --git a/ballot/controllers.py b/ballot/controllers.py index 61718879d..450a530ea 100644 --- a/ballot/controllers.py +++ b/ballot/controllers.py @@ -2564,6 +2564,8 @@ def all_ballot_items_retrieve_for_one_election_for_api(google_civic_election_id, 'no_vote_description': strip_html_tags(contest_measure.ballotpedia_no_vote_description), # 'district_name': "", # TODO Add this 'election_display_name': election_name, + 'referendum_con': strip_html_tags(contest_measure.referendum_con), + 'referendum_pro': strip_html_tags(contest_measure.referendum_pro), # 'regional_display_name': "", # TODO Add this # 'state_display_name': "", # TODO Add this 'we_vote_id': measure_we_vote_id, @@ -3087,6 +3089,8 @@ def generate_ballot_item_list_from_object_list( measure_text = measure_results_dict[measure_we_vote_id].measure_text measure_url = measure_results_dict[measure_we_vote_id].measure_url no_vote_description = measure_results_dict[measure_we_vote_id].ballotpedia_no_vote_description + referendum_con = measure_results_dict[measure_we_vote_id].referendum_con + referendum_pro = measure_results_dict[measure_we_vote_id].referendum_pro state_code = measure_results_dict[measure_we_vote_id].state_code yes_vote_description = measure_results_dict[measure_we_vote_id].ballotpedia_yes_vote_description else: @@ -3095,6 +3099,8 @@ def generate_ballot_item_list_from_object_list( measure_text = ballot_item.measure_text measure_url = ballot_item.measure_url no_vote_description = ballot_item.no_vote_description + referendum_con = ballot_item.referendum_con + referendum_pro = ballot_item.referendum_pro state_code = ballot_item.state_code yes_vote_description = ballot_item.yes_vote_description except Exception as e: @@ -3104,6 +3110,8 @@ def generate_ballot_item_list_from_object_list( measure_text = ballot_item.measure_text measure_url = ballot_item.measure_url no_vote_description = ballot_item.no_vote_description + referendum_con = '' + referendum_pro = '' state_code = ballot_item.state_code yes_vote_description = ballot_item.yes_vote_description one_ballot_item = { @@ -3120,6 +3128,8 @@ def generate_ballot_item_list_from_object_list( 'district_name': "", # TODO Add this 'election_display_name': "", # TODO Add this 'regional_display_name': "", # TODO Add this + 'referendum_con': strip_html_tags(referendum_con), + 'referendum_pro': strip_html_tags(referendum_pro), 'state_code': state_code, 'state_display_name': "", # TODO Add this 'we_vote_id': measure_we_vote_id, diff --git a/ballot/migrations/0009_ballotitem_referendum_con_ballotitem_referendum_pro.py b/ballot/migrations/0009_ballotitem_referendum_con_ballotitem_referendum_pro.py new file mode 100644 index 000000000..8888250b2 --- /dev/null +++ b/ballot/migrations/0009_ballotitem_referendum_con_ballotitem_referendum_pro.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.16 on 2026-09-12 17:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ballot', '0008_ballotitem_election_state_index'), + ] + + operations = [ + migrations.AddField( + model_name='ballotitem', + name='referendum_con', + field=models.TextField(blank=True, default=None), + ), + migrations.AddField( + model_name='ballotitem', + name='referendum_pro', + field=models.TextField(blank=True, default=None), + ), + ] diff --git a/ballot/migrations/0010_alter_ballotitem_referendum_con_and_more.py b/ballot/migrations/0010_alter_ballotitem_referendum_con_and_more.py new file mode 100644 index 000000000..052457b60 --- /dev/null +++ b/ballot/migrations/0010_alter_ballotitem_referendum_con_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.16 on 2026-09-14 17:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ballot', '0009_ballotitem_referendum_con_ballotitem_referendum_pro'), + ] + + operations = [ + migrations.AlterField( + model_name='ballotitem', + name='referendum_con', + field=models.TextField(blank=True, default='', null=True), + ), + migrations.AlterField( + model_name='ballotitem', + name='referendum_pro', + field=models.TextField(blank=True, default='', null=True), + ), + ] diff --git a/ballot/models.py b/ballot/models.py index a7cc5b01b..d34baf684 100644 --- a/ballot/models.py +++ b/ballot/models.py @@ -107,6 +107,8 @@ class BallotItem(models.Model): measure_url = models.TextField(verbose_name='url of measure', null=True) yes_vote_description = models.TextField(verbose_name="what a yes vote means", null=True, blank=True, default=None) no_vote_description = models.TextField(verbose_name="what a no vote means", null=True, blank=True, default=None) + referendum_con = models.TextField(blank=True, default="", null=True) + referendum_pro = models.TextField(blank=True, default="", null=True) class Meta: indexes = [ @@ -235,6 +237,8 @@ def refresh_cached_ballot_item_measure_info(ballot_item, contest_measure=None): ballot_item.measure_text = contest_measure.measure_text ballot_item.measure_url = contest_measure.measure_url ballot_item.no_vote_description = contest_measure.ballotpedia_no_vote_description + ballot_item.referendum_con = contest_measure.referendum_con + ballot_item.referendum_pro = contest_measure.referendum_pro ballot_item.yes_vote_description = contest_measure.ballotpedia_yes_vote_description values_changed = True @@ -388,6 +392,10 @@ def update_or_create_ballot_item_for_voter( create_values['yes_vote_description'] = defaults['yes_vote_description'] if 'no_vote_description' in defaults: create_values['no_vote_description'] = defaults['no_vote_description'] + if 'referendum_con' in defaults: + create_values['referendum_con'] = defaults['referendum_con'] + if 'referendum_pro' in defaults: + create_values['referendum_pro'] = defaults['referendum_pro'] # We search with contest_measure_id and contest_office_id because they are (will be) integers, # which will be a faster search @@ -444,6 +452,12 @@ def update_or_create_ballot_item_for_voter( if 'no_vote_description' in defaults: no_vote_description = defaults['no_vote_description'] ballot_item_on_stage.no_vote_description = no_vote_description + if 'referendum_con' in defaults: + referendum_con = defaults['referendum_con'] + ballot_item_on_stage.referendum_con = referendum_con + if 'referendum_pro' in defaults: + referendum_pro = defaults['referendum_pro'] + ballot_item_on_stage.referendum_pro = referendum_pro ballot_item_on_stage.save() success = True @@ -683,6 +697,10 @@ def create_ballot_item_row_entry( if 'no_vote_description' in defaults: no_vote_description = defaults['no_vote_description'] new_ballot_item.no_vote_description = no_vote_description + if 'referendum_con' in defaults: + new_ballot_item.referendum_con = defaults['referendum_con'] + if 'referendum_pro' in defaults: + new_ballot_item.referendum_pro = defaults['referendum_pro'] if 'state_code' in defaults and positive_value_exists(defaults['state_code']): state_code_from_defaults = defaults['state_code'] state_code_from_defaults = state_code_from_defaults.lower() @@ -695,7 +713,7 @@ def create_ballot_item_row_entry( except Exception as e: success = False new_ballot_item_created = False - status += "BALLOT_ITEM_RETRIEVE_ERROR " + str(e) + " " + status += "CREATE-BALLOT_ITEM_RETRIEVE_ERROR: " + str(e) + " " handle_exception(e, logger=logger, exception_message=status) results = { @@ -758,6 +776,8 @@ def refresh_all_ballot_item_measure_entries(contest_measure): measure_text=contest_measure.measure_text, measure_url=contest_measure.measure_url, no_vote_description=contest_measure.ballotpedia_no_vote_description, + referendum_con=contest_measure.referendum_con, + referendum_pro=contest_measure.referendum_pro, # state_code=contest_measure.state_code, yes_vote_description=contest_measure.ballotpedia_yes_vote_description, ) @@ -956,7 +976,7 @@ def update_ballot_item_row_entry( except Exception as e: success = False ballot_item_updated = False - status += "BALLOT_ITEM_RETRIEVE_ERROR: " + str(e) + " " + status += "UPDATE-BALLOT_ITEM_RETRIEVE_ERROR: " + str(e) + " " handle_exception(e, logger=logger, exception_message=status) results = { @@ -1633,6 +1653,8 @@ def refresh_ballot_items_from_master_tables(self, voter_id, google_civic_electio defaults['measure_url'] = measure.get_measure_url() defaults['yes_vote_description'] = measure.ballotpedia_yes_vote_description defaults['no_vote_description'] = measure.ballotpedia_no_vote_description + defaults['referendum_con'] = measure.referendum_con + defaults['referendum_pro'] = measure.referendum_pro google_ballot_placement = measure.google_ballot_placement ballot_item_display_name = measure.measure_title measure_subtitle = measure.measure_subtitle diff --git a/candidate/controllers.py b/candidate/controllers.py index 253eaa5da..15587ed7d 100644 --- a/candidate/controllers.py +++ b/candidate/controllers.py @@ -30,12 +30,14 @@ from wevote_functions.functions import add_period_to_middle_name_initial, add_period_to_name_prefix_and_suffix, \ convert_to_int, convert_to_political_party_constant, \ extract_instagram_handle_from_text_string, extract_twitter_handle_from_text_string, extract_website_from_url, \ - positive_value_exists, process_request_from_master, \ + is_url_valid, MIDDLE_INITIAL_SUBSTRINGS, \ + normalize_sms_phone_number_for_voter_update, positive_value_exists, process_request_from_master, \ remove_period_from_middle_name_initial, remove_period_from_name_prefix_and_suffix from wevote_functions.functions_date import convert_date_to_we_vote_date_string, \ convert_we_vote_date_string_to_date_as_integer, get_current_date_as_integer, get_current_year_as_integer, \ DATE_FORMAT_YMD_HMS, DATE_FORMAT_YMD from wevote_functions.utils import staticUserAgent +from wevote_functions.validate_email import validate_email from .models import CandidateListManager, CandidateCampaign, CandidateManager, \ CANDIDATE_UNIQUE_ATTRIBUTES_TO_BE_CLEARED, CANDIDATE_UNIQUE_IDENTIFIERS, \ PROFILE_IMAGE_TYPE_BALLOTPEDIA, PROFILE_IMAGE_TYPE_FACEBOOK, PROFILE_IMAGE_TYPE_LINKEDIN, \ @@ -359,6 +361,62 @@ def find_duplicate_candidate(we_vote_candidate, ignore_candidate_id_list, read_o return results +def choose_best_candidate_url( + url_option1, + url_option2, +): + status = '' + success = True + + # Normalize both url_options + url_option1 = normalize_candidate_url_for_comparison(url_option1) + url_option2 = normalize_candidate_url_for_comparison(url_option2) + + if url_option1 == url_option2: + status += "NORMALIZED_URLS_EQUAL " + if is_url_valid(url_option1) and is_url_valid(url_option2): + best_url_string = 'MATCHING' + elif is_url_valid(url_option1): + best_url_string = 'CANDIDATE1' + elif is_url_valid(url_option2): + best_url_string = 'CANDIDATE2' + else: + # Just default to first one + best_url_string = 'CANDIDATE1' + else: + status += "URLS_NOT_EQUAL " + if is_url_valid(url_option1): + best_url_string = 'CANDIDATE1' + elif is_url_valid(url_option2): + best_url_string = 'CANDIDATE2' + else: + best_url_string = 'CANDIDATE1' + + results = { + 'best_url': best_url_string, + 'status': status, + 'success': success, + } + return results + + +def normalize_candidate_url_for_comparison(incoming_url): + # Step by step, Standardize this incoming_url to have "https", no "www", and never have a trailing slash. Return the modified/normalized url + if not incoming_url: + return incoming_url + # Make incoming_url all lower case + incoming_url = incoming_url.lower() + if incoming_url.startswith('http://'): + incoming_url = 'https://' + incoming_url[7:] + # If url doesn't have http or https, add it + if not incoming_url.startswith('https://'): + incoming_url = 'https://' + incoming_url + if incoming_url.startswith('https://www.'): + incoming_url = 'https://' + incoming_url[11:] + if incoming_url.endswith('/'): + incoming_url = incoming_url[:-1] + return incoming_url + def figure_out_candidate_conflict_values(candidate1, candidate2): candidate_merge_conflict_values = {} @@ -387,6 +445,50 @@ def figure_out_candidate_conflict_values(candidate1, candidate2): candidate_merge_conflict_values[attribute] = 'MATCHING' else: candidate_merge_conflict_values[attribute] = 'CONFLICT' + elif attribute in [ + "ballotpedia_candidate_links_retrieved", + "ballotpedia_photo_url_is_broken", + "ballotpedia_photo_url_is_placeholder", + ]: + # Choose the value that is false (unless both are true) -- prevent marking as 'CONFLICT' + # for any of these attributes + if not positive_value_exists(candidate1_attribute_value): + candidate_merge_conflict_values[attribute] = 'CANDIDATE1' + elif not positive_value_exists(candidate2_attribute_value): + candidate_merge_conflict_values[attribute] = 'CANDIDATE2' + else: + candidate_merge_conflict_values[attribute] = 'MATCHING' + elif attribute == "candidate_email": + # We don't want a CONFLICT around candidate_email at this time + if positive_value_exists(candidate1_attribute_value) and \ + positive_value_exists(candidate2_attribute_value) and \ + candidate1_attribute_value.lower() == candidate2_attribute_value.lower(): + candidate_merge_conflict_values[attribute] = 'MATCHING' + elif positive_value_exists(candidate1_attribute_value) and \ + validate_email(candidate1_attribute_value): + candidate_merge_conflict_values[attribute] = 'CANDIDATE1' + elif positive_value_exists(candidate2_attribute_value) and \ + validate_email(candidate2_attribute_value): + candidate_merge_conflict_values[attribute] = 'CANDIDATE2' + else: + candidate_merge_conflict_values[attribute] = 'CANDIDATE1' + elif attribute == "candidate_phone": + # We don't want a CONFLICT around candidate_phone at this time + # normalize_results1 = normalize_sms_phone_number_for_voter_update(candidate1_attribute_value) + # normalized_phone_number1 = normalize_results1['normalized_sms_phone_number'] + # normalize_results2 = normalize_sms_phone_number_for_voter_update(candidate2_attribute_value) + # normalized_phone_number2 = normalize_results2['normalized_sms_phone_number'] + # Note: We could validate phone numbers in the future + if positive_value_exists(candidate1_attribute_value) and \ + positive_value_exists(candidate2_attribute_value) and \ + candidate1_attribute_value == candidate2_attribute_value: + candidate_merge_conflict_values[attribute] = 'MATCHING' + elif positive_value_exists(candidate1_attribute_value): + candidate_merge_conflict_values[attribute] = 'CANDIDATE1' + elif positive_value_exists(candidate2_attribute_value): + candidate_merge_conflict_values[attribute] = 'CANDIDATE2' + else: + candidate_merge_conflict_values[attribute] = 'CANDIDATE1' elif attribute == "candidate_ultimate_election_date": candidate1_attribute_value_integer = convert_to_int(candidate1_attribute_value) \ if positive_value_exists(candidate1_attribute_value) else 0 @@ -408,15 +510,28 @@ def figure_out_candidate_conflict_values(candidate1, candidate2): else: candidate_merge_conflict_values[attribute] = 'CANDIDATE1' elif attribute == "candidate_url": - candidate1_attribute_value_trimmed = candidate1_attribute_value.rstrip('/') - candidate2_attribute_value_trimmed = candidate2_attribute_value.rstrip('/') - if candidate1_attribute_value_trimmed.lower() == candidate2_attribute_value_trimmed.lower(): + # Normalize URLs so equivalent URLs match despite cosmetic differences. + results = \ + choose_best_candidate_url(candidate1_attribute_value, candidate2_attribute_value) + candidate_merge_conflict_values[attribute] = results['best_url_string'] + # if positive_value_exists(results['status']): + # print(results['status']) + # For now, we just want to accept a valid URL -- we don't ever show conflict + # else: + # candidate_merge_conflict_values[attribute] = 'CONFLICT' + elif attribute == "candidate_name": + if candidate1_attribute_value.lower().trim() == candidate2_attribute_value.lower().trim(): candidate_merge_conflict_values[attribute] = 'MATCHING' - elif 'http' in candidate2_attribute_value and 'http' not in candidate1_attribute_value: - candidate_merge_conflict_values[attribute] = 'CANDIDATE2' else: - candidate_merge_conflict_values[attribute] = 'CONFLICT' - elif attribute == "candidate_name" or attribute == "state_code": + # We want to make more automatic matches for names when enough other significant fields match + # Examples include: + # Kristi C. Morris <=> Kristi Morris: Should choose "Kristi Morris" + if candidate_match_is_high_confidence_level(candidate1, candidate2): + # Now figure out which is the preferred name format, if we can auto-choose + candidate_merge_conflict_values[attribute] = preferred_name_identifier(candidate1, candidate2) + else: + candidate_merge_conflict_values[attribute] = 'CONFLICT' + elif attribute == "state_code": if candidate1_attribute_value.lower() == candidate2_attribute_value.lower(): candidate_merge_conflict_values[attribute] = 'MATCHING' else: @@ -457,6 +572,87 @@ def figure_out_candidate_conflict_values(candidate1, candidate2): return candidate_merge_conflict_values +def candidate_match_is_high_confidence_level(candidate1, candidate2): + # Confidence level greater than YY means the names can be slightly different and we still merge + confidence_level = 0 + confidence_threshold = 90 + status = "MATCHING: " + # We don't need to deal with conflicts here -- only positive matches. Conflicts are handled in figure_out_conflict_values + for attribute in CANDIDATE_UNIQUE_IDENTIFIERS: + both_exist = False + neither_exist = False + try: + candidate1_attribute_value = getattr(candidate1, attribute) + candidate2_attribute_value = getattr(candidate2, attribute) + if positive_value_exists(candidate1_attribute_value) and positive_value_exists(candidate2_attribute_value): + both_exist = True + elif not positive_value_exists(candidate1_attribute_value) and not positive_value_exists(candidate2_attribute_value): + neither_exist = True + if neither_exist: + pass + elif attribute == "ballotpedia_candidate_url" \ + or attribute == "candidate_contact_form_url" \ + or attribute == "candidate_instagram_form_url" \ + or attribute == "candidate_url" \ + or attribute == "facebook_url" \ + or attribute == "linkedin_url" \ + or attribute == "youtube_url": + if both_exist and candidate1_attribute_value.lower() == candidate2_attribute_value.lower(): + confidence_level += 20 + status += f"{attribute} " + elif attribute == "candidate_email": + if both_exist and candidate1_attribute_value.lower() == candidate2_attribute_value.lower(): + confidence_level += 50 + status += f"{attribute} " + elif attribute == "candidate_phone": + # We don't want a CONFLICT around candidate_phone at this time + normalize_results1 = normalize_sms_phone_number_for_voter_update(candidate1_attribute_value) + normalized_phone_number1 = normalize_results1['normalized_sms_phone_number'] + normalize_results2 = normalize_sms_phone_number_for_voter_update(candidate2_attribute_value) + normalized_phone_number2 = normalize_results2['normalized_sms_phone_number'] + # Note: We could validate phone numbers in the future + if both_exist and normalized_phone_number1 == normalized_phone_number2: + confidence_level += 50 + status += f"{attribute} " + elif attribute == "vote_usa_politician_id": + if both_exist and candidate1_attribute_value.lower() == candidate2_attribute_value.lower(): + confidence_level += 95 + status += f"{attribute} " + except AttributeError: + pass + if confidence_level >= confidence_threshold: + return True + return False + + +def preferred_name_identifier(candidate1_name, candidate2_name): + # If candidate1_name_lower_case has a value from MIDDLE_INITIAL_SUBSTRINGS in it, return "CANDIDATE2". + # If candidate2_name_lower_case has a value from MIDDLE_INITIAL_SUBSTRINGS in it, return "CANDIDATE1". + # For example, Kristi C. Morris <=> Kristi Morris: Should choose "Kristi Morris" + candidate1_has_middle_initial = False + candidate2_has_middle_initial = False + modified_candidate1_name = candidate1_name + modified_candidate2_name = candidate2_name + for middle_initial_substring in MIDDLE_INITIAL_SUBSTRINGS: + # Remove middle_initial_substring + modified_candidate1_name = modified_candidate1_name.replace(middle_initial_substring, " ") + modified_candidate2_name = modified_candidate2_name.replace(middle_initial_substring, " ") + + if len(candidate1_name) != len(modified_candidate1_name): + candidate1_has_middle_initial = True + if len(candidate2_name) != len(modified_candidate2_name): + candidate2_has_middle_initial = True + + if candidate1_has_middle_initial and candidate2_has_middle_initial: + return "CONFLICT" + elif candidate1_has_middle_initial and not candidate2_has_middle_initial: + # Prefer the name WITHOUT the middle initial: "Kristi Morris" over "Kristi C. Morris" + return "CANDIDATE2" + elif candidate2_has_middle_initial and not candidate1_has_middle_initial: + return "CANDIDATE1" + return "CONFLICT" + + def merge_if_duplicate_candidates(candidate1_on_stage, candidate2_on_stage, conflict_values): success = False status = "MERGE_IF_DUPLICATE_CANDIDATES " @@ -465,6 +661,8 @@ def merge_if_duplicate_candidates(candidate1_on_stage, candidate2_on_stage, conf candidate1_we_vote_id = candidate1_on_stage.we_vote_id candidate2_we_vote_id = candidate2_on_stage.we_vote_id + # conflict_values mostly comes from figure_out_candidate_conflict_values + # Are there any comparisons that require admin intervention? merge_choices = {} for attribute in CANDIDATE_UNIQUE_IDENTIFIERS: diff --git a/import_export_ballotpedia/controllers.py b/import_export_ballotpedia/controllers.py index aa549cb9e..9b2d28435 100644 --- a/import_export_ballotpedia/controllers.py +++ b/import_export_ballotpedia/controllers.py @@ -2814,6 +2814,8 @@ def groom_and_store_sample_ballot_results_api_v4( 'measure_url': contest_measure.measure_url, 'no_vote_description': contest_measure.ballotpedia_no_vote_description, 'polling_location_we_vote_id': polling_location_we_vote_id, + 'referendum_con': contest_measure.referendum_con, + 'referendum_pro': contest_measure.referendum_pro, 'state_code': state_code, 'voter_id': voter_id, 'yes_vote_description': contest_measure.ballotpedia_yes_vote_description, @@ -2853,6 +2855,10 @@ def groom_and_store_sample_ballot_results_api_v4( update_values['ballotpedia_yes_vote_description'] = measure_dict['yes_vote'] if 'no_vote' in measure_dict and positive_value_exists(measure_dict['no_vote']): update_values['ballotpedia_no_vote_description'] = measure_dict['no_vote'] + if 'referendum_con' in measure_dict and positive_value_exists(measure_dict['referendum_con']): + update_values['referendum_con'] = measure_dict['referendum_con'] + if 'referendum_pro' in measure_dict and positive_value_exists(measure_dict['referendum_pro']): + update_values['referendum_pro'] = measure_dict['referendum_pro'] if 'status' in measure_dict and positive_value_exists(measure_dict['status']): update_values['ballotpedia_measure_status'] = measure_dict['status'] create_results = measure_manager.update_or_create_contest_measure( @@ -2872,6 +2878,8 @@ def groom_and_store_sample_ballot_results_api_v4( 'measure_text': contest_measure.measure_text, 'measure_url': contest_measure.measure_url, 'no_vote_description': contest_measure.ballotpedia_no_vote_description, + 'referendum_con': contest_measure.referendum_con, + 'referendum_pro': contest_measure.referendum_pro, 'state_code': state_code, 'polling_location_we_vote_id': polling_location_we_vote_id, 'voter_id': voter_id, diff --git a/import_export_batches/controllers.py b/import_export_batches/controllers.py index 0ded812d0..56fe78f28 100755 --- a/import_export_batches/controllers.py +++ b/import_export_batches/controllers.py @@ -820,6 +820,10 @@ def create_batch_row_action_measure(batch_description, batch_header_map, one_bat "ballotpedia_no_vote_description", batch_header_map, one_batch_row) election_day_text = batch_manager.retrieve_value_from_batch_row( "election_day_text", batch_header_map, one_batch_row) + referendum_con = batch_manager.retrieve_value_from_batch_row( + "referendum_con", batch_header_map, one_batch_row) + referendum_pro = batch_manager.retrieve_value_from_batch_row( + "referendum_pro", batch_header_map, one_batch_row) state_code = batch_manager.retrieve_value_from_batch_row( "state_code", batch_header_map, one_batch_row) @@ -927,6 +931,8 @@ def create_batch_row_action_measure(batch_description, batch_header_map, one_bat batch_row_action_measure.measure_url = measure_url batch_row_action_measure.measure_we_vote_id = measure_we_vote_id batch_row_action_measure.measure_subtitle = measure_subtitle + batch_row_action_measure.referendum_con = referendum_con + batch_row_action_measure.referendum_pro = referendum_pro batch_row_action_measure.state_code = state_code batch_row_action_measure.status = status batch_row_action_measure.kind_of_action = kind_of_action @@ -3016,6 +3022,8 @@ def create_batch_row_action_ballot_item(batch_description, contest_measure_url = "" yes_vote_description = "" no_vote_description = "" + referendum_con = "" + referendum_pro = "" if positive_value_exists(one_batch_row.google_civic_election_id): google_civic_election_id = str(one_batch_row.google_civic_election_id) @@ -3209,6 +3217,8 @@ def create_batch_row_action_ballot_item(batch_description, contest_measure_url = contest_measure.get_measure_url() yes_vote_description = contest_measure.ballotpedia_yes_vote_description no_vote_description = contest_measure.ballotpedia_no_vote_description + referendum_con = contest_measure.referendum_con + referendum_pro = contest_measure.referendum_pro # check for duplicate entries in the live ballot_item data existing_ballot_item_query_completed = False @@ -3296,6 +3306,8 @@ def create_batch_row_action_ballot_item(batch_description, measure_url=contest_measure_url, no_vote_description=no_vote_description, polling_location_we_vote_id=polling_location_we_vote_id, + referendum_con=referendum_con, + referendum_pro=referendum_pro, state_code=state_code, status=status, voter_id=voter_id, @@ -4202,6 +4214,8 @@ def import_measure_data_from_batch_row_actions(batch_header_id, batch_row_id, 'ballotpedia_measure_url': one_batch_row_action.ballotpedia_measure_url, 'ballotpedia_yes_vote_description': one_batch_row_action.ballotpedia_yes_vote_description, 'ballotpedia_no_vote_description': one_batch_row_action.ballotpedia_no_vote_description, + 'referendum_con': one_batch_row_action.referendum_con, + 'referendum_pro': one_batch_row_action.referendum_pro, 'state_code': one_batch_row_action.state_code, } @@ -5652,7 +5666,7 @@ def import_ballot_item_data_from_batch_row_actions(batch_header_id, batch_row_id one_batch_row_action.save() except Exception as e: success = False - status += "BALLOT_ITEM_RETRIEVE_ERROR: " + str(e) + " " + status += "IMPORT-BALLOT_ITEM_RETRIEVE_ERROR: " + str(e) + " " handle_exception(e, logger=logger, exception_message=status) else: status += results['status'] diff --git a/import_export_batches/controllers_vote_usa.py b/import_export_batches/controllers_vote_usa.py index acb175c6c..c7d2031e2 100755 --- a/import_export_batches/controllers_vote_usa.py +++ b/import_export_batches/controllers_vote_usa.py @@ -48,7 +48,6 @@ def store_vote_usa_json_response_to_import_batch_system( google_civic_election_id, organization_we_vote_id, batch_set_id=batch_set_id, state_code=state_code) return results elif kind_of_batch is MEASURE: - # TODO: Not updated from Ballotpedia yet filename = "Measures from Vote USA API" if state_code != "": filename += " for " + state_code.upper() diff --git a/import_export_batches/migrations/0020_batchrowactionballotitem_referendum_con_and_more.py b/import_export_batches/migrations/0020_batchrowactionballotitem_referendum_con_and_more.py new file mode 100644 index 000000000..93a80bc3d --- /dev/null +++ b/import_export_batches/migrations/0020_batchrowactionballotitem_referendum_con_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.16 on 2026-09-12 17:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('import_export_batches', '0019_alter_batchprocess_kind_of_process'), + ] + + operations = [ + migrations.AddField( + model_name='batchrowactionballotitem', + name='referendum_con', + field=models.TextField(blank=True, default=None, null=True), + ), + migrations.AddField( + model_name='batchrowactionballotitem', + name='referendum_pro', + field=models.TextField(blank=True, default=None, null=True), + ), + migrations.AddField( + model_name='batchrowactionmeasure', + name='referendum_con', + field=models.TextField(blank=True, default='', null=True), + ), + migrations.AddField( + model_name='batchrowactionmeasure', + name='referendum_pro', + field=models.TextField(blank=True, default='', null=True), + ), + ] diff --git a/import_export_batches/models.py b/import_export_batches/models.py index 548c6d162..506e90a75 100644 --- a/import_export_batches/models.py +++ b/import_export_batches/models.py @@ -359,10 +359,12 @@ 'measure_name': 'measure_name', 'measure_text': 'measure_text', 'measure_subtitle': 'measure_subtitle', + 'referendum_con': 'referendum_con', + 'referendum_pro': 'referendum_pro', 'state_code': 'state_code', } -# We Vote contest office key on the left, and Ballotpedia field name on right +# We Vote contest office key on the left, and remote system field name on right # This gives us the option of putting the same field from a remote source into two We Vote fields BATCH_HEADER_MAP_MEASURES_TO_BALLOTPEDIA_MEASURES = { 'ballotpedia_district_id': 'ballotpedia_district_id', @@ -406,6 +408,8 @@ 'no_vote_description': 'no_vote_description', 'yes_vote_description': 'yes_vote_description', 'polling_location_we_vote_id': 'polling_location_we_vote_id', + 'referendum_con': 'referendum_con', + 'referendum_pro': 'referendum_pro', 'state_code': 'state_code', 'voter_id': 'voter_id', } @@ -5872,6 +5876,8 @@ class BatchRowActionMeasure(models.Model): ballotpedia_no_vote_description = models.TextField( verbose_name="what a no vote means", null=True, blank=True, default=None) ctcl_uuid = models.CharField(verbose_name="ctcl uuid", max_length=36, null=True, blank=True) + referendum_con = models.TextField(null=True, blank=True, default="") + referendum_pro = models.TextField(null=True, blank=True, default="") status = models.TextField(verbose_name="batch row action measure status", null=True, blank=True, default="") @@ -6649,6 +6655,8 @@ class BatchRowActionBallotItem(models.Model): measure_url = models.TextField(verbose_name='url of measure', null=True) yes_vote_description = models.TextField(verbose_name="what a yes vote means", null=True, blank=True, default=None) no_vote_description = models.TextField(verbose_name="what a no vote means", null=True, blank=True, default=None) + referendum_con = models.TextField(null=True, blank=True, default=None) + referendum_pro = models.TextField(null=True, blank=True, default=None) status = models.TextField(verbose_name="batch row action ballot item status", null=True, blank=True, default="") diff --git a/import_export_google_civic/controllers.py b/import_export_google_civic/controllers.py index d25547936..4a40bb716 100644 --- a/import_export_google_civic/controllers.py +++ b/import_export_google_civic/controllers.py @@ -2516,6 +2516,10 @@ def store_ballot_item_dict_list( if 'yes_vote_description' in one_ballot_item_dict else '' defaults['no_vote_description'] = one_ballot_item_dict['no_vote_description'] \ if 'no_vote_description' in one_ballot_item_dict else '' + defaults['referendum_con'] = one_ballot_item_dict['referendum_con'] \ + if 'referendum_con' in one_ballot_item_dict else '' + defaults['referendum_pro'] = one_ballot_item_dict['referendum_pro'] \ + if 'referendum_pro' in one_ballot_item_dict else '' if positive_value_exists(voter_id): results = ballot_item_manager.update_or_create_ballot_item_for_voter( @@ -3125,10 +3129,14 @@ def process_contest_referendum_from_structured_json( no_vote_description = one_contest_referendum_structured_json['noVoteDescription'] elif 'referendumDetailForNo' in one_contest_referendum_structured_json: no_vote_description = one_contest_referendum_structured_json['referendumDetailForNo'] + else: + no_vote_description = '' if 'yesVoteDescription' in one_contest_referendum_structured_json: yes_vote_description = one_contest_referendum_structured_json['yesVoteDescription'] elif 'referendumDetailForYes' in one_contest_referendum_structured_json: yes_vote_description = one_contest_referendum_structured_json['referendumDetailForYes'] + else: + yes_vote_description = '' referendum_con = one_contest_referendum_structured_json['referendumDetailCon'] if \ 'referendumDetailCon' in one_contest_referendum_structured_json else '' referendum_pro = one_contest_referendum_structured_json['referendumDetailPro'] if \ @@ -3186,6 +3194,10 @@ def process_contest_referendum_from_structured_json( updated_contest_measure_values['ballotpedia_no_vote_description'] = no_vote_description if positive_value_exists(yes_vote_description): updated_contest_measure_values['ballotpedia_yes_vote_description'] = yes_vote_description + if positive_value_exists(referendum_con): + updated_contest_measure_values['referendum_con'] = referendum_con + if positive_value_exists(referendum_pro): + updated_contest_measure_values['referendum_pro'] = referendum_pro measure_manager = ContestMeasureManager() update_or_create_contest_measure_results = measure_manager.update_or_create_contest_measure( @@ -3295,6 +3307,8 @@ def groom_and_store_google_civic_measure_json_2021( # ReferendumTitle longtext * # ReferendumDesc longtext * # ReferendumDetail longtext * + # ReferendumDetailCon longtext * + # ReferendumDetailPro longtext * # ReferendumDetailUrl longtext * # ReferendumFullText longtext * # ReferendumFullTextUrl longtext * @@ -3315,10 +3329,14 @@ def groom_and_store_google_civic_measure_json_2021( no_vote_description = one_contest_json['noVoteDescription'] elif 'referendumDetailForNo' in one_contest_json: no_vote_description = one_contest_json['referendumDetailForNo'] + else: + no_vote_description = '' if 'yesVoteDescription' in one_contest_json: yes_vote_description = one_contest_json['yesVoteDescription'] elif 'referendumDetailForYes' in one_contest_json: yes_vote_description = one_contest_json['referendumDetailForYes'] + else: + yes_vote_description = '' referendum_con = one_contest_json['referendumDetailCon'] if \ 'referendumDetailCon' in one_contest_json else '' referendum_pro = one_contest_json['referendumDetailPro'] if \ @@ -3523,11 +3541,13 @@ def groom_and_store_google_civic_measure_json_2021( if positive_value_exists(district_scope): updated_contest_measure_values['district_scope'] = district_scope if yes_vote_description: - updated_contest_measure_values['ballotpedia_yes_vote_description'] = \ - one_contest_json['yes_vote_description'] + updated_contest_measure_values['ballotpedia_yes_vote_description'] = yes_vote_description if no_vote_description: - updated_contest_measure_values['ballotpedia_no_vote_description'] = \ - one_contest_json['no_vote_description'] + updated_contest_measure_values['ballotpedia_no_vote_description'] = no_vote_description + if referendum_con: + updated_contest_measure_values['referendum_con'] = referendum_con + if referendum_pro: + updated_contest_measure_values['referendum_pro'] = referendum_pro if positive_value_exists(proceed_to_create_measure): update_or_create_contest_measure_results = measure_manager.create_measure_row_entry( @@ -3597,6 +3617,8 @@ def groom_and_store_google_civic_measure_json_2021( 'measure_year': contest_measure.measure_year, 'no_vote_description': contest_measure.ballotpedia_no_vote_description, 'polling_location_we_vote_id': polling_location_we_vote_id, + 'referendum_con': contest_measure.referendum_con, + 'referendum_pro': contest_measure.referendum_pro, 'state_code': state_code, 'voter_id': voter_id, 'yes_vote_description': contest_measure.ballotpedia_yes_vote_description, diff --git a/measure/controllers.py b/measure/controllers.py index eae5b1cb5..28db310a6 100644 --- a/measure/controllers.py +++ b/measure/controllers.py @@ -253,6 +253,8 @@ def measure_retrieve_for_api(measure_id, measure_we_vote_id): # measureRetrieve if contest_measure.measure_url else contest_measure.ballotpedia_measure_url, 'no_vote_description': strip_html_tags(contest_measure.ballotpedia_no_vote_description), 'ocd_division_id': contest_measure.ocd_division_id, + 'referendum_con': contest_measure.referendum_con, + 'referendum_pro': contest_measure.referendum_pro, 'regional_display_name': "", 'state_code': contest_measure.state_code, 'state_display_name': convert_state_code_to_state_text(contest_measure.state_code), @@ -312,6 +314,7 @@ def measures_import_from_structured_json(structured_json): # Consumes measuresS measures_updated = 0 measures_not_processed = 0 for one_measure in structured_json: + vote_usa_measure_id = one_measure['vote_usa_measure_id'] if 'vote_usa_measure_id' in one_measure else '' we_vote_id = one_measure['we_vote_id'] if 'we_vote_id' in one_measure else '' google_civic_election_id = \ one_measure['google_civic_election_id'] if 'google_civic_election_id' in one_measure else 0 @@ -380,7 +383,11 @@ def measures_import_from_structured_json(structured_json): # Consumes measuresS 'measure_year': one_measure['measure_year'] if 'measure_year' in one_measure else '', 'ocd_division_id': one_measure['ocd_division_id'] if 'ocd_division_id' in one_measure else '', 'primary_party': one_measure['primary_party'] if 'primary_party' in one_measure else '', + 'referendum_con': one_measure['referendum_con'] if 'referendum_con' in one_measure else '', + 'referendum_pro': one_measure['referendum_pro'] if 'referendum_pro' in one_measure else '', 'state_code': state_code, + 'vote_usa_measure_id': + one_measure['vote_usa_measure_id'] if 'vote_usa_measure_id' in one_measure else '', 'we_vote_id': we_vote_id, 'wikipedia_page_id': one_measure['wikipedia_page_id'] if 'wikipedia_page_id' in one_measure else '', 'wikipedia_page_title': one_measure['wikipedia_page_title'] if 'wikipedia_page_title' in diff --git a/measure/migrations/0006_contestmeasure_referendum_con_and_more.py b/measure/migrations/0006_contestmeasure_referendum_con_and_more.py new file mode 100644 index 000000000..940d154e8 --- /dev/null +++ b/measure/migrations/0006_contestmeasure_referendum_con_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.16 on 2026-09-12 17:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('measure', '0005_contestmeasuresarepossibleduplicates_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='contestmeasure', + name='referendum_con', + field=models.TextField(blank=True, default=''), + ), + migrations.AddField( + model_name='contestmeasure', + name='referendum_pro', + field=models.TextField(blank=True, default=''), + ), + ] diff --git a/measure/migrations/0007_alter_contestmeasure_referendum_con_and_more.py b/measure/migrations/0007_alter_contestmeasure_referendum_con_and_more.py new file mode 100644 index 000000000..73ba31318 --- /dev/null +++ b/measure/migrations/0007_alter_contestmeasure_referendum_con_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.16 on 2026-09-14 17:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('measure', '0006_contestmeasure_referendum_con_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='contestmeasure', + name='referendum_con', + field=models.TextField(blank=True, default='', null=True), + ), + migrations.AlterField( + model_name='contestmeasure', + name='referendum_pro', + field=models.TextField(blank=True, default='', null=True), + ), + ] diff --git a/measure/models.py b/measure/models.py index 2dec46b6b..0dc78e4e6 100644 --- a/measure/models.py +++ b/measure/models.py @@ -46,6 +46,8 @@ 'measure_url', 'ocd_division_id', 'primary_party', + 'referendum_con', + 'referendum_pro', 'state_code', # 'vote_usa_measure_id', 'vote_smart_id', @@ -63,6 +65,11 @@ # The measure that is on the ballot (equivalent to ContestOffice) class ContestMeasure(models.Model): + def __str__(self): + return f"ContestMeasure(id={self.id}, we_vote_id={self.we_vote_id}, " \ + f"measure_title={self.measure_title}, state_code={self.state_code}, " \ + f"measure_year={self.measure_year})" + # The we_vote_id identifier is unique across all We Vote sites, and allows us to share our data with other # organizations # It starts with "wv" then we add on a database specific identifier like "3v" (WeVoteSetting.site_unique_id_prefix) @@ -156,6 +163,8 @@ class ContestMeasure(models.Model): verbose_name="what a no vote means", null=True, blank=True, default=None) ctcl_uuid = models.CharField(db_index=True, max_length=36, null=True, blank=True) position_dates_set = models.BooleanField(default=False) # Have we finished data update process? + referendum_con = models.TextField(blank=True, default="", null=True) + referendum_pro = models.TextField(blank=True, default="", null=True) vote_usa_measure_id = models.CharField(db_index=True, max_length=255, default=None, null=True) def get_measure_state(self): @@ -952,6 +961,10 @@ def create_measure_row_entry( contest_measure.ballotpedia_yes_vote_description = defaults['ballotpedia_yes_vote_description'] if 'ballotpedia_no_vote_description' in defaults: contest_measure.ballotpedia_no_vote_description = defaults['ballotpedia_no_vote_description'] + if 'referendum_con' in defaults: + contest_measure.referendum_con = defaults['referendum_con'] + if 'referendum_pro' in defaults: + contest_measure.referendum_pro = defaults['referendum_pro'] if 'election_day_text' in defaults: contest_measure.election_day_text = defaults['election_day_text'] if 'measure_url' in defaults: @@ -1045,6 +1058,10 @@ def update_measure_row_entry( existing_measure_entry.ballotpedia_no_vote_description = defaults['ballotpedia_no_vote_description'] if 'measure_url' in defaults: existing_measure_entry.measure_url = defaults['measure_url'] + if 'referendum_con' in defaults: + existing_measure_entry.referendum_con = defaults['referendum_con'] + if 'referendum_pro' in defaults: + existing_measure_entry.referendum_pro = defaults['referendum_pro'] if 'state_code' in defaults: existing_measure_entry.state_code = defaults['state_code'] measure_updated = False @@ -1899,6 +1916,8 @@ def search_measures_in_specific_elections(google_civic_election_id_list, search_ 'measure_url': measure.measure_url, 'measure_we_vote_id': measure.we_vote_id, 'no_vote_description': measure.ballotpedia_no_vote_description, + 'referendum_con': measure.referendum_con, + 'referendum_pro': measure.referendum_pro, 'state_code': measure.state_code, 'yes_vote_description': measure.ballotpedia_yes_vote_description, } diff --git a/measure/views_admin.py b/measure/views_admin.py index fa92f473c..e78def12e 100644 --- a/measure/views_admin.py +++ b/measure/views_admin.py @@ -309,6 +309,7 @@ def find_and_merge_duplicate_measures(state_code=''): def measures_sync_out_view(request): # measuresSyncOut google_civic_election_id = convert_to_int(request.GET.get('google_civic_election_id', 0)) state_code = request.GET.get('state_code', '') + status = '' try: contest_measure_query = ContestMeasure.objects.using('readonly').all() @@ -335,17 +336,21 @@ def measures_sync_out_view(request): # measuresSyncOut 'ocd_division_id', 'primary_party', 'state_code', 'vote_smart_id', + 'vote_usa_measure_id', 'we_vote_id', + 'referendum_con', + 'referendum_pro', 'wikipedia_page_id', 'wikipedia_page_title', 'wikipedia_photo_url') if contest_measure_list_dict: contest_measure_list_json = list(contest_measure_list_dict) return HttpResponse(json.dumps(contest_measure_list_json), content_type='application/json') except Exception as e: - pass + status += 'EXCEPTION processing measures_sync_out_view: ' + str(e) + ' ' + status += 'CONTEST_MEASURE_LIST_MISSING ' json_data = { 'success': False, - 'status': 'CONTEST_MEASURE_LIST_MISSING' + 'status': status, } return HttpResponse(json.dumps(json_data), content_type='application/json') @@ -998,6 +1003,8 @@ def measure_edit_process_view(request): measure_url = request.POST.get('measure_url', False) measure_year = request.POST.get('measure_year', False) maplight_id = request.POST.get('maplight_id', False) + referendum_con = request.POST.get('referendum_con', False) + referendum_pro = request.POST.get('referendum_pro', False) vote_smart_id = request.POST.get('vote_smart_id', False) vote_usa_measure_id = request.POST.get('vote_usa_measure_id', False) state_code = request.POST.get('state_code', False) @@ -1062,6 +1069,10 @@ def measure_edit_process_view(request): measure_on_stage.measure_year = measure_year if maplight_id is not False: measure_on_stage.maplight_id = maplight_id + if referendum_con is not False: + measure_on_stage.referendum_con = referendum_con + if referendum_pro is not False: + measure_on_stage.referendum_pro = referendum_pro if vote_smart_id is not False: measure_on_stage.vote_smart_id = vote_smart_id if vote_usa_measure_id is not False: @@ -1082,8 +1093,6 @@ def measure_edit_process_view(request): measure_on_stage = ContestMeasure( ballotpedia_measure_status=ballotpedia_measure_status, ballotpedia_measure_url=ballotpedia_measure_url, - ballotpedia_no_vote_description=ballotpedia_no_vote_description, - ballotpedia_yes_vote_description=ballotpedia_yes_vote_description, google_civic_election_id=google_civic_election_id, google_civic_measure_title=google_civic_measure_title, google_civic_measure_title2=google_civic_measure_title2, @@ -1110,6 +1119,14 @@ def measure_edit_process_view(request): measure_on_stage.ballotpedia_election_id = 0 else: measure_on_stage.ballotpedia_election_id = ballotpedia_election_id + if ballotpedia_no_vote_description is not False: + measure_on_stage.ballotpedia_no_vote_description = ballotpedia_no_vote_description + if ballotpedia_yes_vote_description is not False: + measure_on_stage.ballotpedia_yes_vote_description = ballotpedia_yes_vote_description + if referendum_con is not False: + measure_on_stage.referendum_con = referendum_con + if referendum_pro is not False: + measure_on_stage.referendum_pro = referendum_pro measure_on_stage.save() messages.add_message(request, messages.INFO, 'New measure saved.') except Exception as e: diff --git a/office/views_admin.py b/office/views_admin.py index 2da322009..19399f11a 100644 --- a/office/views_admin.py +++ b/office/views_admin.py @@ -2371,7 +2371,7 @@ def office_explanations_list_view(request): try: - office_explanations = OfficeExplanation.objects.order_by('we_vote_id') + office_explanations = OfficeExplanation.objects.order_by('office_explanation_name') if positive_value_exists(state_code): office_explanations = office_explanations.filter(state_code__iexact=state_code) if positive_value_exists(office_search): @@ -2394,10 +2394,10 @@ def office_explanations_list_view(request): state_list = sorted(state_codes_modified.items()) template_values = { - 'office_list': office_explanation_list, - 'state_list': state_list, - 'state_code': state_code, - 'office_search': office_search, + 'office_list': office_explanation_list, + 'state_list': state_list, + 'state_code': state_code, + 'office_search': office_search, } return render(request, 'office/office_explanations_list.html', template_values) diff --git a/templates/ballot/ballot_item_list_edit.html b/templates/ballot/ballot_item_list_edit.html index 6f38b2054..9eca92813 100644 --- a/templates/ballot/ballot_item_list_edit.html +++ b/templates/ballot/ballot_item_list_edit.html @@ -44,20 +44,7 @@
{{ measure.measure_subtitle|default_if_none:"" }}
{{ measure.measure_text|default_if_none:"" }}
-A yes vote means...: {{ measure.ballotpedia_yes_vote_description|default_if_none:"" }}
-A no vote means...: {{ measure.ballotpedia_no_vote_description|default_if_none:"" }}
+A yes vote means: {{ measure.ballotpedia_yes_vote_description|default_if_none:"" }}
+A no vote means: {{ measure.ballotpedia_no_vote_description|default_if_none:"" }}
+Pro: {{ measure.referendum_pro|default_if_none:"" }}
+Con: {{ measure.referendum_con|default_if_none:"" }}
- Measure Website: {{ measure.measure_url|default_if_none:"" }}
+ Measure Website:
+ {% if measure.measure_url %}
+
+ {{ measure.measure_url }}
+ {% endif %}
Refresh Position Data with Details from Measure
diff --git a/templates/polling_location/polling_location_summary.html b/templates/polling_location/polling_location_summary.html
index b30aa160c..190495f51 100644
--- a/templates/polling_location/polling_location_summary.html
+++ b/templates/polling_location/polling_location_summary.html
@@ -87,18 +87,7 @@