From c14372dea25f9ad1936c2adb16fc2232281eab35 Mon Sep 17 00:00:00 2001 From: Nicholas Long Date: Wed, 24 Apr 2019 16:43:26 -0600 Subject: [PATCH 01/22] add model method for rename --- seed/models/columns.py | 80 ++++++++++++++ seed/tests/test_columns.py | 206 ++++++++++++++++++++++++++++++++++++- 2 files changed, 282 insertions(+), 4 deletions(-) diff --git a/seed/models/columns.py b/seed/models/columns.py index 3fc1ca52c5..9dfc9ebc29 100644 --- a/seed/models/columns.py +++ b/seed/models/columns.py @@ -578,6 +578,86 @@ def clean(self): 'Column \'%s\':\'%s\' is not a field in the database and not marked as extra data. Mark as extra data to save column.') % ( self.table_name, self.column_name)}) + def rename_column(self, table_name, new_column_name, force=False): + """ + Rename the column and move all the data to the new column. This can move the + data from a canonical field to an extra data field or vice versa. By default the + column. + + :param table_name: string name of the table ƒor the new column + :param new_column_name: string new name of column + :param force: boolean force the overwrite of data in the column? + :return: + """ + from seed.models.properties import PropertyState + + # check if the new_column already exists + new_column = Column.objects.filter(table_name=table_name, column_name=new_column_name) + if len(new_column) > 0: + if not force: + print("There is an existing column, do not allow the rename") + return [False, 'New column already exists, pass force=True to overwrite data'] + + new_column = new_column.first() + + # update the fields in the new column to match the old columns + # new_column.display_name = self.display_name + # new_column.is_extra_data = self.is_extra_data + new_column.unit = self.unit + new_column.import_file = self.import_file + new_column.shared_field_type = self.shared_field_type + new_column.merge_protection = self.merge_protection + if not new_column.is_extra_data and not self.is_extra_data: + new_column.units_pint = self.units_pint + new_column.save() + + elif len(new_column) == 0: + # There isn't a column yet, so creating a new one + # New column will always have extra data. + # The units and related data are copied over to the new field + new_column = Column.objects.create( + organization=self.organization, + table_name=table_name, + column_name=new_column_name, + display_name=self.display_name, + is_extra_data=True, + unit=self.unit, + # unit_pint # Do not import unit_pint since that only works with db fields + import_file=self.import_file, + shared_field_type=self.shared_field_type, + merge_protection=self.merge_protection + ) + + # go through the data and move it to the new field. I'm not sure yet on how long this is + # going to take to run, so we may have to move this to a background task + if self.table_name == 'PropertyState': + properties = PropertyState.objects.filter(organization=new_column.organization) + if new_column.is_extra_data: + if self.is_extra_data: + for prop in properties: + prop.extra_data[new_column.column_name] = prop.extra_data[self.column_name] + del prop.extra_data[self.column_name] + prop.save() + else: + for prop in properties: + prop.extra_data[new_column.column_name] = getattr(prop, self.column_name) + setattr(prop, self.column_name, None) + prop.save() + else: + if self.is_extra_data: + for prop in properties: + setattr(prop, new_column.column_name, prop.extra_data[self.column_name]) + del prop.extra_data[self.column_name] + prop.save() + else: + for prop in properties: + setattr(prop, new_column.column_name, getattr(prop, self.column_name)) + setattr(prop, self.column_name, None) + prop.save() + + # Return true if this opperation was successful + return [True, 'Successfully renamed column and moved data'] + @staticmethod def create_mappings_from_file(filename, organization, user, import_file_id=None): """ diff --git a/seed/tests/test_columns.py b/seed/tests/test_columns.py index a3ec0181ec..72874ca2b8 100644 --- a/seed/tests/test_columns.py +++ b/seed/tests/test_columns.py @@ -5,6 +5,7 @@ :author """ +import copy import os.path from django.core.exceptions import ValidationError @@ -17,6 +18,11 @@ PropertyState, Column, ColumnMapping, + +) +from seed.test_helpers.fake import ( + FakePropertyStateFactory, + FakeTaxLotStateFactory, ) from seed.utils.organizations import create_organization @@ -220,6 +226,194 @@ def test_save_column_mapping_by_file(self): self.assertCountEqual(expected, test_mapping) +class TestRenameColumns(TestCase): + def setUp(self): + user_details = { + 'username': 'test_user@demo.com', + 'password': 'test_pass', + } + self.user = User.objects.create_superuser( + email='test_user@demo.com', **user_details + ) + self.org, _, _ = create_organization(self.user) + self.client.login(**user_details) + + self.property_state_factory = FakePropertyStateFactory(organization=self.org) + self.tax_lot_state_factory = FakeTaxLotStateFactory(organization=self.org) + + self.extra_data_column = Column.objects.create( + table_name='PropertyState', + column_name='test_column', + organization=self.org, + is_extra_data=True, + ) + + def test_rename_column_no_data(self): + address_column = Column.objects.filter(column_name='address_line_1').first() + + # verify that the column has to be new + self.assertFalse(address_column.rename_column('PropertyState', 'custom_id_1')[0]) + + def test_rename_column_no_data_and_force(self): + orig_address_column = Column.objects.filter(column_name='address_line_1').first() + + # verify that the column has to be new + self.assertTrue(orig_address_column.rename_column('PropertyState', 'custom_id_1', True)[0]) + + # get the address column and check the fields + address_column = Column.objects.filter(column_name='address_line_1').first() + self.assertEqual(address_column.is_extra_data, False) + self.assertEqual(address_column.display_name, orig_address_column.display_name) + + def test_rename_column_field_to_field(self): + address_column = Column.objects.filter(column_name='address_line_1').first() + + # create the test data and assemble the expected data result + expected_data = [] + for i in range(0, 20): + state = self.property_state_factory.get_property_state() + expected_data.append(state.address_line_1) + + result = address_column.rename_column('PropertyState', 'property_type', force=True) + self.assertTrue(result) + + results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + 'property_type', flat=True) + ) + self.assertListEqual(results, expected_data) + + # verify that the original field is now empty + results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + 'address_line_1', flat=True) + ) + self.assertListEqual(results, [None for _x in range(20)]) + + def test_rename_column_field_to_extra_data(self): + address_column = Column.objects.filter(column_name='address_line_1').first() + + # create the test data and assemble the expected data result + expected_data = [] + for i in range(0, 20): + state = self.property_state_factory.get_property_state( + extra_data={'string': 'abc %s' % i}) + expected_data.append({'string': state.extra_data['string'], + 'new_address_line_1': state.address_line_1}) + + result = address_column.rename_column('PropertyState', 'new_address_line_1') + self.assertTrue(result) + + results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + 'extra_data', flat=True) + ) + self.assertListEqual(results, expected_data) + + # verify that the original field is now empty + results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + 'address_line_1', flat=True) + ) + self.assertListEqual(results, [None for _x in range(20)]) + + def test_rename_column_extra_data_to_field(self): + # create the test data and assemble the expected data result + expected_data = [] + for i in range(0, 20): + state = self.property_state_factory.get_property_state( + extra_data={self.extra_data_column.column_name: 'abc %s' % i, 'skip': 'value'} + ) + expected_data.append(state.extra_data[self.extra_data_column.column_name]) + + result = self.extra_data_column.rename_column('PropertyState', 'address_line_1', force=True) + self.assertTrue(result) + + results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + 'address_line_1', flat=True) + ) + print(results) + print(expected_data) + self.assertListEqual(results, expected_data) + + # verify that the original field is now empty + results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + 'extra_data', flat=True) + ) + self.assertListEqual(results, [{'skip': 'value'} for _x in range(20)]) + + def test_rename_column_extra_data_to_extra_data(self): + # create the test data and assemble the expected data result + expected_data = [] + for i in range(0, 20): + state = self.property_state_factory.get_property_state( + extra_data={self.extra_data_column.column_name: 'abc %s' % i, 'skip': 'value'} + ) + expected_data.append(state.extra_data[self.extra_data_column.column_name]) + + result = self.extra_data_column.rename_column('PropertyState', 'new_extra', force=True) + self.assertTrue(result) + + results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + 'extra_data', flat=True) + ) + results = [x['new_extra'] for x in results] + print(results) + print(expected_data) + self.assertListEqual(results, expected_data) + + # verify that the original field is now empty + results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + 'extra_data', flat=True) + ) + results = [x.get(self.extra_data_column.column_name, None) for x in results] + self.assertListEqual(results, [None for _x in range(20)]) + + def test_rename_column_extra_data_to_field_int_to_int(self): + # create the test data and assemble the expected data result + expected_data = [] + for i in range(0, 20): + state = self.property_state_factory.get_property_state( + extra_data={self.extra_data_column.column_name: i} + ) + expected_data.append(state.extra_data[self.extra_data_column.column_name]) + + result = self.extra_data_column.rename_column('PropertyState', 'building_count', force=True) + self.assertTrue(result) + + results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + 'building_count', flat=True) + ) + print(results) + print(expected_data) + self.assertListEqual(results, expected_data) + + # def test_rename_column_extra_data_to_field_str_to_int(self): + # # create the test data and assemble the expected data result + # expected_data = [] + # for i in range(0, 20): + # state = self.property_state_factory.get_property_state( + # extra_data={self.extra_data_column.column_name: '%s' % i} + # ) + # expected_data.append(state.extra_data[self.extra_data_column.column_name]) + # + # result = self.extra_data_column.rename_column('PropertyState', 'building_count', force=True) + # self.assertTrue(result) + # + # results = list( + # PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + # 'building_count', flat=True) + # ) + # print(results) + # print(expected_data) + # self.assertListEqual(results, expected_data) + + class TestColumnMapping(TestCase): """Test ColumnMapping utility methods.""" @@ -561,10 +755,12 @@ def test_column_retrieve_db_fields(self): 'latitude', 'longitude', 'lot_number', 'normalized_address', 'number_properties', 'occupied_floor_area', 'owner', 'owner_address', 'owner_city_state', 'owner_email', 'owner_postal_code', 'owner_telephone', 'pm_parent_property_id', 'pm_property_id', - 'postal_code', 'property_footprint', 'property_name', 'property_notes', 'property_type', + 'postal_code', 'property_footprint', 'property_name', 'property_notes', + 'property_type', 'recent_sale_date', 'release_date', 'site_eui', 'site_eui_modeled', 'site_eui_weather_normalized', 'source_eui', 'source_eui_modeled', - 'source_eui_weather_normalized', 'space_alerts', 'state', 'taxlot_footprint', 'ubid', 'ulid', 'updated', + 'source_eui_weather_normalized', 'space_alerts', 'state', 'taxlot_footprint', + 'ubid', 'ulid', 'updated', 'use_description', 'year_built', 'year_ending'] self.assertCountEqual(c, data) @@ -579,10 +775,12 @@ def test_retrieve_db_field_name_from_db_tables(self): 'jurisdiction_tax_lot_id', 'latitude', 'longitude', 'lot_number', 'number_properties', 'occupied_floor_area', 'owner', 'owner_address', 'owner_city_state', 'owner_email', 'owner_postal_code', 'owner_telephone', - 'pm_parent_property_id', 'pm_property_id', 'postal_code', 'property_footprint', 'property_name', + 'pm_parent_property_id', 'pm_property_id', 'postal_code', 'property_footprint', + 'property_name', 'property_notes', 'property_type', 'recent_sale_date', 'release_date', 'site_eui', 'site_eui_modeled', 'site_eui_weather_normalized', 'source_eui', - 'source_eui_modeled', 'source_eui_weather_normalized', 'space_alerts', 'state', 'taxlot_footprint', + 'source_eui_modeled', 'source_eui_weather_normalized', 'space_alerts', 'state', + 'taxlot_footprint', 'ubid', 'ulid', 'updated', 'use_description', 'year_built', 'year_ending'] method_columns = Column.retrieve_db_field_name_for_hash_comparison() From 5f4ab91bbdb193f95d42f8bed9e29c0d39128fd0 Mon Sep 17 00:00:00 2001 From: Nicholas Long Date: Sun, 28 Apr 2019 07:47:24 -0600 Subject: [PATCH 02/22] rename column viewset and cleanup --- seed/data_importer/equivalence_partitioner.py | 2 - seed/models/columns.py | 104 +++++----- seed/tests/test_column_views.py | 184 +++++++++++++++++- seed/tests/test_columns.py | 50 ++--- seed/utils/api.py | 20 +- seed/views/columns.py | 46 ++++- 6 files changed, 303 insertions(+), 103 deletions(-) diff --git a/seed/data_importer/equivalence_partitioner.py b/seed/data_importer/equivalence_partitioner.py index d4cc78f331..7c533c97a6 100644 --- a/seed/data_importer/equivalence_partitioner.py +++ b/seed/data_importer/equivalence_partitioner.py @@ -17,8 +17,6 @@ _log = get_task_logger(__name__) -STR_TO_CLASS = {'TaxLotState': TaxLotState, 'PropertyState': PropertyState} - class EquivalencePartitioner(object): """Class for calculating equivalence classes on model States diff --git a/seed/models/columns.py b/seed/models/columns.py index 9dfc9ebc29..6c709d35f5 100644 --- a/seed/models/columns.py +++ b/seed/models/columns.py @@ -90,18 +90,18 @@ class Column(models.Model): # These are the columns that are removed when looking to see if the records are the same COLUMN_EXCLUDE_FIELDS = [ - 'analysis_state', - 'bounding_box', - 'centroid', - 'data_state', - 'extra_data', - 'geocoding_confidence', - 'id', - 'import_file', - 'long_lat', - 'merge_state', - 'source_type', - ] + EXCLUDED_COLUMN_RETURN_FIELDS + 'analysis_state', + 'bounding_box', + 'centroid', + 'data_state', + 'extra_data', + 'geocoding_confidence', + 'id', + 'import_file', + 'long_lat', + 'merge_state', + 'source_type', + ] + EXCLUDED_COLUMN_RETURN_FIELDS # These are fields that should not be mapped to, ever. EXCLUDED_MAPPING_FIELDS = [ @@ -576,26 +576,26 @@ def clean(self): raise ValidationError( {'is_extra_data': _( 'Column \'%s\':\'%s\' is not a field in the database and not marked as extra data. Mark as extra data to save column.') % ( - self.table_name, self.column_name)}) + self.table_name, self.column_name)}) - def rename_column(self, table_name, new_column_name, force=False): + def rename_column(self, new_column_name, force=False): """ Rename the column and move all the data to the new column. This can move the data from a canonical field to an extra data field or vice versa. By default the column. - :param table_name: string name of the table ƒor the new column :param new_column_name: string new name of column :param force: boolean force the overwrite of data in the column? :return: """ from seed.models.properties import PropertyState + from seed.models.tax_lots import TaxLotState, DATA_STATE_MATCHING + STR_TO_CLASS = {'TaxLotState': TaxLotState, 'PropertyState': PropertyState} # check if the new_column already exists - new_column = Column.objects.filter(table_name=table_name, column_name=new_column_name) + new_column = Column.objects.filter(table_name=self.table_name, column_name=new_column_name) if len(new_column) > 0: if not force: - print("There is an existing column, do not allow the rename") return [False, 'New column already exists, pass force=True to overwrite data'] new_column = new_column.first() @@ -617,7 +617,7 @@ def rename_column(self, table_name, new_column_name, force=False): # The units and related data are copied over to the new field new_column = Column.objects.create( organization=self.organization, - table_name=table_name, + table_name=self.table_name, column_name=new_column_name, display_name=self.display_name, is_extra_data=True, @@ -630,32 +630,34 @@ def rename_column(self, table_name, new_column_name, force=False): # go through the data and move it to the new field. I'm not sure yet on how long this is # going to take to run, so we may have to move this to a background task - if self.table_name == 'PropertyState': - properties = PropertyState.objects.filter(organization=new_column.organization) - if new_column.is_extra_data: - if self.is_extra_data: - for prop in properties: - prop.extra_data[new_column.column_name] = prop.extra_data[self.column_name] - del prop.extra_data[self.column_name] - prop.save() - else: - for prop in properties: - prop.extra_data[new_column.column_name] = getattr(prop, self.column_name) - setattr(prop, self.column_name, None) - prop.save() + orig_data = STR_TO_CLASS[self.table_name].objects.filter( + organization=new_column.organization, + data_state=DATA_STATE_MATCHING + ) + if new_column.is_extra_data: + if self.is_extra_data: + for datum in orig_data: + datum.extra_data[new_column.column_name] = datum.extra_data[self.column_name] + del datum.extra_data[self.column_name] + datum.save() else: - if self.is_extra_data: - for prop in properties: - setattr(prop, new_column.column_name, prop.extra_data[self.column_name]) - del prop.extra_data[self.column_name] - prop.save() - else: - for prop in properties: - setattr(prop, new_column.column_name, getattr(prop, self.column_name)) - setattr(prop, self.column_name, None) - prop.save() + for datum in orig_data: + datum.extra_data[new_column.column_name] = getattr(datum, self.column_name) + setattr(datum, self.column_name, None) + datum.save() + else: + if self.is_extra_data: + for datum in orig_data: + setattr(datum, new_column.column_name, datum.extra_data[self.column_name]) + del datum.extra_data[self.column_name] + datum.save() + else: + for datum in orig_data: + setattr(datum, new_column.column_name, getattr(datum, self.column_name)) + setattr(datum, self.column_name, None) + datum.save() - # Return true if this opperation was successful + # Return true if this operation was successful return [True, 'Successfully renamed column and moved data'] @staticmethod @@ -882,7 +884,7 @@ def select_col_obj(column_name, table_name, organization_column): is_extra_data = True for c in Column.DATABASE_COLUMNS: if field['to_table_name'] == c['table_name'] and field['to_field'] == c[ - 'column_name']: + 'column_name']: is_extra_data = False break @@ -974,16 +976,16 @@ def save_column_names(model_obj): organization=model_obj.organization) for c in columns: if not ColumnMapping.objects.filter( - Q(column_raw=c) | Q(column_mapped=c)).exists(): + Q(column_raw=c) | Q(column_mapped=c)).exists(): _log.debug("Deleting column object {}".format(c.column_name)) c.delete() # Check if there are more than one column still if Column.objects.filter( - table_name=model_obj.__class__.__name__, - column_name=key[:511], - is_extra_data=is_extra_data, - organization=model_obj.organization).count() > 1: + table_name=model_obj.__class__.__name__, + column_name=key[:511], + is_extra_data=is_extra_data, + organization=model_obj.organization).count() > 1: raise Exception( "Could not fix duplicate columns for {}. Contact dev team").format( key) @@ -1117,9 +1119,9 @@ def retrieve_db_fields_from_db_tables(): """ all_columns = [] for f in apps.get_model('seed', 'PropertyState')._meta.fields + \ - apps.get_model('seed', 'TaxLotState')._meta.fields + \ - apps.get_model('seed', 'Property')._meta.fields + \ - apps.get_model('seed', 'TaxLot')._meta.fields: + apps.get_model('seed', 'TaxLotState')._meta.fields + \ + apps.get_model('seed', 'Property')._meta.fields + \ + apps.get_model('seed', 'TaxLot')._meta.fields: # this remove import_file and others if f.get_internal_type() == 'ForeignKey': diff --git a/seed/tests/test_column_views.py b/seed/tests/test_column_views.py index 452f358425..0da9fa42c3 100644 --- a/seed/tests/test_column_views.py +++ b/seed/tests/test_column_views.py @@ -11,6 +11,10 @@ from seed.landing.models import SEEDUser as User from seed.models import ( Column, + PropertyState, + TaxLotState, + DATA_STATE_MATCHING, + ) from seed.utils.organizations import create_organization @@ -21,6 +25,10 @@ 'city', 'state_province', ] +from seed.test_helpers.fake import ( + FakePropertyStateFactory, + FakeTaxLotStateFactory, +) from seed.tests.util import DeleteModelsTestCase @@ -36,12 +44,28 @@ def setUp(self): 'password': 'test_pass', 'email': 'test_user@demo.com' } + user_details_2 = { + 'username': 'test_user_2@demo.com', + 'password': 'test_pass_2', + 'email': 'test_user_2@demo.com' + } self.user = User.objects.create_superuser(**user_details) + self.user_2 = User.objects.create_superuser(**user_details_2) self.org, _, _ = create_organization(self.user, "test-organization-a") + self.org_2, _, _ = create_organization(self.user_2, "test-organization-b") + + self.property_state_factory = FakePropertyStateFactory(organization=self.org) + self.tax_lot_state_factory = FakeTaxLotStateFactory(organization=self.org) - Column.objects.create(column_name='test') - Column.objects.create(column_name='extra_data_test', table_name='PropertyState', + Column.objects.create(column_name='test', organization=self.org) + Column.objects.create(column_name='extra_data_test', + table_name='PropertyState', + organization=self.org, is_extra_data=True) + self.cross_org_column = Column.objects.create(column_name='extra_data_test', + table_name='PropertyState', + organization=self.org_2, + is_extra_data=True) self.client.login(**user_details) @@ -72,8 +96,7 @@ def test_set_default_columns(self): # get show_shared_buildings url = reverse_lazy('api:v2:users-shared-buildings', args=[self.user.pk]) response = self.client.get(url) - json_string = response.content - data = json.loads(json_string) + data = response.json() self.assertEqual(data['show_shared_buildings'], True) # set show_shared_buildings to False @@ -124,3 +147,156 @@ def test_get_all_columns(self): # randomly check a column self.assertIn(expected, data) + + def test_rename_column_property(self): + column = Column.objects.filter( + organization=self.org, table_name='PropertyState', column_name='address_line_1' + ).first() + + for i in range(1, 10): + self.property_state_factory.get_property_state(data_state=DATA_STATE_MATCHING) + self.tax_lot_state_factory.get_taxlot_state(data_state=DATA_STATE_MATCHING) + + for ps in PropertyState.objects.filter(organization=self.org).order_by("pk"): + # orig_data = [{"al1": ps.address_line_1, + # "ed": ps.extra_data, + # "na": ps.normalized_address}] + expected_data = [{"al1": None, + "ed": {"address_line_1_extra_data": ps.address_line_1}, + "na": None}] + + # test building list columns + response = self.client.post( + reverse('api:v2:columns-rename', args=[column.pk]), + content_type='application/json', + data=json.dumps({ + 'new_column_name': 'address_line_1_extra_data', + 'overwrite': False + }) + ) + result = response.json() + self.assertEqual(response.status_code, 200) + self.assertTrue(result['success']) + + for ps in PropertyState.objects.filter(organization=self.org).order_by("pk"): + new_data = [{"al1": ps.address_line_1, + "ed": ps.extra_data, + "na": ps.normalized_address}] + + self.assertListEqual(expected_data, new_data) + + def test_rename_column_property_existing(self): + column = Column.objects.filter( + organization=self.org, table_name='PropertyState', column_name='address_line_1' + ).first() + + for i in range(1, 10): + self.property_state_factory.get_property_state(data_state=DATA_STATE_MATCHING) + + for ps in PropertyState.objects.filter(organization=self.org).order_by("pk"): + expected_data = [{"al1": None, + "pn": ps.address_line_1, + "na": None}] + + response = self.client.post( + reverse('api:v2:columns-rename', args=[column.pk]), + content_type='application/json', + data=json.dumps({ + 'new_column_name': 'property_name', + 'overwrite': False + }) + ) + result = response.json() + self.assertEqual(response.status_code, 200) + self.assertFalse(result['success']) + + response = self.client.post( + reverse('api:v2:columns-rename', args=[column.pk]), + content_type='application/json', + data=json.dumps({ + 'new_column_name': 'property_name', + 'overwrite': True + }) + ) + result = response.json() + self.assertEqual(response.status_code, 200) + self.assertTrue(result['success']) + + for ps in PropertyState.objects.filter(organization=self.org).order_by("pk"): + new_data = [{"al1": ps.address_line_1, + "pn": ps.property_name, + "na": ps.normalized_address}] + + self.assertListEqual(expected_data, new_data) + + def test_rename_column_taxlot(self): + column = Column.objects.filter( + organization=self.org, table_name='TaxLotState', column_name='address_line_1' + ).first() + + for i in range(1, 10): + self.property_state_factory.get_property_state(data_state=DATA_STATE_MATCHING) + self.tax_lot_state_factory.get_taxlot_state(data_state=DATA_STATE_MATCHING) + + for ps in TaxLotState.objects.filter(organization=self.org).order_by("pk"): + # orig_data = [{"al1": ps.address_line_1, + # "ed": ps.extra_data, + # "na": ps.normalized_address}] + expected_data = [{"al1": None, + "ed": {"address_line_1_extra_data": ps.address_line_1}, + "na": None}] + + # test building list columns + response = self.client.post( + reverse('api:v2:columns-rename', args=[column.pk]), + content_type='application/json', + data=json.dumps({ + 'new_column_name': 'address_line_1_extra_data', + 'overwrite': False + }) + ) + result = response.json() + self.assertEqual(response.status_code, 200) + self.assertTrue(result['success']) + + for ps in TaxLotState.objects.filter(organization=self.org).order_by("pk"): + new_data = [{"al1": ps.address_line_1, + "ed": ps.extra_data, + "na": ps.normalized_address}] + + self.assertListEqual(expected_data, new_data) + + def test_rename_column_wrong_org(self): + response = self.client.post( + reverse('api:v2:columns-rename', args=[self.cross_org_column.pk]), + content_type='application/json', + ) + result = response.json() + self.assertFalse(result['success']) + self.assertEqual( + result['message'], + 'Cannot find column in org=%s with pk=%s' % (self.org.id, self.cross_org_column.pk) + ) + + # try setting the org id explicity -- it should still fail with permission denied + response = self.client.post( + reverse('api:v2:columns-rename', args=[self.cross_org_column.pk]), + data=json.dumps({ + 'organization_id': self.org_2.pk + }), + content_type='application/json' + ) + self.assertEqual(response.status_code, 403) + result = response.json() + self.assertEqual(result['detail'], 'Permission denied.') + + def test_rename_column_dne(self): + # test building list columns + response = self.client.post( + reverse('api:v2:columns-rename', args=[-999]), + content_type='application/json', + ) + self.assertEqual(response.status_code, 404) + result = response.json() + self.assertFalse(result['success']) + self.assertEqual(result['message'], 'Cannot find column in org=%s with pk=-999' % self.org.id) diff --git a/seed/tests/test_columns.py b/seed/tests/test_columns.py index 72874ca2b8..4ba03a1a37 100644 --- a/seed/tests/test_columns.py +++ b/seed/tests/test_columns.py @@ -5,7 +5,6 @@ :author """ -import copy import os.path from django.core.exceptions import ValidationError @@ -15,6 +14,7 @@ from seed.landing.models import SEEDUser as User from seed.lib.superperms.orgs.models import Organization from seed.models import ( + DATA_STATE_MATCHING, PropertyState, Column, ColumnMapping, @@ -252,13 +252,13 @@ def test_rename_column_no_data(self): address_column = Column.objects.filter(column_name='address_line_1').first() # verify that the column has to be new - self.assertFalse(address_column.rename_column('PropertyState', 'custom_id_1')[0]) + self.assertFalse(address_column.rename_column('custom_id_1')[0]) def test_rename_column_no_data_and_force(self): orig_address_column = Column.objects.filter(column_name='address_line_1').first() # verify that the column has to be new - self.assertTrue(orig_address_column.rename_column('PropertyState', 'custom_id_1', True)[0]) + self.assertTrue(orig_address_column.rename_column('custom_id_1', True)[0]) # get the address column and check the fields address_column = Column.objects.filter(column_name='address_line_1').first() @@ -271,10 +271,10 @@ def test_rename_column_field_to_field(self): # create the test data and assemble the expected data result expected_data = [] for i in range(0, 20): - state = self.property_state_factory.get_property_state() + state = self.property_state_factory.get_property_state(data_state=DATA_STATE_MATCHING) expected_data.append(state.address_line_1) - result = address_column.rename_column('PropertyState', 'property_type', force=True) + result = address_column.rename_column('property_type', force=True) self.assertTrue(result) results = list( @@ -297,11 +297,12 @@ def test_rename_column_field_to_extra_data(self): expected_data = [] for i in range(0, 20): state = self.property_state_factory.get_property_state( + data_state=DATA_STATE_MATCHING, extra_data={'string': 'abc %s' % i}) expected_data.append({'string': state.extra_data['string'], 'new_address_line_1': state.address_line_1}) - result = address_column.rename_column('PropertyState', 'new_address_line_1') + result = address_column.rename_column('new_address_line_1') self.assertTrue(result) results = list( @@ -322,19 +323,18 @@ def test_rename_column_extra_data_to_field(self): expected_data = [] for i in range(0, 20): state = self.property_state_factory.get_property_state( + data_state=DATA_STATE_MATCHING, extra_data={self.extra_data_column.column_name: 'abc %s' % i, 'skip': 'value'} ) expected_data.append(state.extra_data[self.extra_data_column.column_name]) - result = self.extra_data_column.rename_column('PropertyState', 'address_line_1', force=True) + result = self.extra_data_column.rename_column('address_line_1', force=True) self.assertTrue(result) results = list( PropertyState.objects.filter(organization=self.org).order_by('id').values_list( 'address_line_1', flat=True) ) - print(results) - print(expected_data) self.assertListEqual(results, expected_data) # verify that the original field is now empty @@ -349,11 +349,12 @@ def test_rename_column_extra_data_to_extra_data(self): expected_data = [] for i in range(0, 20): state = self.property_state_factory.get_property_state( + data_state=DATA_STATE_MATCHING, extra_data={self.extra_data_column.column_name: 'abc %s' % i, 'skip': 'value'} ) expected_data.append(state.extra_data[self.extra_data_column.column_name]) - result = self.extra_data_column.rename_column('PropertyState', 'new_extra', force=True) + result = self.extra_data_column.rename_column('new_extra', force=True) self.assertTrue(result) results = list( @@ -361,8 +362,6 @@ def test_rename_column_extra_data_to_extra_data(self): 'extra_data', flat=True) ) results = [x['new_extra'] for x in results] - print(results) - print(expected_data) self.assertListEqual(results, expected_data) # verify that the original field is now empty @@ -378,41 +377,20 @@ def test_rename_column_extra_data_to_field_int_to_int(self): expected_data = [] for i in range(0, 20): state = self.property_state_factory.get_property_state( + data_state=DATA_STATE_MATCHING, extra_data={self.extra_data_column.column_name: i} ) expected_data.append(state.extra_data[self.extra_data_column.column_name]) - result = self.extra_data_column.rename_column('PropertyState', 'building_count', force=True) + result = self.extra_data_column.rename_column('building_count', force=True) self.assertTrue(result) results = list( PropertyState.objects.filter(organization=self.org).order_by('id').values_list( 'building_count', flat=True) ) - print(results) - print(expected_data) self.assertListEqual(results, expected_data) - # def test_rename_column_extra_data_to_field_str_to_int(self): - # # create the test data and assemble the expected data result - # expected_data = [] - # for i in range(0, 20): - # state = self.property_state_factory.get_property_state( - # extra_data={self.extra_data_column.column_name: '%s' % i} - # ) - # expected_data.append(state.extra_data[self.extra_data_column.column_name]) - # - # result = self.extra_data_column.rename_column('PropertyState', 'building_count', force=True) - # self.assertTrue(result) - # - # results = list( - # PropertyState.objects.filter(organization=self.org).order_by('id').values_list( - # 'building_count', flat=True) - # ) - # print(results) - # print(expected_data) - # self.assertListEqual(results, expected_data) - class TestColumnMapping(TestCase): """Test ColumnMapping utility methods.""" @@ -823,7 +801,7 @@ def test_db_columns_in_default_columns(self): found = False for def_column in Column.DATABASE_COLUMNS: if column['table_name'] == def_column['table_name'] and \ - column['column_name'] == def_column['column_name']: + column['column_name'] == def_column['column_name']: found = True continue diff --git a/seed/utils/api.py b/seed/utils/api.py index 758f770fb4..6d54fbb7f5 100644 --- a/seed/utils/api.py +++ b/seed/utils/api.py @@ -230,13 +230,14 @@ class OrgMixin(object): Provides get_organization and get_parent_org method """ - def get_organization(self, request, return_obj=None): + def get_organization(self, request, return_obj=False): """Get org from query param or request.user. :param request: request object. :param return_obj: bool. Set to True if obj vs pk is desired. :return: int representing a valid organization pk or organization object. """ + # print("my return obj is set to %s" % return_obj) if not request.user: return None @@ -246,12 +247,17 @@ def get_organization(self, request, return_obj=None): if not org_id: org = get_user_org(request.user) org_id = int(getattr(org, 'pk')) - if return_obj and not org: - try: - org = request.user.orgs.get(pk=org_id) - except ObjectDoesNotExist: - raise PermissionDenied('Incorrect org id.') - self._organization = org_id if not return_obj else org + if return_obj: + if not org: + try: + org = request.user.orgs.get(pk=org_id) + self._organization = org + except ObjectDoesNotExist: + raise PermissionDenied('Incorrect org id.') + else: + self._organization = org + else: + self._organization = org_id return self._organization def get_parent_org(self, request): diff --git a/seed/views/columns.py b/seed/views/columns.py index cb93c85d32..b2844c0503 100644 --- a/seed/views/columns.py +++ b/seed/views/columns.py @@ -10,7 +10,7 @@ import coreapi from django.http import JsonResponse from rest_framework import status -from rest_framework.decorators import list_route +from rest_framework.decorators import list_route, detail_route from rest_framework.exceptions import NotFound, ParseError from rest_framework.filters import BaseFilterBackend from rest_framework.parsers import JSONParser, FormParser @@ -59,8 +59,8 @@ class ColumnViewSet(OrgValidateMixin, SEEDOrgCreateUpdateModelViewSet): def get_queryset(self): # check if the request is properties or taxlots - org_id = self.get_organization(self.request) - return Column.objects.filter(organization_id=org_id) + org = self.get_organization(self.request, True) + return Column.objects.filter(organization=org) @ajax_request_class def list(self, request): @@ -248,3 +248,43 @@ def add_column_names(self, request): ) columns = ColumnSerializer(columns, many=True) return Response(columns.data, status=status.HTTP_200_OK) + + @ajax_request_class + @has_perm_class('can_modify_data') + @detail_route(methods=['POST']) + def rename(self, request, pk=None): + org = self.get_organization(request, True) + try: + column = Column.objects.get(id=pk, organization=org) + except Column.DoesNotExist: + return JsonResponse({ + 'success': False, + 'message': 'Cannot find column in org=%s with pk=%s' % (org.pk, pk) + }, status=status.HTTP_404_NOT_FOUND) + + new_column_name = request.data.get('new_column_name', None) + overwrite = request.data.get('overwrite', False) + if not new_column_name: + return JsonResponse({ + 'success': False, + 'message': 'You must specify the name of the new column as "new_column_name"' + }) + + result = column.rename_column(new_column_name, overwrite) + if not result[0]: + return JsonResponse({ + 'success': False, + 'message': 'Unable to rename column with message: "%s"' % result[1] + }) + else: + return JsonResponse({ + 'success': True, + 'message': result[1] + }) + + + + + + + From 34e21987640ffcabe91fe87d5e2468d0bb4d40c8 Mon Sep 17 00:00:00 2001 From: Nicholas Long Date: Sun, 28 Apr 2019 07:54:39 -0600 Subject: [PATCH 03/22] flake8 --- seed/models/columns.py | 44 ++++++++++++++++----------------- seed/tests/test_column_views.py | 12 ++++----- seed/tests/test_columns.py | 2 +- seed/views/columns.py | 7 ------ 4 files changed, 29 insertions(+), 36 deletions(-) diff --git a/seed/models/columns.py b/seed/models/columns.py index 6c709d35f5..2463d4dc89 100644 --- a/seed/models/columns.py +++ b/seed/models/columns.py @@ -90,18 +90,18 @@ class Column(models.Model): # These are the columns that are removed when looking to see if the records are the same COLUMN_EXCLUDE_FIELDS = [ - 'analysis_state', - 'bounding_box', - 'centroid', - 'data_state', - 'extra_data', - 'geocoding_confidence', - 'id', - 'import_file', - 'long_lat', - 'merge_state', - 'source_type', - ] + EXCLUDED_COLUMN_RETURN_FIELDS + 'analysis_state', + 'bounding_box', + 'centroid', + 'data_state', + 'extra_data', + 'geocoding_confidence', + 'id', + 'import_file', + 'long_lat', + 'merge_state', + 'source_type', + ] + EXCLUDED_COLUMN_RETURN_FIELDS # These are fields that should not be mapped to, ever. EXCLUDED_MAPPING_FIELDS = [ @@ -576,7 +576,7 @@ def clean(self): raise ValidationError( {'is_extra_data': _( 'Column \'%s\':\'%s\' is not a field in the database and not marked as extra data. Mark as extra data to save column.') % ( - self.table_name, self.column_name)}) + self.table_name, self.column_name)}) def rename_column(self, new_column_name, force=False): """ @@ -884,7 +884,7 @@ def select_col_obj(column_name, table_name, organization_column): is_extra_data = True for c in Column.DATABASE_COLUMNS: if field['to_table_name'] == c['table_name'] and field['to_field'] == c[ - 'column_name']: + 'column_name']: is_extra_data = False break @@ -976,16 +976,16 @@ def save_column_names(model_obj): organization=model_obj.organization) for c in columns: if not ColumnMapping.objects.filter( - Q(column_raw=c) | Q(column_mapped=c)).exists(): + Q(column_raw=c) | Q(column_mapped=c)).exists(): _log.debug("Deleting column object {}".format(c.column_name)) c.delete() # Check if there are more than one column still if Column.objects.filter( - table_name=model_obj.__class__.__name__, - column_name=key[:511], - is_extra_data=is_extra_data, - organization=model_obj.organization).count() > 1: + table_name=model_obj.__class__.__name__, + column_name=key[:511], + is_extra_data=is_extra_data, + organization=model_obj.organization).count() > 1: raise Exception( "Could not fix duplicate columns for {}. Contact dev team").format( key) @@ -1119,9 +1119,9 @@ def retrieve_db_fields_from_db_tables(): """ all_columns = [] for f in apps.get_model('seed', 'PropertyState')._meta.fields + \ - apps.get_model('seed', 'TaxLotState')._meta.fields + \ - apps.get_model('seed', 'Property')._meta.fields + \ - apps.get_model('seed', 'TaxLot')._meta.fields: + apps.get_model('seed', 'TaxLotState')._meta.fields + \ + apps.get_model('seed', 'Property')._meta.fields + \ + apps.get_model('seed', 'TaxLot')._meta.fields: # this remove import_file and others if f.get_internal_type() == 'ForeignKey': diff --git a/seed/tests/test_column_views.py b/seed/tests/test_column_views.py index 0da9fa42c3..f247076d88 100644 --- a/seed/tests/test_column_views.py +++ b/seed/tests/test_column_views.py @@ -180,8 +180,8 @@ def test_rename_column_property(self): for ps in PropertyState.objects.filter(organization=self.org).order_by("pk"): new_data = [{"al1": ps.address_line_1, - "ed": ps.extra_data, - "na": ps.normalized_address}] + "ed": ps.extra_data, + "na": ps.normalized_address}] self.assertListEqual(expected_data, new_data) @@ -224,8 +224,8 @@ def test_rename_column_property_existing(self): for ps in PropertyState.objects.filter(organization=self.org).order_by("pk"): new_data = [{"al1": ps.address_line_1, - "pn": ps.property_name, - "na": ps.normalized_address}] + "pn": ps.property_name, + "na": ps.normalized_address}] self.assertListEqual(expected_data, new_data) @@ -261,8 +261,8 @@ def test_rename_column_taxlot(self): for ps in TaxLotState.objects.filter(organization=self.org).order_by("pk"): new_data = [{"al1": ps.address_line_1, - "ed": ps.extra_data, - "na": ps.normalized_address}] + "ed": ps.extra_data, + "na": ps.normalized_address}] self.assertListEqual(expected_data, new_data) diff --git a/seed/tests/test_columns.py b/seed/tests/test_columns.py index 4ba03a1a37..778748dce8 100644 --- a/seed/tests/test_columns.py +++ b/seed/tests/test_columns.py @@ -801,7 +801,7 @@ def test_db_columns_in_default_columns(self): found = False for def_column in Column.DATABASE_COLUMNS: if column['table_name'] == def_column['table_name'] and \ - column['column_name'] == def_column['column_name']: + column['column_name'] == def_column['column_name']: found = True continue diff --git a/seed/views/columns.py b/seed/views/columns.py index b2844c0503..2af2da034d 100644 --- a/seed/views/columns.py +++ b/seed/views/columns.py @@ -281,10 +281,3 @@ def rename(self, request, pk=None): 'success': True, 'message': result[1] }) - - - - - - - From 5a93e078dfa269145bbcbc7884e83939c2834b84 Mon Sep 17 00:00:00 2001 From: Adrian Lara Date: Mon, 29 Apr 2019 15:55:46 -0600 Subject: [PATCH 04/22] Build out Rename Column Modal in Column Settings page --- .../controllers/column_settings_controller.js | 20 ++++++ .../rename_column_modal_controller.js | 70 +++++++++++++++++++ seed/static/seed/js/seed.js | 1 + .../seed/js/services/columns_service.js | 16 +++++ .../static/seed/partials/column_settings.html | 5 ++ .../seed/partials/rename_column_modal.html | 61 ++++++++++++++++ seed/templates/seed/_scripts.html | 1 + 7 files changed, 174 insertions(+) create mode 100644 seed/static/seed/js/controllers/rename_column_modal_controller.js create mode 100644 seed/static/seed/partials/rename_column_modal.html diff --git a/seed/static/seed/js/controllers/column_settings_controller.js b/seed/static/seed/js/controllers/column_settings_controller.js index 8161d01860..525dda5cf8 100644 --- a/seed/static/seed/js/controllers/column_settings_controller.js +++ b/seed/static/seed/js/controllers/column_settings_controller.js @@ -8,6 +8,7 @@ angular.module('BE.seed.controller.column_settings', []) '$q', '$state', '$stateParams', + '$uibModal', 'Notification', 'columns', 'organization_payload', @@ -25,6 +26,7 @@ angular.module('BE.seed.controller.column_settings', []) $q, $state, $stateParams, + $uibModal, Notification, columns, organization_payload, @@ -130,4 +132,22 @@ angular.module('BE.seed.controller.column_settings', []) }); }; + $scope.open_rename_column_modal = function (column_id, column_name) { + var modalInstance = $uibModal.open({ + templateUrl: urls.static_url + 'seed/partials/rename_column_modal.html', + controller: 'rename_column_modal_controller', + resolve: { + column_id: function () { + return column_id; + }, + column_name: function () { + return column_name; + }, + all_column_names: function() { + return _.map($scope.columns, 'column_name'); + }, + }, + }); + }; + }]); diff --git a/seed/static/seed/js/controllers/rename_column_modal_controller.js b/seed/static/seed/js/controllers/rename_column_modal_controller.js new file mode 100644 index 0000000000..05f3e8e9b7 --- /dev/null +++ b/seed/static/seed/js/controllers/rename_column_modal_controller.js @@ -0,0 +1,70 @@ +/** + * :copyright (c) 2014 - 2019, The Regents of the University of California, through Lawrence Berkeley National Laboratory (subject to receipt of any required approvals from the U.S. Department of Energy) and contributors. All rights reserved. + * :author + */ +angular.module('BE.seed.controller.rename_column_modal', []) + .controller('rename_column_modal_controller', [ + '$scope', + '$state', + '$uibModalInstance', + 'all_column_names', + 'column_id', + 'column_name', + 'columns_service', + 'spinner_utility', + function ( + $scope, + $state, + $uibModalInstance, + all_column_names, + column_id, + column_name, + columns_service, + spinner_utility, + ) { + $scope.step = { + number: 1, + }; + + $scope.current_column_name = column_name; + $scope.all_column_names = all_column_names; + + $scope.column = { + id: column_id, + name: "", + exists: false, + } + + $scope.settings = { + user_acknowledgement: false, + overwrite_preference: false, + } + + $scope.check_name_exists = function () { + $scope.column.exists = _.find($scope.all_column_names, function(col_name) { + return col_name === $scope.column.name; + }); + }; + + $scope.accept_rename = function() { + spinner_utility.show(); + columns_service.rename_column($scope.column.id, $scope.column.name, $scope.settings.overwrite_preference) + .then(function(response) { + $scope.results = { + success: response.data.success, + message: response.data.message, + }; + $scope.step.number = 2; + spinner_utility.hide(); + }); + }; + + $scope.dismiss_and_refresh = function() { + $state.reload(); + $uibModalInstance.close(); + }; + + $scope.cancel = function () { + $uibModalInstance.close(); + }; + }]); diff --git a/seed/static/seed/js/seed.js b/seed/static/seed/js/seed.js index 0fe3da0b1a..745d27e677 100644 --- a/seed/static/seed/js/seed.js +++ b/seed/static/seed/js/seed.js @@ -75,6 +75,7 @@ angular.module('BE.seed.controllers', [ 'BE.seed.controller.pairing', 'BE.seed.controller.pairing_settings', 'BE.seed.controller.profile', + 'BE.seed.controller.rename_column_modal', 'BE.seed.controller.security', 'BE.seed.controller.settings_profile_modal', 'BE.seed.controller.show_populated_columns_modal', diff --git a/seed/static/seed/js/services/columns_service.js b/seed/static/seed/js/services/columns_service.js index 982d75bede..acc71609bd 100644 --- a/seed/static/seed/js/services/columns_service.js +++ b/seed/static/seed/js/services/columns_service.js @@ -23,6 +23,22 @@ angular.module('BE.seed.service.columns', []).factory('columns_service', [ }); }; + columns_service.rename_column = function (column_id, column_name, overwrite_preference) { + return $http.post('/api/v2/columns/' + column_id + '/rename/', { + new_column_name: column_name, + overwrite: overwrite_preference, + }).then(function (response) { + return response + }).catch(function (error_response) { + return { + data: { + success: false, + message: "Unsuccessful! " + error_response.statusText, + } + } + }); + }; + return columns_service; }]); diff --git a/seed/static/seed/partials/column_settings.html b/seed/static/seed/partials/column_settings.html index fb1d55751a..2b98a9e940 100644 --- a/seed/static/seed/partials/column_settings.html +++ b/seed/static/seed/partials/column_settings.html @@ -45,6 +45,7 @@

Modifying Column Settings

Display Name Column Name + Rename Data Type Merge Protection @@ -65,6 +66,7 @@

Modifying Column Settings

+ @@ -76,6 +78,9 @@

Modifying Column Settings

{$:: column.column_name $} extra data + + + diff --git a/seed/static/seed/partials/rename_column_modal.html b/seed/static/seed/partials/rename_column_modal.html new file mode 100644 index 0000000000..9b3ef3fe87 --- /dev/null +++ b/seed/static/seed/partials/rename_column_modal.html @@ -0,0 +1,61 @@ + + + + + diff --git a/seed/templates/seed/_scripts.html b/seed/templates/seed/_scripts.html index 20b964ff6e..b07313c0e3 100644 --- a/seed/templates/seed/_scripts.html +++ b/seed/templates/seed/_scripts.html @@ -63,6 +63,7 @@ + From 172bd41d84c6d7ed4ea2fcf4296d5f29d2ee36d3 Mon Sep 17 00:00:00 2001 From: Adrian Lara Date: Mon, 29 Apr 2019 15:55:59 -0600 Subject: [PATCH 05/22] Update translations for column renaming. --- locale/en_US/LC_MESSAGES/django.mo | Bin 64779 -> 65481 bytes locale/en_US/LC_MESSAGES/django.po | 27 +++++++++++++++++++++++++++ locale/fr_CA/LC_MESSAGES/django.mo | Bin 72396 -> 73161 bytes locale/fr_CA/LC_MESSAGES/django.po | 27 +++++++++++++++++++++++++++ seed/static/seed/locales/en_US.json | 9 +++++++++ seed/static/seed/locales/fr_CA.json | 9 +++++++++ 6 files changed, 72 insertions(+) diff --git a/locale/en_US/LC_MESSAGES/django.mo b/locale/en_US/LC_MESSAGES/django.mo index a787af8b07f799eef5b9a889c185a32cb426ee44..8047b89eaa4402c292d345fd4950e6e59db00057 100644 GIT binary patch delta 17255 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 diff --git a/locale/en_US/LC_MESSAGES/django.po b/locale/en_US/LC_MESSAGES/django.po index 85286e9842..8067856a82 100644 --- a/locale/en_US/LC_MESSAGES/django.po +++ b/locale/en_US/LC_MESSAGES/django.po @@ -58,6 +58,9 @@ msgstr "Accept" msgid "Accept Terms of Service?" msgstr "Accept Terms of Service?" +msgid "Acknowledge" +msgstr "Acknowledge" + msgid "Actions" msgstr "Actions" @@ -333,6 +336,9 @@ msgstr "Column Name" msgid "Column Order/Visibility" msgstr "Column Order/Visibility" +msgid "COLUMN_NAME_EXISTS_WARNING" +msgstr "Warning: Column name already exists." + msgid "Commercial" msgstr "Commercial" @@ -471,6 +477,9 @@ msgstr "Created label {label_name}" msgid "CREATED_NEW_CYCLE" msgstr "Created new Cycle {cycle_name}" +msgid "Current Name" +msgstr "Current Name" + msgid "Current password" msgstr "Current password" @@ -611,6 +620,9 @@ msgstr "Designates whether the user can log into this admin site." msgid "Designates whether this user should be treated as active. Unselect this instead of deleting accounts." msgstr "Designates whether this user should be treated as active. Unselect this instead of deleting accounts." +msgid "Desired Name" +msgstr "Desired Name" + msgid "Determine Time Period from Field:" msgstr "Determine Time Period from Field:" @@ -1002,6 +1014,9 @@ msgstr "Invite a New Member" msgid "Invite an Owner" msgstr "Invite an Owner" +msgid "IRREVERSIBLE_OPERATION_WARNING" +msgstr "This operation is irreversible." + msgid "It is necessary to map your field names to SEED field names. You can select from the list that appears as you start to type, which is based on the Building Energy Data Exchange Specification (BEDES), or you can type in your own name, as well as typing in the field name from the original datafile." msgstr "It is necessary to map your field names to SEED field names. You can select from the list that appears as you start to type, which is based on the Building Energy Data Exchange Specification (BEDES), or you can type in your own name, as well as typing in the field name from the original datafile." @@ -1095,6 +1110,9 @@ msgstr "Log in to SEED Platform" msgid "Logout" msgstr "Logout" +msgid "LONG_OPERATION_WARNING" +msgstr "This operation may take a while." + msgid "Make this a snapshot project." msgstr "Make this a snapshot project." @@ -1289,6 +1307,9 @@ msgstr "New properties" msgid "New tax lots" msgstr "New tax lots" +msgid "NEW_COLUMN_REPLACE_DATA_WARNING" +msgstr "A new column will be created, and/or existing data can be replaced." + msgid "Next" msgstr "Next" @@ -1406,6 +1427,9 @@ msgstr "Organizations I Manage" msgid "Organizations:" msgstr "Organizations:" +msgid "OVERWRITE_COLUMN_DATA_QUESTION" +msgstr "Overwrite data if the column already exists?" + msgid "Owner" msgstr "Owner" @@ -1695,6 +1719,9 @@ msgstr "Remove User" msgid "Removing buildings from project" msgstr "Removing buildings from project" +msgid "RENAME_COLUMN_TITLE" +msgstr "Rename Column" + msgid "Reorder Columns" msgstr "Reorder Columns" diff --git a/locale/fr_CA/LC_MESSAGES/django.mo b/locale/fr_CA/LC_MESSAGES/django.mo index a498ed101f2dd87e3bf9db58529296ec8bf1dca5..367f153f790c11bbc878959fc745850b3755fb7c 100644 GIT binary patch delta 17133 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 diff --git a/locale/fr_CA/LC_MESSAGES/django.po b/locale/fr_CA/LC_MESSAGES/django.po index 54c8d27e76..645528752b 100644 --- a/locale/fr_CA/LC_MESSAGES/django.po +++ b/locale/fr_CA/LC_MESSAGES/django.po @@ -59,6 +59,9 @@ msgstr "Acceptez" msgid "Accept Terms of Service?" msgstr "Accepter les conditions d'utilisation?" +msgid "Acknowledge" +msgstr "Reconnaître" + msgid "Actions" msgstr "Actions" @@ -336,6 +339,9 @@ msgstr "Nom de colonne" msgid "Column Order/Visibility" msgstr "L'Ordre Des Colonnes/Visibilité" +msgid "COLUMN_NAME_EXISTS_WARNING" +msgstr "Attention: le nom de la colonne existe déjà." + msgid "Commercial" msgstr "Commercial" @@ -474,6 +480,9 @@ msgstr "Créé étiquette {label_name}" msgid "CREATED_NEW_CYCLE" msgstr "Nouveau cycle créé avec nom {cycle_name}" +msgid "Current Name" +msgstr "Nom Actuel" + msgid "Current password" msgstr "Mot de passe actuel" @@ -614,6 +623,9 @@ msgstr "Désigne si l'utilisateur peut se connecter à ce site d'administration. msgid "Designates whether this user should be treated as active. Unselect this instead of deleting accounts." msgstr "Indique si cet utilisateur doit être traité comme actif. Désélectionnez ceci au lieu de supprimer des comptes." +msgid "Desired Name" +msgstr "Nom Désiré" + msgid "Determine Time Period from Field:" msgstr "Déterminer la durée du champ:" @@ -1006,6 +1018,9 @@ msgstr "Inviter un nouveau membre" msgid "Invite an Owner" msgstr "Inviter un propriétaire" +msgid "IRREVERSIBLE_OPERATION_WARNING" +msgstr "Cette opération est irréversible." + msgid "It is necessary to map your field names to SEED field names. You can select from the list that appears as you start to type, which is based on the Building Energy Data Exchange Specification (BEDES), or you can type in your own name, as well as typing in the field name from the original datafile." msgstr "Il est nécessaire de mapper vos noms de champs aux noms de champs SEED. Vous pouvez sélectionner dans la liste qui apparaît lorsque vous commencez à taper, qui est basée sur la spécification BEDES (Building Energy Data Exchange Specification) ou vous pouvez taper votre propre nom et saisir le nom du champ dans le fichier de données d'origine." @@ -1100,6 +1115,9 @@ msgstr "Connectez-vous à SEED Platform" msgid "Logout" msgstr "Déconnexion" +msgid "LONG_OPERATION_WARNING" +msgstr "Cette opération peut prendre un certain temps." + msgid "Make this a snapshot project." msgstr "Faire un projet de capture instantanée." @@ -1294,6 +1312,9 @@ msgstr "Nouvelles propriétés" msgid "New tax lots" msgstr "Nouveaux lots d'impôt" +msgid "NEW_COLUMN_REPLACE_DATA_WARNING" +msgstr "Une nouvelle colonne sera créée et/ou les données existantes peuvent être remplacées." + msgid "Next" msgstr "Prochain" @@ -1411,6 +1432,9 @@ msgstr "Organisations que je gère" msgid "Organizations:" msgstr "Organisations:" +msgid "OVERWRITE_COLUMN_DATA_QUESTION" +msgstr "Écraser les données si la colonne existe déjà?" + msgid "Owner" msgstr "Propriétaire" @@ -1700,6 +1724,9 @@ msgstr "Supprimer l'utilisateur" msgid "Removing buildings from project" msgstr "Retrait des bâtiments du projet" +msgid "RENAME_COLUMN_TITLE" +msgstr "Renommer la Colonne" + msgid "Reorder Columns" msgstr "Réorganiser les colonnes" diff --git a/seed/static/seed/locales/en_US.json b/seed/static/seed/locales/en_US.json index cbe2b065d5..7d45880ac8 100644 --- a/seed/static/seed/locales/en_US.json +++ b/seed/static/seed/locales/en_US.json @@ -14,6 +14,7 @@ "About SEED Platform™": "About SEED Platform™", "Accept": "Accept", "Accept Terms of Service?": "Accept Terms of Service?", + "Acknowledge": "Acknowledge", "Actions": "Actions", "active": "active", "Active": "Active", @@ -100,6 +101,7 @@ "Collapse Tabs": "Collapse Tabs", "Column Name": "Column Name", "Column Order\/Visibility": "Column Order\/Visibility", + "COLUMN_NAME_EXISTS_WARNING": "Warning: Column name already exists.", "Commercial": "Commercial", "Complete": "Complete", "COMPLETE_AND_REFRESH": "Complete and Refresh Page", @@ -144,6 +146,7 @@ "created": "created", "CREATED_LABEL_NAMED": "Created label {label_name}", "CREATED_NEW_CYCLE": "Created new Cycle {cycle_name}", + "Current Name": "Current Name", "Current password": "Current password", "Custom ID 1": "Custom ID 1", "Custom ID 1 (Property)": "Custom ID 1 (Property)", @@ -189,6 +192,7 @@ "description": "description", "Designates whether the user can log into this admin site.": "Designates whether the user can log into this admin site.", "Designates whether this user should be treated as active. Unselect this instead of deleting accounts.": "Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + "Desired Name": "Desired Name", "Determine Time Period from Field:": "Determine Time Period from Field:", "Developer": "Developer", "Development Team": "Development Team", @@ -314,6 +318,7 @@ "Invite a new member": "Invite a new member", "Invite a New Member": "Invite a New Member", "Invite an Owner": "Invite an Owner", + "IRREVERSIBLE_OPERATION_WARNING": "This operation is irreversible.", "It is necessary to map your field names to SEED field names. You can select from the list that appears as you start to type, which is based on the Building Energy Data Exchange Specification (BEDES), or you can type in your own name, as well as typing in the field name from the original datafile.": "It is necessary to map your field names to SEED field names. You can select from the list that appears as you start to type, which is based on the Building Energy Data Exchange Specification (BEDES), or you can type in your own name, as well as typing in the field name from the original datafile.", "Jurisdiction Property ID": "Jurisdiction Property ID", "Jurisdiction Tax Lot ID": "Jurisdiction Tax Lot ID", @@ -343,6 +348,7 @@ "Log in": "Log in", "Log in to SEED Platform": "Log in to SEED Platform", "Logout": "Logout", + "LONG_OPERATION_WARNING": "This operation may take a while.", "Make this a snapshot project.": "Make this a snapshot project.", "Manage available cycles.": "Manage available cycles.", "Manage compliance": "Manage compliance", @@ -406,6 +412,7 @@ "New password": "New password", "New properties": "New properties", "New tax lots": "New tax lots", + "NEW_COLUMN_REPLACE_DATA_WARNING": "A new column will be created, and\/or existing data can be replaced.", "Next": "Next", "No": "No", "No action was taken": "No action was taken", @@ -444,6 +451,7 @@ "Organizations I Belong To": "Organizations I Belong To", "Organizations I Manage": "Organizations I Manage", "Organizations:": "Organizations:", + "OVERWRITE_COLUMN_DATA_QUESTION": "Overwrite data if the column already exists?", "Owner": "Owner", "owner": "owner", "Owner Address": "Owner Address", @@ -534,6 +542,7 @@ "Remove inventory and organizations": "Remove inventory and organizations", "Remove User": "Remove User", "Removing buildings from project": "Removing buildings from project", + "RENAME_COLUMN_TITLE": "Rename Column", "Reorder Columns": "Reorder Columns", "Reporting Deadline": "Reporting Deadline", "Reports": "Reports", diff --git a/seed/static/seed/locales/fr_CA.json b/seed/static/seed/locales/fr_CA.json index 4bb54367ab..9a353c5f25 100644 --- a/seed/static/seed/locales/fr_CA.json +++ b/seed/static/seed/locales/fr_CA.json @@ -14,6 +14,7 @@ "About SEED Platform™": "À propos de SEED Platform™", "Accept": "Acceptez", "Accept Terms of Service?": "Accepter les conditions d'utilisation?", + "Acknowledge": "Reconnaître", "Actions": "Actions", "active": "actif", "Active": "actif", @@ -100,6 +101,7 @@ "Collapse Tabs": "Réduire les onglets", "Column Name": "Nom de colonne", "Column Order\/Visibility": "L'Ordre Des Colonnes\/Visibilité", + "COLUMN_NAME_EXISTS_WARNING": "Attention: le nom de la colonne existe déjà.", "Commercial": "Commercial", "Complete": "Complète", "COMPLETE_AND_REFRESH": "Complétez et Actualisez la Page", @@ -144,6 +146,7 @@ "created": "créé", "CREATED_LABEL_NAMED": "Créé étiquette {label_name}", "CREATED_NEW_CYCLE": "Nouveau cycle créé avec nom {cycle_name}", + "Current Name": "Nom Actuel", "Current password": "Mot de passe actuel", "Custom ID 1": "ID personnalisée 1", "Custom ID 1 (Property)": "ID personnalisé 1 (propriété)", @@ -189,6 +192,7 @@ "description": "la description", "Designates whether the user can log into this admin site.": "Désigne si l'utilisateur peut se connecter à ce site d'administration.", "Designates whether this user should be treated as active. Unselect this instead of deleting accounts.": "Indique si cet utilisateur doit être traité comme actif. Désélectionnez ceci au lieu de supprimer des comptes.", + "Desired Name": "Nom Désiré", "Determine Time Period from Field:": "Déterminer la durée du champ:", "Developer": "Développeur", "Development Team": "Équipe de développement", @@ -314,6 +318,7 @@ "Invite a new member": "Inviter un nouveau membre", "Invite a New Member": "Inviter un nouveau membre", "Invite an Owner": "Inviter un propriétaire", + "IRREVERSIBLE_OPERATION_WARNING": "Cette opération est irréversible.", "It is necessary to map your field names to SEED field names. You can select from the list that appears as you start to type, which is based on the Building Energy Data Exchange Specification (BEDES), or you can type in your own name, as well as typing in the field name from the original datafile.": "Il est nécessaire de mapper vos noms de champs aux noms de champs SEED. Vous pouvez sélectionner dans la liste qui apparaît lorsque vous commencez à taper, qui est basée sur la spécification BEDES (Building Energy Data Exchange Specification) ou vous pouvez taper votre propre nom et saisir le nom du champ dans le fichier de données d'origine.", "Jurisdiction Property ID": "Juridiction", "Jurisdiction Tax Lot ID": "ID de lot d'impôt de juridiction", @@ -343,6 +348,7 @@ "Log in": "S'identifier", "Log in to SEED Platform": "Connectez-vous à SEED Platform", "Logout": "Déconnexion", + "LONG_OPERATION_WARNING": "Cette opération peut prendre un certain temps.", "Make this a snapshot project.": "Faire un projet de capture instantanée.", "Manage available cycles.": "Gérer les cycles disponibles.", "Manage compliance": "Gérer la conformité", @@ -406,6 +412,7 @@ "New password": "Nouveau mot de passe", "New properties": "Nouvelles propriétés", "New tax lots": "Nouveaux lots d'impôt", + "NEW_COLUMN_REPLACE_DATA_WARNING": "Une nouvelle colonne sera créée et\/ou les données existantes peuvent être remplacées.", "Next": "Prochain", "No": "Non", "No action was taken": "Aucune action n'a été prise", @@ -444,6 +451,7 @@ "Organizations I Belong To": "Organisations auxquelles j'appartiens", "Organizations I Manage": "Organisations que je gère", "Organizations:": "Organisations:", + "OVERWRITE_COLUMN_DATA_QUESTION": "Écraser les données si la colonne existe déjà?", "Owner": "Propriétaire", "owner": "propriétaire", "Owner Address": "Adresse du propriétaire", @@ -534,6 +542,7 @@ "Remove inventory and organizations": "Supprimer l'inventaire et les organisations", "Remove User": "Supprimer l'utilisateur", "Removing buildings from project": "Retrait des bâtiments du projet", + "RENAME_COLUMN_TITLE": "Renommer la Colonne", "Reorder Columns": "Réorganiser les colonnes", "Reporting Deadline": "Date limite de déclaration", "Reports": "Rapports", From cde1733fb9576d16f80e52658406bf0a381b2898 Mon Sep 17 00:00:00 2001 From: Adrian Lara Date: Tue, 30 Apr 2019 10:13:28 -0600 Subject: [PATCH 06/22] FE test fix --- .../seed/js/controllers/rename_column_modal_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seed/static/seed/js/controllers/rename_column_modal_controller.js b/seed/static/seed/js/controllers/rename_column_modal_controller.js index 05f3e8e9b7..bc8a22ad6d 100644 --- a/seed/static/seed/js/controllers/rename_column_modal_controller.js +++ b/seed/static/seed/js/controllers/rename_column_modal_controller.js @@ -20,7 +20,7 @@ angular.module('BE.seed.controller.rename_column_modal', []) column_id, column_name, columns_service, - spinner_utility, + spinner_utility ) { $scope.step = { number: 1, From 9579d51d5e209c26e294bb165e1ed0b1e9ece173 Mon Sep 17 00:00:00 2001 From: Nicholas Long Date: Tue, 30 Apr 2019 10:53:00 -0600 Subject: [PATCH 07/22] pass org as id --- seed/models/columns.py | 4 ++-- seed/views/columns.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/seed/models/columns.py b/seed/models/columns.py index 2463d4dc89..95e334ca2e 100644 --- a/seed/models/columns.py +++ b/seed/models/columns.py @@ -596,7 +596,7 @@ def rename_column(self, new_column_name, force=False): new_column = Column.objects.filter(table_name=self.table_name, column_name=new_column_name) if len(new_column) > 0: if not force: - return [False, 'New column already exists, pass force=True to overwrite data'] + return [False, 'New column already exists, specify overwrite data if desired'] new_column = new_column.first() @@ -619,7 +619,7 @@ def rename_column(self, new_column_name, force=False): organization=self.organization, table_name=self.table_name, column_name=new_column_name, - display_name=self.display_name, + display_name=new_column_name, is_extra_data=True, unit=self.unit, # unit_pint # Do not import unit_pint since that only works with db fields diff --git a/seed/views/columns.py b/seed/views/columns.py index 2af2da034d..38981fb760 100644 --- a/seed/views/columns.py +++ b/seed/views/columns.py @@ -59,8 +59,8 @@ class ColumnViewSet(OrgValidateMixin, SEEDOrgCreateUpdateModelViewSet): def get_queryset(self): # check if the request is properties or taxlots - org = self.get_organization(self.request, True) - return Column.objects.filter(organization=org) + org_id = self.get_organization(self.request) + return Column.objects.filter(organization_id=org_id) @ajax_request_class def list(self, request): @@ -253,7 +253,7 @@ def add_column_names(self, request): @has_perm_class('can_modify_data') @detail_route(methods=['POST']) def rename(self, request, pk=None): - org = self.get_organization(request, True) + org = self.get_organization(request, return_obj=True) try: column = Column.objects.get(id=pk, organization=org) except Column.DoesNotExist: From 21d49e306d0f0272e3ebd64604b5d6807df83d76 Mon Sep 17 00:00:00 2001 From: Adrian Lara Date: Tue, 30 Apr 2019 11:47:14 -0600 Subject: [PATCH 08/22] Column rename modal visual updates --- seed/static/seed/partials/rename_column_modal.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/seed/static/seed/partials/rename_column_modal.html b/seed/static/seed/partials/rename_column_modal.html index 9b3ef3fe87..fdbfdf6dcc 100644 --- a/seed/static/seed/partials/rename_column_modal.html +++ b/seed/static/seed/partials/rename_column_modal.html @@ -11,24 +11,24 @@

Desired Name

-
COLUMN_NAME_EXISTS_WARNING
+
COLUMN_NAME_EXISTS_WARNING
- OVERWRITE_COLUMN_DATA_QUESTION + OVERWRITE_COLUMN_DATA_QUESTION
-
- +
+
  • IRREVERSIBLE_OPERATION_WARNING
  • NEW_COLUMN_REPLACE_DATA_WARNING
  • LONG_OPERATION_WARNING
- - +
+
Acknowledge - +
From 52650bf53f165092c8028a297f4cf3c8d36474d5 Mon Sep 17 00:00:00 2001 From: Nicholas Long Date: Tue, 30 Apr 2019 11:01:32 -0600 Subject: [PATCH 09/22] org_id in rename method --- seed/views/columns.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/seed/views/columns.py b/seed/views/columns.py index 38981fb760..f82f7d06db 100644 --- a/seed/views/columns.py +++ b/seed/views/columns.py @@ -253,13 +253,13 @@ def add_column_names(self, request): @has_perm_class('can_modify_data') @detail_route(methods=['POST']) def rename(self, request, pk=None): - org = self.get_organization(request, return_obj=True) + org_id = self.get_organization(request) try: - column = Column.objects.get(id=pk, organization=org) + column = Column.objects.get(id=pk, organization_id=org_id) except Column.DoesNotExist: return JsonResponse({ 'success': False, - 'message': 'Cannot find column in org=%s with pk=%s' % (org.pk, pk) + 'message': 'Cannot find column in org=%s with pk=%s' % (org_id, pk) }, status=status.HTTP_404_NOT_FOUND) new_column_name = request.data.get('new_column_name', None) From 77fcd0265d05a9438c55e8cca92db947e239a70c Mon Sep 17 00:00:00 2001 From: Nicholas Long Date: Tue, 30 Apr 2019 12:18:28 -0600 Subject: [PATCH 10/22] fix test --- seed/tests/test_column_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seed/tests/test_column_views.py b/seed/tests/test_column_views.py index f247076d88..70e13c05c7 100644 --- a/seed/tests/test_column_views.py +++ b/seed/tests/test_column_views.py @@ -288,7 +288,7 @@ def test_rename_column_wrong_org(self): ) self.assertEqual(response.status_code, 403) result = response.json() - self.assertEqual(result['detail'], 'Permission denied.') + self.assertEqual(result['detail'], 'No relationship to organization') def test_rename_column_dne(self): # test building list columns From 04571af7a064863d859a4b63029e4aa25ef46835 Mon Sep 17 00:00:00 2001 From: Nicholas Long Date: Tue, 30 Apr 2019 13:12:43 -0600 Subject: [PATCH 11/22] pass organization_id to rename --- seed/static/seed/js/services/columns_service.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/seed/static/seed/js/services/columns_service.js b/seed/static/seed/js/services/columns_service.js index acc71609bd..1a7a037186 100644 --- a/seed/static/seed/js/services/columns_service.js +++ b/seed/static/seed/js/services/columns_service.js @@ -25,8 +25,9 @@ angular.module('BE.seed.service.columns', []).factory('columns_service', [ columns_service.rename_column = function (column_id, column_name, overwrite_preference) { return $http.post('/api/v2/columns/' + column_id + '/rename/', { + organization_id: user_service.get_organization().id, new_column_name: column_name, - overwrite: overwrite_preference, + overwrite: overwrite_preference }).then(function (response) { return response }).catch(function (error_response) { From f65caed779cbe9f21400794f2c65587598734f33 Mon Sep 17 00:00:00 2001 From: Adrian Lara Date: Wed, 1 May 2019 13:27:10 -0600 Subject: [PATCH 12/22] Renaming columns typecast cases are tested for and addressed --- seed/models/columns.py | 158 ++++++++++-------- seed/tests/test_columns.py | 317 +++++++++++++++++++++++++++++++++++++ 2 files changed, 411 insertions(+), 64 deletions(-) diff --git a/seed/models/columns.py b/seed/models/columns.py index 95e334ca2e..b9ccf11594 100644 --- a/seed/models/columns.py +++ b/seed/models/columns.py @@ -13,7 +13,10 @@ from django.apps import apps from django.core.exceptions import ValidationError -from django.db import models +from django.db import ( + models, + transaction, +) from django.db.models import Q from django.db.models.signals import pre_save from django.utils.translation import ugettext_lazy as _ @@ -588,74 +591,101 @@ def rename_column(self, new_column_name, force=False): :param force: boolean force the overwrite of data in the column? :return: """ + from datetime import ( + datetime as datetime_type, + date as date_type, + ) + from django.db.utils import DataError + from pint.errors import DimensionalityError from seed.models.properties import PropertyState from seed.models.tax_lots import TaxLotState, DATA_STATE_MATCHING + from quantityfield import ureg STR_TO_CLASS = {'TaxLotState': TaxLotState, 'PropertyState': PropertyState} - # check if the new_column already exists - new_column = Column.objects.filter(table_name=self.table_name, column_name=new_column_name) - if len(new_column) > 0: - if not force: - return [False, 'New column already exists, specify overwrite data if desired'] - - new_column = new_column.first() - - # update the fields in the new column to match the old columns - # new_column.display_name = self.display_name - # new_column.is_extra_data = self.is_extra_data - new_column.unit = self.unit - new_column.import_file = self.import_file - new_column.shared_field_type = self.shared_field_type - new_column.merge_protection = self.merge_protection - if not new_column.is_extra_data and not self.is_extra_data: - new_column.units_pint = self.units_pint - new_column.save() - - elif len(new_column) == 0: - # There isn't a column yet, so creating a new one - # New column will always have extra data. - # The units and related data are copied over to the new field - new_column = Column.objects.create( - organization=self.organization, - table_name=self.table_name, - column_name=new_column_name, - display_name=new_column_name, - is_extra_data=True, - unit=self.unit, - # unit_pint # Do not import unit_pint since that only works with db fields - import_file=self.import_file, - shared_field_type=self.shared_field_type, - merge_protection=self.merge_protection - ) - - # go through the data and move it to the new field. I'm not sure yet on how long this is - # going to take to run, so we may have to move this to a background task - orig_data = STR_TO_CLASS[self.table_name].objects.filter( - organization=new_column.organization, - data_state=DATA_STATE_MATCHING - ) - if new_column.is_extra_data: - if self.is_extra_data: - for datum in orig_data: - datum.extra_data[new_column.column_name] = datum.extra_data[self.column_name] - del datum.extra_data[self.column_name] - datum.save() + def _serialize_for_extra_data(column_value): + if isinstance(column_value, datetime_type): + return column_value.isoformat() + elif isinstance(column_value, date_type): + return column_value.isoformat() + elif isinstance(column_value, ureg.Quantity): + return column_value.magnitude else: - for datum in orig_data: - datum.extra_data[new_column.column_name] = getattr(datum, self.column_name) - setattr(datum, self.column_name, None) - datum.save() - else: - if self.is_extra_data: - for datum in orig_data: - setattr(datum, new_column.column_name, datum.extra_data[self.column_name]) - del datum.extra_data[self.column_name] - datum.save() - else: - for datum in orig_data: - setattr(datum, new_column.column_name, getattr(datum, self.column_name)) - setattr(datum, self.column_name, None) - datum.save() + return column_value + + if self.table_name == 'Property': + return [False, "This column can't be renamed."] + + try: + with transaction.atomic(): + # check if the new_column already exists + new_column = Column.objects.filter(table_name=self.table_name, column_name=new_column_name) + if len(new_column) > 0: + if not force: + return [False, 'New column already exists, specify overwrite data if desired'] + + new_column = new_column.first() + + # update the fields in the new column to match the old columns + # new_column.display_name = self.display_name + # new_column.is_extra_data = self.is_extra_data + new_column.unit = self.unit + new_column.import_file = self.import_file + new_column.shared_field_type = self.shared_field_type + new_column.merge_protection = self.merge_protection + if not new_column.is_extra_data and not self.is_extra_data: + new_column.units_pint = self.units_pint + new_column.save() + + elif len(new_column) == 0: + # There isn't a column yet, so creating a new one + # New column will always have extra data. + # The units and related data are copied over to the new field + new_column = Column.objects.create( + organization=self.organization, + table_name=self.table_name, + column_name=new_column_name, + display_name=new_column_name, + is_extra_data=True, + unit=self.unit, + # unit_pint # Do not import unit_pint since that only works with db fields + import_file=self.import_file, + shared_field_type=self.shared_field_type, + merge_protection=self.merge_protection + ) + + # go through the data and move it to the new field. I'm not sure yet on how long this is + # going to take to run, so we may have to move this to a background task + orig_data = STR_TO_CLASS[self.table_name].objects.filter( + organization=new_column.organization, + data_state=DATA_STATE_MATCHING + ) + if new_column.is_extra_data: + if self.is_extra_data: + for datum in orig_data: + datum.extra_data[new_column.column_name] = datum.extra_data[self.column_name] + del datum.extra_data[self.column_name] + datum.save() + else: + for datum in orig_data: + column_value = _serialize_for_extra_data(getattr(datum, self.column_name)) + datum.extra_data[new_column.column_name] = column_value + setattr(datum, self.column_name, None) + datum.save() + else: + if self.is_extra_data: + for datum in orig_data: + setattr(datum, new_column.column_name, datum.extra_data[self.column_name]) + del datum.extra_data[self.column_name] + datum.save() + else: + for datum in orig_data: + setattr(datum, new_column.column_name, getattr(datum, self.column_name)) + setattr(datum, self.column_name, None) + datum.save() + except (ValidationError, DataError): + return [False, "The column data aren't formatted properly for the new column due to type constraints (e.g., Datatime, Quanties, etc.)."] + except DimensionalityError: + return [False, "The column data can't be converted to the new column due to conversion contraints (e.g., converting square feet to kBtu etc.)."] # Return true if this operation was successful return [True, 'Successfully renamed column and moved data'] diff --git a/seed/tests/test_columns.py b/seed/tests/test_columns.py index 778748dce8..84eca9e36b 100644 --- a/seed/tests/test_columns.py +++ b/seed/tests/test_columns.py @@ -25,6 +25,7 @@ FakeTaxLotStateFactory, ) from seed.utils.organizations import create_organization +from quantityfield import ureg class TestColumns(TestCase): @@ -391,6 +392,322 @@ def test_rename_column_extra_data_to_field_int_to_int(self): ) self.assertListEqual(results, expected_data) + def test_rename_datetime_field_to_extra_data(self): + expected_data = [] + + new_col_name = 'recent_sale_date_renamed' + + for i in range(0, 5): + date = "2018-04-02T19:53:0{}+00:00".format(i) + state = self.property_state_factory.get_property_state( + data_state=DATA_STATE_MATCHING, + recent_sale_date=date + ) + expected_data.append({new_col_name: state.recent_sale_date}) + + old_column = Column.objects.filter(column_name='recent_sale_date').first() + result = old_column.rename_column(new_col_name) + self.assertTrue(result) + + results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + 'extra_data', flat=True) + ) + + self.assertListEqual(results, expected_data) + + def test_rename_datetime_field_to_another_datetime_field(self): + expected_data = [] + + new_col_name = 'recent_sale_date' + + for i in range(0, 5): + date = "2018-04-02T19:53:0{}+00:00".format(i) + self.property_state_factory.get_property_state( + data_state=DATA_STATE_MATCHING, + generation_date=date + ) + expected_data.append(date) + + old_column = Column.objects.filter(column_name='generation_date').first() + result = old_column.rename_column(new_col_name, force=True) + self.assertTrue(result) + + new_col_results_raw = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + new_col_name, flat=True) + ) + new_col_results = [dt.isoformat() for dt in new_col_results_raw] + self.assertListEqual(new_col_results, expected_data) + + # Check that generation_dates were cleared + for p in PropertyState.objects.all(): + self.assertIsNone(p.generation_date) + + def test_rename_extra_data_field_to_datetime_field_success(self): + expected_data = [] + + new_col_name = 'recent_sale_date' + + for i in range(0, 5): + date = "2018-04-02T19:53:0{}+00:00".format(i) + self.property_state_factory.get_property_state( + data_state=DATA_STATE_MATCHING, + extra_data={self.extra_data_column.column_name: date} + ) + expected_data.append(date) + + result = self.extra_data_column.rename_column(new_col_name, force=True) + self.assertTrue(result) + + raw_results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + new_col_name, flat=True) + ) + + results = [dt.isoformat() for dt in raw_results] + + self.assertListEqual(results, expected_data) + + def test_rename_extra_data_field_to_datetime_field_unsuccessful(self): + expected_data = [] + original_column_count = Column.objects.count() + + new_col_name = 'recent_sale_date' + + for i in range(9, 11): # range is purposely set to cause errors in the date format but not immediately + date = "2018-04-02T19:53:0{}+00:00".format(i) + self.property_state_factory.get_property_state( + data_state=DATA_STATE_MATCHING, + extra_data={self.extra_data_column.column_name: date} + ) + expected_data.append(date) + + result = self.extra_data_column.rename_column(new_col_name, force=True) + self.assertEqual(result, [False, "The column data aren't formatted properly for the new column due to type constraints (e.g., Datatime, Quanties, etc.)."]) + + new_column_count = Column.objects.count() + self.assertEqual(original_column_count, new_column_count) + + # Check that none of the PropertyStates were updated. + for p in PropertyState.objects.all(): + self.assertIsNone(p.recent_sale_date) + + def test_rename_date_field_to_extra_data(self): + expected_data = [] + + new_col_name = 'year_ending_renamed' + + for i in range(1, 5): + date = "2018-04-0{}".format(i) + state = self.property_state_factory.get_property_state( + data_state=DATA_STATE_MATCHING, + year_ending=date + ) + expected_data.append({new_col_name: state.year_ending}) + + old_column = Column.objects.filter(column_name='year_ending').first() + result = old_column.rename_column(new_col_name) + self.assertTrue(result) + + results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + 'extra_data', flat=True) + ) + + self.assertListEqual(results, expected_data) + + def test_rename_extra_data_field_to_date_field_success(self): + expected_data = [] + + new_col_name = 'year_ending' + + for i in range(1, 5): + date = "2018-04-0{}".format(i) + self.property_state_factory.get_property_state( + data_state=DATA_STATE_MATCHING, + extra_data={self.extra_data_column.column_name: date} + ) + expected_data.append(date) + + result = self.extra_data_column.rename_column(new_col_name, force=True) + self.assertTrue(result) + + raw_results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + new_col_name, flat=True) + ) + + results = [dt.isoformat() for dt in raw_results] + + self.assertListEqual(results, expected_data) + + def test_rename_extra_data_field_to_date_field_unsuccessful(self): + expected_data = [] + original_column_count = Column.objects.count() + + new_col_name = 'year_ending' + + for i in range(9, 11): # range is purposely set to cause errors in the date format but not immediately + date = "2018-04-0{}".format(i) + self.property_state_factory.get_property_state( + data_state=DATA_STATE_MATCHING, + extra_data={self.extra_data_column.column_name: date} + ) + expected_data.append(date) + + result = self.extra_data_column.rename_column(new_col_name, force=True) + self.assertEqual(result, [False, "The column data aren't formatted properly for the new column due to type constraints (e.g., Datatime, Quanties, etc.)."]) + + new_column_count = Column.objects.count() + self.assertEqual(original_column_count, new_column_count) + + # Check that none of the PropertyStates were updated. + for p in PropertyState.objects.all(): + self.assertIsNone(p.recent_sale_date) + + def test_rename_quantity_field_to_extra_data(self): + expected_data = [] + + new_col_name = 'gross_floor_area_renamed' + + for i in range(1, 5): + area = i * 100.5 + self.property_state_factory.get_property_state( + data_state=DATA_STATE_MATCHING, + gross_floor_area=area + ) + expected_data.append({new_col_name: area}) + + old_column = Column.objects.filter(column_name='gross_floor_area').first() + result = old_column.rename_column(new_col_name) + self.assertTrue(result) + + results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + 'extra_data', flat=True) + ) + + self.assertListEqual(results, expected_data) + + def test_rename_extra_data_field_to_quantity_field_success(self): + expected_data = [] + + new_col_name = 'gross_floor_area' + + for i in range(1, 5): + area = i * 100.5 + self.property_state_factory.get_property_state( + data_state=DATA_STATE_MATCHING, + extra_data={self.extra_data_column.column_name: area} + ) + expected_data.append(ureg.Quantity(area, "foot ** 2")) + + result = self.extra_data_column.rename_column(new_col_name, force=True) + self.assertTrue(result) + + results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + 'gross_floor_area', flat=True) + ) + + self.assertListEqual(results, expected_data) + + def test_rename_extra_data_field_to_quantity_field_unsuccessful(self): + expected_data = [] + original_column_count = Column.objects.count() + + new_col_name = 'gross_floor_area' + + for i in range(0, 2): + # add a valid and invalid area + area = (100 if i == 0 else "not a number") + state = self.property_state_factory.get_property_state( + data_state=DATA_STATE_MATCHING, + extra_data={self.extra_data_column.column_name: area} + ) + # Capture default gross_floor_areas + expected_data.append(ureg.Quantity(state.gross_floor_area, "foot ** 2")) + + result = self.extra_data_column.rename_column(new_col_name, force=True) + self.assertEqual(result, [False, "The column data aren't formatted properly for the new column due to type constraints (e.g., Datatime, Quanties, etc.)."]) + + new_column_count = Column.objects.count() + self.assertEqual(original_column_count, new_column_count) + + # check that the states' gross_floor_area values were unchanged + results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + 'gross_floor_area', flat=True) + ) + + self.assertListEqual(results, expected_data) + + def test_rename_quantity_field_to_another_quantity_field_success(self): + expected_data = [] + + new_col_name = 'occupied_floor_area' + + for i in range(1, 5): + area = i * 100.5 + self.property_state_factory.get_property_state( + data_state=DATA_STATE_MATCHING, + gross_floor_area=area + ) + # Capture the magnitude with default occupied_floor_area units + expected_data.append(ureg.Quantity(area, "foot ** 2")) + + old_column = Column.objects.filter(column_name='gross_floor_area').first() + result = old_column.rename_column(new_col_name, force=True) + self.assertTrue(result) + + results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + new_col_name, flat=True) + ) + + self.assertListEqual(results, expected_data) + + # Check that gross_floor_areas were cleared + for p in PropertyState.objects.all(): + self.assertIsNone(p.gross_floor_area) + + def test_rename_quantity_field_to_another_quantity_field_unsuccessful(self): + # This should be unsuccessful because conversions don't exist between certain column units + expected_data = [] + original_column_count = Column.objects.count() + + new_col_name = 'site_eui' + + for i in range(1, 5): + area = i * 100.5 + self.property_state_factory.get_property_state( + data_state=DATA_STATE_MATCHING, + gross_floor_area=area + ) + # Capture these pre-rename-attempt values + expected_data.append(ureg.Quantity(area, "foot ** 2")) + + old_column = Column.objects.filter(column_name='gross_floor_area').first() + result = old_column.rename_column(new_col_name, force=True) + self.assertEqual(result, [False, "The column data can't be converted to the new column due to conversion contraints (e.g., converting square feet to kBtu etc.)."]) + + new_column_count = Column.objects.count() + self.assertEqual(original_column_count, new_column_count) + + # check that the states' gross_floor_area values were unchanged + results = list( + PropertyState.objects.filter(organization=self.org).order_by('id').values_list( + 'gross_floor_area', flat=True) + ) + + self.assertListEqual(results, expected_data) + + def test_rename_property_campus_field_unsuccessful(self): + old_column = Column.objects.filter(column_name='campus').first() + result = old_column.rename_column("new_col_name", force=True) + self.assertEqual(result, [False, "This column can't be renamed."]) + class TestColumnMapping(TestCase): """Test ColumnMapping utility methods.""" From 02eb2c60c7f8235b1000f4aa81241d5fdb7af9c3 Mon Sep 17 00:00:00 2001 From: Adrian Lara Date: Wed, 1 May 2019 14:07:43 -0600 Subject: [PATCH 13/22] Add column rename warning about default Quantity units settings --- locale/en_US/LC_MESSAGES/django.mo | Bin 65481 -> 65606 bytes locale/en_US/LC_MESSAGES/django.po | 3 +++ locale/fr_CA/LC_MESSAGES/django.mo | Bin 73161 -> 73318 bytes locale/fr_CA/LC_MESSAGES/django.po | 3 +++ seed/static/seed/locales/en_US.json | 1 + seed/static/seed/locales/fr_CA.json | 1 + .../seed/partials/rename_column_modal.html | 1 + 7 files changed, 9 insertions(+) diff --git a/locale/en_US/LC_MESSAGES/django.mo b/locale/en_US/LC_MESSAGES/django.mo index 8047b89eaa4402c292d345fd4950e6e59db00057..16d814b89355d7db6f96e6a72a507fb3ba5389f5 100644 GIT binary patch delta 16806 zcmZwO1$b4*zK7ujfdCcYGsX4?K@y{9Dtg@2+V+UQ4^2FYd#+R56Q&)DJFf5I&F%p~OF-(G4s}QwBj*|Ti%A6;6coeS8VxjOh(z?Rm(KUhzeC<)XXcO8Z<^lq&4br4M4SvvGSn! z{V%A!y^b2-sV#p-O~9wNSx8>gbK$7=5BFK8``$@Tu%BBKWyp;p)iwSrFQgM(3#nTQ&2 zi7ju(5|mG31SYL#7SI*-i8>I~?>zLy6{s_{9W~+o=+exNl2OMeF%0inQ`I-2FNw9N zua3Nl&TMR^cBqJzY+weeirTVzmN3#)LxgiB*=dJ*t{Y~>VW^Mbr5KEHm=^D$J^}x9k9W2Abw5R zHI0lqJc3%;71RWNwZ1_=%75cB^kL)w#I2|auHlvDULj zpdv5`b92_BF_3a>TPErvbBc^A9%3+lLcQ;q+nGHJN3F0f>a27|?R7WQzync-aXe~E zrl2M`AJuLp>iNyK{uk8Sbsk-s*^3 zq1=z^_y%faPcR3@qs~(1PG;g|Pz!8~dYd9qE3%HMI^!~pjqZvBinbYi# z3UO}K-WEgcbv;ywyI@irfND1kb!H}^_I3e!;TqJ0V^Ldr1he90RQtcskMSL^uJ(OL z4G@lc;2YGy^-(K|M7@R+P!U;z`ea*eJ#6dmVlwI#}rX!96xkvz)&(8 zs33Y{Rn*EGVqNTxCGj8@!w=XH3wLvzvN#@f|0wptMBVu+#y+Ti7oa{TVo|U6anxCR z(w+EgMyYz3El7_FS#I>kQm8$whFVce^udm(i1bCZpMh$(67}3}RKG{j3(ukt-ax&^ zPf=&cqbKoK!@!;O-ja{UK_gB)!c5X;G2O ziCSm@)Zs0TYUe6TMk}d}>bMnZuOd+cy3h}2pxUiMg>*Zrqd4nP>nZC6>ow~gRR0fA z5qyP2!sUD>Q-BJeKK#6w$Z^V{``fP{=YaZf)J$8ULfH*<>c^wDW;rSX`%n?Mi;Bz_ z)R#=={-(d;7)rS|>Vv2!dh7k~N5+qeQK*^CvM#svTQCjvKchl;1r^e}_Wl#p+wcLW zVx0kwGg4R#x7e!xjT$b8V%KrLhdreu64hD zKihiW!REOPs8HuXZCPQ|!YZIXKkA^y>5MLQJcx`!Itnx3B2+|nVGBHoI;1&=n7z-B z3ULX{h)qxv?t?nrBT!owW9yfq`rU|H=rPm;?hGORn&B%dw4yJl2_+k9Cg6)2C@boI z1=LEaqn>MmT2Uv|p&X8ycr>b?1*rBbF+IkjCU^qN;mx7MUxy*%F!K=^f+Z>ELG57# zs^hoVlEe8K(_ssqt4V#Y5oUn>){~f*Z=vh>o5LG2nqM^G@5tATb6|}5ZCI|c=2tgA zxX9>KzO!Z+$8WwUmqP8~NYu(FV;P)f%NMZ{<(sHbhm1EXD~OpXS3rfh4JsnNupo{> zZN+xfC!p)BZSWAaH}6mbrTw>jgn$QK* zfUhtZlSaE=PnVOOjP|5FYR2_Y4LYDY8jV`v0@PM*KyAqu)S=sF%g0guTtc;fg!%AK zOpaM6@>zjlm<-RM`~AO0Mh`whJs6K#ndc;P>N8*_$|0x+%b>o;D`RntL>;bWs0r`J zFuZ{I@e3+)`M)>g)WE!yTVOuUku#o*CvHd0bRT-*5!9BPv-cmPBJjfAe}gG0e?l!F z1t-=UgU|zWqMpx-3Vj7zZj6&S#8K!f&5id{%;73B)vT-!W}|)%R>XZ+2c2o=Evbv` zC^koJ)jrgry@k0jON?1iP1Ho{qaxQBQ(gcL1Kf)B0U!(f@Xzx4IO+-_pzRUtpk!Aj0 zkJ`h&s6#dp{c$O3qT5kt<^(F#7f}OUMMdxyYC_M^58q=LrkrWoxr&oDP2TxU7-wFLG_p|q-t+TNQ^~*5?{pWDf@oUs;+aFuw3~YgKP?4(rgCflS*C(SD zbwtfH3e#h6)QY1q5T~Icv=-HVJL>*^REIyKB5(|KHm;(!?mlYb?@-UDoNFQzfI)iy zbCJ;pL`8Im9Q8mHYHvqiAVy;bT!uOu`!E}xK|TK#H36S_rrlSl=Zf2MJzMUITId*b zMUk0EM(^=E%!dK<%^|6b3SDn(j^|N(9=3q5U_L+!p|)tqLbK9&sK{-`p7;w6#Nvz0 zLbjmJ)*cMUV~dEtI{cdotvu0UV`|i)%4o}Zt;JF8E29Rgfm(4p)M4w13jIh_WF}z- z+>F`r1h&Oj7=-nfxXcQomYBVqWSx#WL~~IC-9+u>8`Q*-FEy`UAnG*dLmlGsm>Fwg z0gObopNpF4TGZC=LPg?~i;Px&1GOhl?2Q+w!}1XWFxfIQKrre|n!pxJi*fe8>oOS~lG~_I#iJVfE;k(nqfU8F)I{^6R#XZzVog;0&Zw33 zM@?uVYM`a4i0nWe+Do?l80m-oUtvO#7WH5j)IcRr4^%GAjRKK@STlf|g=@cs!QTE@5j8+hYT4`=;L2O337`DV1)VJG9)ZqzO#b*Z= zM*4KdViL@=+Dy0ry3YhEVl`0{Y=N417fi+Y&KP@Rx^*$?P;Eek@E~f>FQE?2ZPY1$ zX8jX2@E3bO$r{tn5A|FKs(o(M0>e>}EQ#*l|J5d=(6mMk*w>b$u>|GC7=hPOD=4?t z{Loqp)p2*!77fH?7>&s>1~svHsD2k?7;dxPUrYQI`qb;p9~d%Yb;{qNLca|)VCwZ| zpp586IXmiX6hv*sH>i-dLJu5-n#eHJ1jb?toQO$rCMuHi))RkCU=0-t%}#sc0BS|2 zP#xVyh3pY#!oO@e{RY!654zt9biWlCPP@*izc0*34ZPw<6X|uR_HizoIfokH9%^NO zpeE$G(F~9ZQ&Rp4^hh^Xc)#(F0$D~CLT3#nk^=B0jNl1L2q=0k}Cx;O!=~6Hx;$K^@*LsI7R0 z`4jU+x5IoM)ZV4YG4%*CX=yM9x$n%yNw@&ZVXocg--KOIr+PX5gd1=U_Sj>-<&y0s zN|Z-nF#7Mazretplv`n89E*i;7hY26o{~|=2lpF~p+a;XwbGlI8egE!$lq4q1E!n} z6@lWYKQL6oK+3%jnyr|GM<_4H7>tZ#+wdd0CX7yARtG1I;`YD-t34(o1gibpUres$bLuJm!@uTa&c zLNkm+&2R{6=HpO%HygF01J+ZhiQhm)>^|zyJwa{Nd(`vEPMCiw1)vW3AXK~WF*z=D zkx|1{sEO=A4RjV2(%Yy={DB^r`lR_l@)(Mf5ynUauNhfbwwEDc@+z_wfbgG^aUF z_yzTm-0zJ0M>Ll+n2a|Si%|n?u;sm|hR2YtcTS@wcnVE;YID1_0d5KTiJo@JZysBg0>*7ml3IBF}# zq9!&Ob%qw9Lcazz&=zYPYJq1_Tm2YysNY^9{&mTux@e@ zf7AnoQSBlcN#i3Su40U+VquO0XE#wKR-+0tkCFYf4AucD7j5-QK zH7th;X>C+T&8%!SBOUe*Dq0fwR8nrKuc=3oI_gZlfzHPpm*+|~ye`+t;-D!lFx zZVbW_SQm@oMC^h;VFc#AYksd6gZf~JL#^lqhN8zk^Ffpybyo7A7FY~5u^QH<=>Gd( zCo(!@gHR!ygbL|Q)PoC9Z^I^>ib?L9Urx`&HI%db%X|c%L`~=mX27rqCL&eP{dS4KWrV05WsJQ)o<*SZWff%T~Sv9|sQ>cKOpP~SxN-lA3(k2>W^9-8Meq592_8L=2@ zVhvCW>-LcNw;(fu3Z2qhsJ(xHI(#ot4|+c`1Ls2RX%WiQ6LGQJJRDs6R0H{>DETX^`Wo8KAdy1nLhAQ;;Ukp6BKd z3@cvn!wcm;FU@bmZewf8-~Mh6>JKy5(@YXejSyJ0>Yit1+_YT!M{S#dc>$>gNs4pu|YH|D*sg9>#c zREWEvI_!`7#2jl~gqqM!)JN`R492GzgkEpW#Is`>%0*H4t6`wt|1M-SvyrI1ia{@& zjXLd1ZFwUq0=rS|PoP$I19cXX|G^IzmMJUPYi*&O2S9JMgdPv*IlsIBn(MEvz&7Aj;O)QStEIx1_+wNMjlit4Dnz26zN_x;cxhod4l z9ktShsP>02DIP^d>J(}L_g!Q((66Y|8IKBa^3P_#bf^x4QT2IH6D($}h~bp$pavR^ zn#csy+p+*P(0bGawxYIj7iv7$aWdM&o0uM7qdG|b#mqDlYK4VRp)QFUs4Oak6;Tsv zfI8J}P=~WOs@-B#WLKctZ9pw}7qTGUe==I(Mbu31pdNUH>fi-xpf{)$C3X_I>(iiC zk{-3PoYs7(iG(9((0N7T#rltHGB>|U{2e9z(_0bJRfp$vWuh)Go_3f1_dor(?{32Q&Quz;Ck-M$2TPGka`yx&2X*!B zbFHv8>38b&lZ>v+*oyQM`8Om*N>^%oUwQok>KJJx`6jmhck(}ycLnetPf{SAJ|HzC z>FR^4@v1GWZY%j8NXtmaRl)U?be`u2l4g;zyKBvV+Cc}>Q|cFE1D@MR`irFN8|{BT zlXq@lCmNn5RV6=+)RBhI?R~L`dyS~;N`5a^q^wiCm(+!PL(*yLb&VviE34HTpV@nV zkbh0el$iK`Pvs@ss41@DA$=SVuyy6EdGQb0u5)*5{xgEQ61G!q{zKbVcm1hvg5&J{ z{p2f?KS^3cdP3VCE;1EKJ-9iYiW%f@{G*elHouYb2g+lx5vd;aZ&B9> zQZDZKlOB+CePy59O*xOPTTGp6zAaRv(Ogm*%AN2bshoW%6?MaHy9?9@*?O|>e}X4w z<-c-&4&^D_yH9=-Nw2G}Jk-@9#Z%Xo@-keA7xnkQN!&O~A>q=ypMts}wxU07CRHIV zp+4bCPX1q{qjaX9nD|a{Uo%xl+D>Xo(iOq|zU1>^IJP03myGYeLileU(soh|={Psj zlJ<~vh4El}98Wo;yM}K&d#?a~&HZ0Ux^hy!Z?aB%@_nh_iC3r_fFCK3BE2D9cJUwm zR#KO~VE&@qpY#{`Z*e7#;r@J5Rom_r^)Ja^$2+!eE>@)Naq9M9!gZ5O66zAJi?;ni zoK3yHf819++h_@;rKEJEJEVjw5qs=s+l<5S=-@Ny3n>fb={SNkgM10nR#Fbi|GYL( z7)pIDOvb&HiHZM@WU3@IX(upk`7T$%5ynLb<}|CB>6rj>;CT<%CpJMqJ0NaMpD8xmV55=zl}^VwH+}r z4LnGhC?`cvQa0*!O(egTd{5E>TUVI6CZu1ue-GP{bPdFwHec3yjc1D4d^#+n_kTT= z*LdKR?eJgZvvD&T{cPiw2l>e3G<`>8+SF7fu*XH@+AANkJYQpSwXcVZCW47)R{ziIb%TX>eQ8XHhQ?RDJ(SZCc`*6Ii`+;(LI_+a8P{?|X`-i!=%eMdj zw4~j0+Kf-QL4F?j79>{;H>**xmO?(#Ey_hn*U9TDO!}MhCz7twv|UcROj=L*7^yA! z>fHa0^gU@YsW^4_xOaqn9Qkue`VeX#ZDp;{lcTF%~y+qZujT3LrCyG&G?C zo06K5w$RRp=Vsytd_i3~Qd-IdNF_ z>gRtSGA~GXNpom87jL7kZ%IL<-jvsovXH(dH6%4A-J`9pmDWep=~{%3sVjr?NH=Z$ zZtnS$W{^5?|AWG(>uVg0`)x<6iz1(rw19isr}eh&d#f+Lrv5!`5|PhBzA{yY|sOGdEl>$VA#ksmZ@2f6kUq(V#o|(Kwt`l=~U%z2`WUvaWih1C%S1x{;43 z^&)?P`{l{&dPn(B%DJwKYdb2BU2Hx#4yKW=H{{oluVL?*T2@bakP`Pg zCsblEb>4VE&tPlZP70v@0OiEwm*Ggg|GmhBl8RHQD;j%~CX=7iuf~0e_Nzc8K_=xfjOt_wqX~Dg9)Yl^YOu4N-{}Zkw zHZzSz!88ab>AHfMsY`2$P7HZn*|8G$24ic=Kah_kzlr<;(kha!y|$m<$gkr57+co8 zn~AdUj|DOlRN{JUa0=7t5RHeCVxP@wnke>Og(g*k8dR@cwN0hURVp;9)v!&Y+SMC2 zXw#%Z{o2&Vo`1b7aqNqUk2+PW(xZ1shaOS=x^?dpG9a>VmymwlBm0K*iRjxmvU}$~ sVIdtOI<@N;)z@9E7Ew_l{rW_74C&;4u6zH8-hCo_bnla&F5jp6A2-H6utzujo;#@?G6MbOe((HfuZR+Q)S%{g8#&wV||`z%I{pa80**o0h!22|=O939!94^l^j*}g0g*Z++ z569_QLbZ+)U)^!i<2cNSiRg<{#QGj%Df`xNoLo2*`JXe7e`x1& zE|UqT;y$XwjGs6Tb8(8IKSp9YtcjjjAA>L&(_%Md&Q2`)U_7dQqP;&C)!!1-L|33U zZow>!@9ZL@nV&*+a2NgX1%_g}nvRnVbE8&P71h2zdSM6D1iE2X9D$nn9E`xVsOOKN zCh!o|?mrA+d?&b;sVIT!s2*yiUtmxC3UlHqEQ&9%2u9R)oc0)vt#LhipidpgiE?8R zsED?%tCc#=AXMZgVIN$Mt~fHmpPH3S#tf8aVJTdJ>hL^jz zk3bDn5Vhj!sE9T~Z|sPQZ1*VQpP9@UTQM6Ifz7B8A4Y}p8fvefS>GVX#c{7^2C9Wx zVH?!M`l9+BfdM!j)8i@(!%bKmkJTgodceKDnP~uO;BZtV%Ai(W6SXCcZGCg}q1+jR zu@7p1c=W;_Yn2z!rm#s+Cz)T<%HS>a~231iJse?K+9Z(@1WbenL zA~P9%aRzEZOHqez6RQ1D)WR;KCiE0Fjw^jb6Ox>$P*<_#MyQT@VFnzAdN2WfaWSg> zPpG}!jT+#XEvKL+@DjCI-FHe6R3&l_5L>_lZlEBsFlT{Ivk1_aHg$aZcRcR zs$HlM9!Kr@ZB+Zm=#B5K-i^(`fvEc-)_mxt_rEk5Jx~R;!n&vxe1@6ub5vydp$3e% z<#||vauRmI2dD)!jW(aC9Z~&`L_Zvl+KPFo2`@vJLb8sGI!?kOxZnC5Gg8jo#Bmy6 zB=Y7t-(oAZLk*a_sTrs^YRf91Ki0y0*xWh{(@8MXI2QK9++ zHG$)($Xv7acTg*OiR#F!nTePm>P$q~at&0w7*r&?qPBJ*mcpsch`$#fm3yhsz(-|!<@JhGvj{LC*VaF84dgj z6}oq*NO-p}p$$a6_qk9VS40h15A{jb2G!9Z)boj`i7dciT#Y(odr*<7+0xW^#L|>q zab(otTGXEHKuzGN^&AFJzJZJJB|gX5902Y8WPb8!zNtU~qkJC4FrsEN03V)blfK{ZFX3Yb&}mvwdW=lGCUM?w~q+W_^PSsr%=qJ`@%DaMVOAS));B zjYH$g_s?;q0Y+b&xyYd&l4(g;5!V%urEv~E1@FO2sOcOs0j{6O?(V$>!zVr zwB33TwScpzh+RP)x)fBzUZS2)+nM;YI47vHnRy(lVFGH;=b{=eLrr8eYM{fYkX}Ho z>?x+f3|-6zl0Ry~Sy2-yjM|!#s0CNE)_0N7Cs+&A107HU_C<9#0=1W8Q0*q6CNKjP z%4MjIlTj->g8A?g>MVJ7H51Q^+M24Uw<#Jmo~tXF++^Y~FU~;^+>H^q7d5~=)Bvw+ zIo+40U1oHrJ}YX1*->vn1eV3}sDWcq{f z{@u;q=0xpv1ylqYqbAq^)vgQb%zTa7+tHXFC!;1j2eqYZF%ND>wZDo1jPE=llls2* zFau;mJx~ZWa7EOLqEWA5A5=u*QJ-v+tgCJPK1@gbVbnmU&>L@|7d}QUUtjE8+?)hu5$fhV~-d7>l~U4*TLAEP!o#n|?>57B~m>dT&6TwIjWW zzh?BD3T?qVRLJ~e%|!B`_OJwMMYS*!Hb6zBEvo$xRJ#f2i;GbGuEX^BGiJhM)N6bU zb%yT65`Q(+e-A0N-k1@?Q4f?rJy0FB6|FD>_CdA#7X5I7t)GvY$QtV|R3uNJ`g@3e z_ztz;OfKT3z08fOD2QrM+158i4crXXaS!xCJ*QA6picEF)cbx2HPC%je{WHd%hb;- zG#GVwbD`R~@{!R>%Az{1joPbd)PP@N01iR5n}`bOJXA+3t?R6ttlO-+tb0-YA3{a& z46;y{lR~CA6)*9k8}IVh_H7uTpU<3d)J$umLK%ZP^|7d}NkBzlDJlZ{P?5QX`XS>v z(DauJ!zq_VeGs)oU%mhB$OKT)9W}FI)&yHW3;n5IgE}iaP$AuC?;kKIW8%ygoOf7+a>+qvAsx_%@tr|rvfvohKyy&1c&#mOLk)b$dJ;8(OZI+>t$&4j z&TX&>bw<>dg`yT#5Ov7Qp~h*1E_K|Aj6&KSv*K7(L>6KjOhO&fOhe4x2cbe7j@hsp zYT!1g!`ltDWrJ+}I8?vWQ43vC>KP9xGpLpt*`|4L~X@9)F+_p zXWQTqYHu#127YeKZt>=$HW)prFN^B1GAguq(!B?0L=YDUV zUy6Qu|2L4)gUP6s9mBME6SL!e)Pvp$=I6IRmZw|>b*OryCOig<;Cw8G2QeGIMolni zJZA&*qbArC(=xs@f{bPwkLhs=YAfcd0d7P^V5_~q1AQnTKy`c)eK7^~o$)E^`Io5B zXW%5OoEsB3yydYn<=-c=|2kA|lg!GhVFcwbuqMW1BRquOn0>POwww>ORq?39x)KZE zL)3(Wrw&!ssc)Un}cDg&ypKneZFbgWp*vp;kNt)zK1L-hi6e zc2qz6?EOQih@M0J5W9|w>`Tmp?o&W1{LC7r~wC~ zIvj25C!r=d*SZo*QQm}_*lpBA9-!V9w;#+nel9YaKrs4Y7;3;`s6DHWI%Lf;5PP8} zIs&!A>8L$lfEs8CDuOFf6Z!=M@OLbNr%~-Z5>13%-elC!AGP8z)JhAZR#+Ytk(#J2 zsfX&I32LAgs1j$D1G6c1-@z$xRiOj_69KzK||1RhLG}FOL)WB({n-82oRL3PT z5Nn|B$5=ndSjw@O3om00bY_^>_7iMRu>-cj9jHi!&NLCuf$8-Amms5=md7Bhidu0D zhG1J%ga)7@FamXd9IC^~sEH<`&c+hd)~!WNd=KjR)2PT?M|}c5!)$v0GtIIgM?Fv; zwYLp01YIk( zK>39(vzINX(4MwO9ilF%fmWdQatCT+ConVKK%M4)QE!RwBJiRbgKFOeHPHd6 ztsRYu#7xwJm%GSlPm=77t*FDY4}?pu350Tscy zsD4+Xws0pZ(kD?7J&zgn{-==9N}pTbVk^pNmYH8jw8H|Fx1kQtb!>v~kfu)ba&tIe zpeFnVQ_sW-v(jMH1oNXNUK(`<8mpf1o%Xgt57eQGLxnH_wdV^_53E9+@}I4{(S!0q zd;h5Q66(48sP@lM3w)1?WZIQxVxj0#XbO?hfYnju7_5LjunR6jt-xoM`LY^<>bN3m z#kDXU#-JCrLrttJs^1=11czJKqR!5_Rm8smnLAWOVc8!|=!c^QJcr(R3)ACc)M0vy z+KP;;O-Ku38p^d%6Ny4iAR6_2bM(ZHs7Q82O`!j3`~HuzH@-)$XeO$oRj80{Kz$ed z! ztD{2P9@W7h)I>(3KA0w>4)01-WD0CF^|i4y<#wq4rlLPCK}}$@bteX>ivwi%Y;i8& zb95z{84lQF1{{gu)K5fpv>r!cGAbfhmD<7ZbTi z%&a9eCc}NF3npMUtcK69B9`82PIW9MQ;tKvOE{IbnV)jUaVq5oznX8wm$4w__n03G z{$~EJ5RH+PN8<_Hf~kN1OW1A(NJNEb9%`j4P^WY&>Wu8SUahour~uWMiiY=An9tx<=j6KaCJQSIVT zAsuDwXQQ55ikjF))IyR`?N6ZkyKGHKCjL514{U|!9&;*vQK8DGvOB-4!EjdCkLBE@ zK5?&Ek!zp*!m{7AuZo$u-v@PA$6!mGf?;?U6-loHCQ{iCxXcX8P@x&tMa{ekYVSTr zt>}B}Owa#l$*?MOdYJzi7d$$zJ;xDMsK0>v3KVm|g4s~|?un2yFZ7~+LWruJi z9>cVH|6`7t5Vb`ep5CaAN8x##j+#inzfFe&P!m~<8fXjV#@on>9G_#RzYtVEg)j?N zK}EPJYHK@T5aT;T$YjK+s2MIoHC%yyxEXzLFQ&(1=#LjM6Fx$n?)Rt#gdH~nmqZO* z8`XasRQv82frHUii_8Kt+S?nbL*>KAQ#K5+7DLt7Lv2MgYGSQWhp0R1RQE>>G|W04 zb(rU%BC`>-@}1Zi&z>Ou`iL!b()?ke3pS%X53Ax6)cwd)<_`-!u>j>usE*T}HY?15 zia=@1i%n1yio*aLfr?lnY9h-q5R*<5f30XA6$;G}%z>9t54^Jt{mz&NBTyYjqPC(U z>bcq&f-#sGd!yQo!eAVaip)|}`y>p(JuWg@$pzG&KSeckJ8MGhhpG=p&A7NN*Tk%p zqfs69KyAqo)QZQWwrqhdFSqx%+WJ3H>iH*EyKES48CcXcs$dutm3Rb}Em&{>oj@>9v#4h*} zi($LV=5t^?YDHTy9FL$rh#sR3*$Y&p(p)hU3$jL_>Pw=(i%e}Y3SkRWNIRk$bVI!j zLvRuv#R=H)Dqp+sA(p}!*UW?tVphtpP!S2ZZr={nVJ?lDSRGq#imAW)bHhw5Cu(68unji2LHu<}S5l$9 z{|Uoz8|uL`sDYoM_SEgB*~0)-eQwl>i=(z83N?Was0sE(EoeAuLh+~xOhJva^d|9F zgJdeSl0Q)ooa1)h)X z9-|^y0gK|NsD1{b1|Exw>{QIJ_y0#So>Uw|b$A99+Iy&u{zZLMdOb0Qqb5=j^-kN*W2@sa9pgJopPT=tTZ82(A4MH1?-ypqxv&W3 z3Rn!gqTY^asDU@UqXpF9{&c{u9%#i;v#qf4js3>ih>B5IFrqdIPKQtT!D@7-fQBo*Q3%K zKGm=`YA=_gPU~JQfWB|djLTpe%9T+euZ}t^pJE6$M;*pk)WXK0Ryq;271L19EwiqD zOZ>Is%~YtP-Kg>r)WpuCI=X4^-$m{HD-1-p|4ihvU>?f3Q0;4>4s{eNQjJg(>5Lku z2kLMRa*5W^Sc>v7)Ie$9nThzI-j)-iyHO$Bi<;0W)TzFPI-Ji@?Q*|2ArsL(-cVQ(Dfp&0W2kn)hGk~)#Tq`o}u(xWE*@umI$hx+QY z`<;9W`4yyOlCDteBkt!SUzxfijPFdLVm?V%K74KSEA0bCt?RjelDr%H(h_IWZh&oj zlY19Q<0wxiP2>I=${Fpmz8FG%0n%vdYv5p##&<@O$wPx?Bp3O~cm;RjpWL5M(xLkJ z%FDg#wxT(1rQC;SmS9uTIZ}Db2W^`(lCrMft^c|)egPWlI&Yn7osCCqL){-rexfap zv29fUj(i8|TibhQDfib6u8P#nAYGuYK5nD#H2K;1E2gghF*jBz&(+zG`mbKIqukp- znn`}jNB4U0@Pd!Z*;6~?bHZAJXLLnS{@G-myVSj)oXg%Dh|?pvxtDZ^!e;w$Gx8Cn z+N5xjuG`$JOa61WAFfsk)UK^ft>=prwZpla!M1E|ds%DC%PHR=e*|0Ex?Vi%PkthG z2W(wA>S~fIP#2C3NR7ysAbq&*kPjibPVrDhY9HG>J5ev;KI(IS^uTWN|4?6w`s?J! zqkb9pD|IR4=i0VQ$?IB3d7aIFVpaVx>RV$psftdGu3-DH^6|FuITh^HnEM;Zk0K@5 zwiU={v-#ig8_JtVGf28pNR4e@-O1l2U8P-P)YX-IqC4ZaBNIc#_oOG}-;#8#v7RR% z_t6Tv*!)QD|H8e^q|>BVw*5TvPi&h@)CG|5Pr6NgQ&JP^mXb!0Pt;5L4e7%*h)e^@ z`egr%M&FQhHKML6w!> zBtP=*+@FlP+K@6(F3CMzw@4c)=b&7Nq%R4B6_TZ-q1;$RIacL zNxF7W&o>umS!#*TO`Jt4Onohqeqig`PP%6MQ2AS%|CIbB`f?Se;#)F>?2QZD7((99 zmi16G(qE*XsGDymp!!4PJ#4$aWq#SGHUlV@OYJS#8&oKZ(~#o9H7EAJ{=klK=R6P1|$a>tvt* zn|x*(e69W8Px7~|J-P8Gjn<&PGyQBI$bx^7DpOy?)J3`k^hBu z{m9?3Z9btqm=s3&D))7{hVoBfYS#QyoEtf9xgL$Lkv=7P+Xn`6uORtCwv8A0lHAvo zom7|nJ?i?AUQ^z!8}_%_OaTImB?&ES&ht&4O{~?vJR2lMYV4#o zO3gWEsGnf6&VSb9*q%0_+^a{vB~B-;CSQ~K(&V?>KI+N9BnuhU4^ebQ6% zFYNiU#B}*)}bxn`85?AL)D? z6^p3Q=gwZr1IUlS%eGN5+llB!T`SUd?r$d5Bn|)Q{zJ+^q#U&APijLtPWi)Cmhu;r zGnzVnYxL1gHR??HIgRq-Li>~_Gdn?@u6X=cdFpoHC|j?4QRIj5TrB=Z%0_;LZrE#w zwUT|Ns+;1^Re^?IQ+Ww>FE{GCh+#J07Y|UEhJhnf@0x$oQQyyYSjZY`?*~#&An!}6 zM#@GyNL>@#XBPeZ|AlnYR@AW_(9T&wT`cxifomkmm%6`5O~`k|Y4{({=)VA;kaWdS zK85c|TWJ$aN>6G@`3m?&sCBG3DQ!arMZ~>`2 zX{8!)9i+_`>`IzLzB>2b;7XFNU$Hmodr}gq80mmM|6h>_qVY`pj)q(C!?ln4qz?sa zI_k=jA5OdC*cK1tF6tvm%_--$ZTqN>@-bT$Q>Yt&)2L7Vt5;0#|<8E+Og4s`HnZ^d}YB zNvG%_E$PGMLtSlK_=5TxHb0X30NZX1b#EzO!!)+e1J{ubQ{RR9#iV5NJ4yM;kKCXw-?PS-?I4pKK2xOS0lkoJ*o(WW8k7@Z8E?2f;Y{w4q6Do42vTH SE$&I-2{*eYHG1+g!~XyV7!nu& diff --git a/locale/en_US/LC_MESSAGES/django.po b/locale/en_US/LC_MESSAGES/django.po index 8067856a82..8bbe1de2e8 100644 --- a/locale/en_US/LC_MESSAGES/django.po +++ b/locale/en_US/LC_MESSAGES/django.po @@ -2309,6 +2309,9 @@ msgstr "users" msgid "Using your current browser will prevent you from accessing features on our website. Use the links below to download a new browser or upgrade your existing browser." msgstr "Using your current browser will prevent you from accessing features on our website. Use the links below to download a new browser or upgrade your existing browser." +msgid "USING_DEFAULT_UNITS_WARNING" +msgstr "For columns with unit settings, default units will used for conversions." + msgid "Version" msgstr "Version" diff --git a/locale/fr_CA/LC_MESSAGES/django.mo b/locale/fr_CA/LC_MESSAGES/django.mo index 367f153f790c11bbc878959fc745850b3755fb7c..9930ccd5ce3415023773c2ab19d9bfda4b0d5348 100644 GIT binary patch delta 13812 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 Xe6XdkVNkOz;ZtT#+7=P)X&d%`^5^k+ diff --git a/locale/fr_CA/LC_MESSAGES/django.po b/locale/fr_CA/LC_MESSAGES/django.po index 645528752b..e51e23837b 100644 --- a/locale/fr_CA/LC_MESSAGES/django.po +++ b/locale/fr_CA/LC_MESSAGES/django.po @@ -2315,6 +2315,9 @@ msgstr "utilisateurs" msgid "Using your current browser will prevent you from accessing features on our website. Use the links below to download a new browser or upgrade your existing browser." msgstr "L'utilisation de votre navigateur actuel vous empêchera d'accéder aux fonctionnalités de notre site Web. Utilisez les liens ci-dessous pour télécharger un nouveau navigateur ou mettre à niveau votre navigateur existant." +msgid "USING_DEFAULT_UNITS_WARNING" +msgstr "Pour les colonnes avec des paramètres d'unité, les unités par défaut seront utilisées pour les conversions." + msgid "Version" msgstr "Version" diff --git a/seed/static/seed/locales/en_US.json b/seed/static/seed/locales/en_US.json index 7d45880ac8..e2a613dbbb 100644 --- a/seed/static/seed/locales/en_US.json +++ b/seed/static/seed/locales/en_US.json @@ -725,6 +725,7 @@ "username (email)": "username (email)", "users": "users", "Using your current browser will prevent you from accessing features on our website. Use the links below to download a new browser or upgrade your existing browser.": "Using your current browser will prevent you from accessing features on our website. Use the links below to download a new browser or upgrade your existing browser.", + "USING_DEFAULT_UNITS_WARNING": "For columns with unit settings, default units will used for conversions.", "Version": "Version", "View by Property": "View by Property", "View by Tax Lot": "View by Tax Lot", diff --git a/seed/static/seed/locales/fr_CA.json b/seed/static/seed/locales/fr_CA.json index 9a353c5f25..b45d9028cd 100644 --- a/seed/static/seed/locales/fr_CA.json +++ b/seed/static/seed/locales/fr_CA.json @@ -725,6 +725,7 @@ "username (email)": "nom d'utilisateur (email)", "users": "utilisateurs", "Using your current browser will prevent you from accessing features on our website. Use the links below to download a new browser or upgrade your existing browser.": "L'utilisation de votre navigateur actuel vous empêchera d'accéder aux fonctionnalités de notre site Web. Utilisez les liens ci-dessous pour télécharger un nouveau navigateur ou mettre à niveau votre navigateur existant.", + "USING_DEFAULT_UNITS_WARNING": "Pour les colonnes avec des paramètres d'unité, les unités par défaut seront utilisées pour les conversions.", "Version": "Version", "View by Property": "Examiner par propriété", "View by Tax Lot": "Examiner par lot d'impôt", diff --git a/seed/static/seed/partials/rename_column_modal.html b/seed/static/seed/partials/rename_column_modal.html index fdbfdf6dcc..2ed60c1311 100644 --- a/seed/static/seed/partials/rename_column_modal.html +++ b/seed/static/seed/partials/rename_column_modal.html @@ -22,6 +22,7 @@
  • IRREVERSIBLE_OPERATION_WARNING
  • NEW_COLUMN_REPLACE_DATA_WARNING
  • +
  • USING_DEFAULT_UNITS_WARNING
  • LONG_OPERATION_WARNING
From d2b5ef67a8e31054b69f52088f461eca2922b99a Mon Sep 17 00:00:00 2001 From: Nicholas Long Date: Thu, 2 May 2019 10:20:41 -0600 Subject: [PATCH 14/22] bump version --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f3dd84e7d..bbc13c451b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# SEED Version 2.5.2 + +* Placeholder + # SEED Version 2.5.1 Date Range: 03/28/19 - 04/15/19 diff --git a/package.json b/package.json index 978271ef04..5be6dd4e0a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "SEED", - "version": "2.5.1", + "version": "2.5.2", "description": "Standard Energy Efficiency Data (SEED) Platform™", "license": "SEE LICENSE IN LICENSE", "directories": { From 2b6d076358656416ae7fbcde48fddd4c38fc5d17 Mon Sep 17 00:00:00 2001 From: Nicholas Long Date: Fri, 3 May 2019 13:50:22 -0600 Subject: [PATCH 15/22] add bldg and suite to address parsing --- requirements/base.txt | 2 +- seed/tests/test_address_normalization.py | 125 +++++++++++------------ seed/utils/address.py | 42 ++++++-- 3 files changed, 97 insertions(+), 72 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 765808daf5..7453797918 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -44,7 +44,7 @@ raven==6.9.0 jellyfish==0.6.1 Markdown==3.0.1 python-dateutil==2.7.3 -street-address==0.3.0 +street-address==0.4.0 unicodecsv==0.14.1 unidecode==1.0.22 usaddress==0.5.10 diff --git a/seed/tests/test_address_normalization.py b/seed/tests/test_address_normalization.py index c1dc1859b7..3b985780c8 100644 --- a/seed/tests/test_address_normalization.py +++ b/seed/tests/test_address_normalization.py @@ -9,72 +9,67 @@ from seed.utils.address import normalize_address_str -def make_method(message, expected): - def run(self): - result = normalize_address_str(message) - self.assertEquals(expected, result) - return run +class TestColumnListSettings(TestCase): + def test_adding_columns(self): + cases = [ + # case, test str, expected resulting string, actual response + ('simple', '123 Test St.', '123 test st'), + ('none input', None, None), + ('empty input', '', None), + ('missing number', 'Test St.', 'test st'), + ('missing street', '123', '123'), + ('integer address', 123, '123'), + ('strip leading zeros', '0000123', '123'), + ('street 1', 'STREET', 'st'), + ('street 2', 'Street', 'st'), + ('boulevard', 'Boulevard', 'blvd'), + ('avenue', 'avenue', 'ave'), + ('trailing direction', '123 Test St. NE', '123 test st ne'), + ('prefix direction', '123 South Test St.', '123 s test st'), + ('verbose direction', '123 Test St. Northeast', '123 test st ne'), + ('two directions', '111 S West Main', '111 s west main'), + ('numeric street and direction', '555 11th St. NW', '555 11th st nw'), + ('direction 1', '100 Main S', '100 main s'), + ('direction 2', '100 Main South', '100 main s'), + ('direction 3', '100 Main S.', '100 main s'), + ('direction 4', '100 Main', '100 main'), + # Found edge cases + # https://github.com/SEED-platform/seed/issues/378 + ('regression 1', '100 Peach Ave. East', '100 peach ave e'), + ('regression 2', '100 Peach Avenue E.', '100 peach ave e'), + ('multiple addresses', 'M St., N St., 4th St., Delaware St., SW', + 'm st., n st., 4th st., delaware st., sw'), + # House numbers declared as ranges + ('no range separator', '300 322 S Green St', '300-322 s green st'), + ('- as separator no whitespace', '300-322 S Green St', '300-322 s green st'), + ('/ as separator no whitespace', '300/322 S Green St', '300-322 s green st'), + ('\\ as separator no whitespace', '300\\322 S Green St', '300-322 s green st'), + ('- as separator whitespace', '300 - 322 S Green St', '300-322 s green st'), + ('/ as separator whitespace', '300 / 322 S Green St', '300-322 s green st'), + ('\\ as separator whitespace', '300 \\ 322 S Green St', '300-322 s green st'), + # Ranges which leave off common prefix. + ('end of range leaves off common prefix', '300-22 S Green St', '300-322 s green st'), + # Odd characters + ('unicode characters', '123 Main St\uFFFD', '123 main st'), + # Straight numbers + ('straight numbers', 56195600100, '56195600100'), + # bytestrings + ('bytestring', b'123456 Main St', '123456 main st'), + # Suites / building ids + ('suite 1', '2655 SEELY AV Suite 9', '2655 seely av suite 9'), + ('suite 1', '2655 SEELY AV Ste 9', '2655 seely av suite 9'), + ('suite 1', '2655 SEELY AV BLDG 9', '2655 seely av building 9'), + ('suite 1', '2655 SEELY AV BUILDING 9', '2655 seely av building 9'), + ('suite 1', '2655 SEELY AV BUILDING 9 suite 50', '2655 seely av building 9 suite 50'), + ] -# Metaclass to create individual test methods per test case. -class NormalizeAddressTester(type): + results = [] + expected_results = [c[2] for c in cases] - def __new__(cls, name, bases, attrs): - cases = attrs.get('cases', []) + for case in cases: + results.append(normalize_address_str(case[1])) - for doc, message, expected in cases: - test = make_method(message, expected) - test_name = 'test_normalize_address_%s' % doc.lower().replace(' ', '_') - if test_name in attrs: - raise KeyError("Test name {0} duplicated".format(test_name)) - test.__name__ = test_name - test.__doc__ = doc - attrs[test_name] = test - return super(NormalizeAddressTester, cls).__new__(cls, name, bases, attrs) - - -class NormalizeStreetAddressTests(TestCase): - __metaclass__ = NormalizeAddressTester - - # test name, input, expected output - cases = [ - ('simple', '123 Test St.', '123 test st'), - ('none input', None, None), - ('empty input', '', None), - ('missing number', 'Test St.', 'test st'), - ('missing street', '123', '123'), - ('integer address', 123, '123'), - ('strip leading zeros', '0000123', '123'), - ('street 1', 'STREET', 'st'), - ('street 2', 'Street', 'st'), - ('boulevard', 'Boulevard', 'blvd'), - ('avenue', 'avenue', 'ave'), - ('trailing direction', '123 Test St. NE', '123 test st ne'), - ('prefix direction', '123 South Test St.', '123 s test st'), - ('verbose direction', '123 Test St. Northeast', '123 test st ne'), - ('two directions', '111 S West Main', '111 s west main'), - ('numeric street and direction', '555 11th St. NW', '555 11th st nw'), - ('direction 1', '100 Main S', '100 main s'), - ('direction 2', '100 Main South', '100 main s'), - ('direction 3', '100 Main S.', '100 main s'), - ('direction 4', '100 Main', '100 main'), - # Found edge cases - # https://github.com/SEED-platform/seed/issues/378 - ('regression 1', '100 Peach Ave. East', '100 peach ave e'), - ('regression 2', '100 Peach Avenue E.', '100 peach ave e'), - ('multiple addresses', 'M St., N St., 4th St., Delaware St., SW', 'm st., n st., 4th st., delaware st., sw'), - # House numbers declared as ranges - ('no range separator', '300 322 S Green St', '300-322 s green st'), - ('- as separator no whitespace', '300-322 S Green St', '300-322 s green st'), - ('/ as separator no whitespace', '300/322 S Green St', '300-322 s green st'), - ('\\ as separator no whitespace', '300\\322 S Green St', '300-322 s green st'), - ('- as separator whitespace', '300 - 322 S Green St', '300-322 s green st'), - ('/ as separator whitespace', '300 / 322 S Green St', '300-322 s green st'), - ('\\ as separator whitespace', '300 \\ 322 S Green St', '300-322 s green st'), - # Ranges which leave off common prefix. - ('end of range leaves off common prefix', '300-22 S Green St', '300-322 s green st'), - # Odd characters - ('unicode characters', '123 Main St\uFFFD', '123 main st'), - # Straight numbers - ('straight numbers', 56195600100, '56195600100'), - ] + # print(results) + # print(expected_results) + self.assertListEqual(results, expected_results) diff --git a/seed/utils/address.py b/seed/utils/address.py index cef3d580cc..ac8f8f5d41 100644 --- a/seed/utils/address.py +++ b/seed/utils/address.py @@ -8,10 +8,32 @@ import re import usaddress -from past.builtins import basestring +# from past.builtins import basestring from streetaddress import StreetAddressFormatter +def _normalize_subaddress_type(subaddress_type): + subaddress_type = subaddress_type.lower().replace('.', '') + map = { + 'bldg': 'building', + 'blg': 'building', + } + if subaddress_type in map: + return map[subaddress_type] + return subaddress_type + + +def _normalize_occupancy_type(occupancy_id): + occupancy_id = occupancy_id.lower().replace('.', '') + map = { + 'ste': 'suite', + 'suite': 'suite', + } + if occupancy_id in map: + return map[occupancy_id] + return occupancy_id + + def _normalize_address_direction(direction): direction = direction.lower().replace('.', '') direction_map = { @@ -83,17 +105,18 @@ def normalize_address_str(address_val): If a valid address is provided, a normalized version is returned. """ - # if this string is empty the regular expression in the sa wont # like it, and fail, so leave returning nothing if not address_val: return None - # encode the string as utf-8 - if not isinstance(address_val, basestring): + # if this is a byte string, then convert to a string-string + if isinstance(address_val, bytes): + address_val = address_val.decode('utf-8') + elif not isinstance(address_val, str): address_val = str(address_val) else: - address_val = str(address_val.encode('utf-8')) + pass # Do some string replacements to remove odd characters that we come across replacements = { @@ -117,6 +140,7 @@ def normalize_address_str(address_val): # Address can be parsed, so let's format it. normalized_address = '' + print(addr) if 'AddressNumber' in addr and addr['AddressNumber'] is not None: normalized_address = _normalize_address_number( addr['AddressNumber']) @@ -137,8 +161,14 @@ def normalize_address_str(address_val): normalized_address = normalized_address + ' ' + _normalize_address_direction( addr['StreetNamePostDirectional']) # NOQA + if 'SubaddressType' in addr and addr['SubaddressType'] is not None: + normalized_address = normalized_address + ' ' + _normalize_subaddress_type(addr['SubaddressType']) # NOQA + + if 'SubaddressIdentifier' in addr and addr['SubaddressIdentifier'] is not None: + normalized_address = normalized_address + ' ' + addr['SubaddressIdentifier'] + if 'OccupancyType' in addr and addr['OccupancyType'] is not None: - normalized_address = normalized_address + ' ' + addr['OccupancyType'] + normalized_address = normalized_address + ' ' + _normalize_occupancy_type(addr['OccupancyType']) if 'OccupancyIdentifier' in addr and addr['OccupancyIdentifier'] is not None: normalized_address = normalized_address + ' ' + addr['OccupancyIdentifier'] From 3bf3ffac7d31aa824d9252981f384f0c858dc8ab Mon Sep 17 00:00:00 2001 From: Nicholas Long Date: Fri, 3 May 2019 14:41:53 -0600 Subject: [PATCH 16/22] add migration to recalculate all of the normalized addresses --- seed/migrations/0102_auto_20190503_1251.py | 37 ++++++++++++++++++++++ seed/tests/test_address_normalization.py | 10 +++--- seed/utils/address.py | 1 - 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 seed/migrations/0102_auto_20190503_1251.py diff --git a/seed/migrations/0102_auto_20190503_1251.py b/seed/migrations/0102_auto_20190503_1251.py new file mode 100644 index 0000000000..fb7025ebb7 --- /dev/null +++ b/seed/migrations/0102_auto_20190503_1251.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-05-03 19:51 +from __future__ import unicode_literals + +from django.db import migrations, transaction + +from seed.utils.address import normalize_address_str + + +def forwards(apps, schema_editor): + PropertyState = apps.get_model("seed", "PropertyState") + TaxLotState = apps.get_model("seed", "TaxLotState") + + with transaction.atomic(): + for index, p in enumerate(PropertyState.objects.filter(address_line_1__isnull=False)): + if index % 1000 == 0: + print('iterating ... %s' % index) + + p.normalized_address = normalize_address_str(p.address_line_1) + p.save(update_fields=["normalized_address"]) + + for index, t in enumerate(TaxLotState.objects.filter(address_line_1__isnull=False)): + if index % 1000 == 0: + print('iterating ... %s' % index) + + t.normalized_address = normalize_address_str(t.address_line_1) + t.save(update_fields=["normalized_address"]) + + +class Migration(migrations.Migration): + dependencies = [ + ('seed', '0101_auto_20190318_1835'), + ] + + operations = [ + migrations.RunPython(forwards), + ] diff --git a/seed/tests/test_address_normalization.py b/seed/tests/test_address_normalization.py index 3b985780c8..d29ebd15e4 100644 --- a/seed/tests/test_address_normalization.py +++ b/seed/tests/test_address_normalization.py @@ -8,7 +8,6 @@ from seed.utils.address import normalize_address_str - class TestColumnListSettings(TestCase): def test_adding_columns(self): @@ -58,10 +57,11 @@ def test_adding_columns(self): ('bytestring', b'123456 Main St', '123456 main st'), # Suites / building ids ('suite 1', '2655 SEELY AV Suite 9', '2655 seely av suite 9'), - ('suite 1', '2655 SEELY AV Ste 9', '2655 seely av suite 9'), - ('suite 1', '2655 SEELY AV BLDG 9', '2655 seely av building 9'), - ('suite 1', '2655 SEELY AV BUILDING 9', '2655 seely av building 9'), - ('suite 1', '2655 SEELY AV BUILDING 9 suite 50', '2655 seely av building 9 suite 50'), + ('suite 2', '2655 SEELY AV Ste 9', '2655 seely av suite 9'), + ('bldg 1', '2655 SEELY AV BLDG 9', '2655 seely av building 9'), + ('bldg 2', '2655 SEELY AV BUILDING 9', '2655 seely av building 9'), + ('b+s 1', '2655 SEELY AV BUILDING 9a ste 50', '2655 seely av building 9a suite 50'), + ('b+s 2', '2655 SEELY AV BUILDING 9 suite 50', '2655 seely av building 9 suite 50'), ] results = [] diff --git a/seed/utils/address.py b/seed/utils/address.py index ac8f8f5d41..1bf1187ac0 100644 --- a/seed/utils/address.py +++ b/seed/utils/address.py @@ -140,7 +140,6 @@ def normalize_address_str(address_val): # Address can be parsed, so let's format it. normalized_address = '' - print(addr) if 'AddressNumber' in addr and addr['AddressNumber'] is not None: normalized_address = _normalize_address_number( addr['AddressNumber']) From a1334b9bde73299682602bfa40478fe4a4f97123 Mon Sep 17 00:00:00 2001 From: Nicholas Long Date: Fri, 3 May 2019 15:24:37 -0600 Subject: [PATCH 17/22] flake8 --- seed/tests/test_address_normalization.py | 1 + 1 file changed, 1 insertion(+) diff --git a/seed/tests/test_address_normalization.py b/seed/tests/test_address_normalization.py index d29ebd15e4..33fc61c0cc 100644 --- a/seed/tests/test_address_normalization.py +++ b/seed/tests/test_address_normalization.py @@ -8,6 +8,7 @@ from seed.utils.address import normalize_address_str + class TestColumnListSettings(TestCase): def test_adding_columns(self): From b7e07c0bf135c79fddc25fec6904cb553ce28fb9 Mon Sep 17 00:00:00 2001 From: Alex Swindler Date: Fri, 3 May 2019 16:17:58 -0600 Subject: [PATCH 18/22] UI fixes for column renaming --- docs/source/translation.rst | 2 +- locale/en_US/LC_MESSAGES/django.mo | Bin 65606 -> 65634 bytes locale/en_US/LC_MESSAGES/django.po | 7 ++-- locale/fr_CA/LC_MESSAGES/django.mo | Bin 73318 -> 73219 bytes locale/fr_CA/LC_MESSAGES/django.po | 6 +++- script/get_python_translations | 4 +-- .../controllers/column_settings_controller.js | 11 +++--- .../rename_column_modal_controller.js | 32 ++++++++++++------ .../seed/js/services/columns_service.js | 6 ++-- seed/static/seed/locales/en_US.json | 5 +-- seed/static/seed/locales/fr_CA.json | 3 +- .../seed/partials/rename_column_modal.html | 17 +++++----- seed/views/columns.py | 4 +-- 13 files changed, 59 insertions(+), 38 deletions(-) diff --git a/docs/source/translation.rst b/docs/source/translation.rst index 2dd732f2eb..e4f08ccf4f 100644 --- a/docs/source/translation.rst +++ b/docs/source/translation.rst @@ -9,7 +9,7 @@ Translating SEED .. code:: bash - scripts/get_python_translations + script/get_python_translations script/get_angular_translations 4. Verify and commit changes diff --git a/locale/en_US/LC_MESSAGES/django.mo b/locale/en_US/LC_MESSAGES/django.mo index 16d814b89355d7db6f96e6a72a507fb3ba5389f5..41d9426138105c452937288c1009f08c3c1555f1 100644 GIT binary patch delta 16760 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 l+_7i3{@r^7x9`T|odyL*42RkimX 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 UljJl?NgA9Iv3*|-x7xY>2WvG6)c^nh diff --git a/locale/fr_CA/LC_MESSAGES/django.po b/locale/fr_CA/LC_MESSAGES/django.po index e51e23837b..e3705b7a2d 100644 --- a/locale/fr_CA/LC_MESSAGES/django.po +++ b/locale/fr_CA/LC_MESSAGES/django.po @@ -339,6 +339,9 @@ msgstr "Nom de colonne" msgid "Column Order/Visibility" msgstr "L'Ordre Des Colonnes/Visibilité" +msgid "COLUMN_NAME_DUPLICATE_ERROR" +msgstr "Erreur: Le nom de la nouvelle colonne ne peut pas correspondre au nom précédent." + msgid "COLUMN_NAME_EXISTS_WARNING" msgstr "Attention: le nom de la colonne existe déjà." @@ -1433,7 +1436,7 @@ msgid "Organizations:" msgstr "Organisations:" msgid "OVERWRITE_COLUMN_DATA_QUESTION" -msgstr "Écraser les données si la colonne existe déjà?" +msgstr "Écraser les données?" msgid "Owner" msgstr "Propriétaire" @@ -2315,6 +2318,7 @@ msgstr "utilisateurs" msgid "Using your current browser will prevent you from accessing features on our website. Use the links below to download a new browser or upgrade your existing browser." msgstr "L'utilisation de votre navigateur actuel vous empêchera d'accéder aux fonctionnalités de notre site Web. Utilisez les liens ci-dessous pour télécharger un nouveau navigateur ou mettre à niveau votre navigateur existant." +#, fuzzy msgid "USING_DEFAULT_UNITS_WARNING" msgstr "Pour les colonnes avec des paramètres d'unité, les unités par défaut seront utilisées pour les conversions." diff --git a/script/get_python_translations b/script/get_python_translations index 1d28c3c6a0..dcbf50a11b 100755 --- a/script/get_python_translations +++ b/script/get_python_translations @@ -18,9 +18,9 @@ lokalise \ unzip $tmp/SEED_Platform-locale.zip -d $tmp mv $tmp/locale/fr_CA.po $dest/fr_CA/LC_MESSAGES/django.po -/usr/local/opt/gettext/bin/msgfmt -o $dest/fr_CA/LC_MESSAGES/django.{mo,po} +msgfmt -o $dest/fr_CA/LC_MESSAGES/django.{mo,po} mv $tmp/locale/en_US.po $dest/en_US/LC_MESSAGES/django.po -/usr/local/opt/gettext/bin/msgfmt -o $dest/en_US/LC_MESSAGES/django.{mo,po} +msgfmt -o $dest/en_US/LC_MESSAGES/django.{mo,po} rm -rf $tmp diff --git a/seed/static/seed/js/controllers/column_settings_controller.js b/seed/static/seed/js/controllers/column_settings_controller.js index 525dda5cf8..1e6b097252 100644 --- a/seed/static/seed/js/controllers/column_settings_controller.js +++ b/seed/static/seed/js/controllers/column_settings_controller.js @@ -62,7 +62,8 @@ angular.module('BE.seed.controller.column_settings', []) {id: 'date', label: $translate.instant('Date')}, {id: 'boolean', label: $translate.instant('Boolean')}, {id: 'area', label: $translate.instant('Area')}, - {id: 'eui', label: $translate.instant('EUI')} + {id: 'eui', label: $translate.instant('EUI')}, + {id: 'geometry', label: $translate.instant('Geometry')} ]; $scope.change_merge_protection = function (column) { @@ -133,7 +134,7 @@ angular.module('BE.seed.controller.column_settings', []) }; $scope.open_rename_column_modal = function (column_id, column_name) { - var modalInstance = $uibModal.open({ + $uibModal.open({ templateUrl: urls.static_url + 'seed/partials/rename_column_modal.html', controller: 'rename_column_modal_controller', resolve: { @@ -143,10 +144,10 @@ angular.module('BE.seed.controller.column_settings', []) column_name: function () { return column_name; }, - all_column_names: function() { + all_column_names: function () { return _.map($scope.columns, 'column_name'); - }, - }, + } + } }); }; diff --git a/seed/static/seed/js/controllers/rename_column_modal_controller.js b/seed/static/seed/js/controllers/rename_column_modal_controller.js index bc8a22ad6d..2b7891df7b 100644 --- a/seed/static/seed/js/controllers/rename_column_modal_controller.js +++ b/seed/static/seed/js/controllers/rename_column_modal_controller.js @@ -23,7 +23,7 @@ angular.module('BE.seed.controller.rename_column_modal', []) spinner_utility ) { $scope.step = { - number: 1, + number: 1 }; $scope.current_column_name = column_name; @@ -31,35 +31,35 @@ angular.module('BE.seed.controller.rename_column_modal', []) $scope.column = { id: column_id, - name: "", - exists: false, - } + name: '', + exists: false + }; $scope.settings = { user_acknowledgement: false, - overwrite_preference: false, - } + overwrite_preference: false + }; $scope.check_name_exists = function () { - $scope.column.exists = _.find($scope.all_column_names, function(col_name) { + $scope.column.exists = _.find($scope.all_column_names, function (col_name) { return col_name === $scope.column.name; }); }; - $scope.accept_rename = function() { + $scope.accept_rename = function () { spinner_utility.show(); columns_service.rename_column($scope.column.id, $scope.column.name, $scope.settings.overwrite_preference) - .then(function(response) { + .then(function (response) { $scope.results = { success: response.data.success, - message: response.data.message, + message: response.data.message }; $scope.step.number = 2; spinner_utility.hide(); }); }; - $scope.dismiss_and_refresh = function() { + $scope.dismiss_and_refresh = function () { $state.reload(); $uibModalInstance.close(); }; @@ -67,4 +67,14 @@ angular.module('BE.seed.controller.rename_column_modal', []) $scope.cancel = function () { $uibModalInstance.close(); }; + + $scope.valid = function () { + if (!$scope.column.name || $scope.column.name === $scope.current_column_name) return false; + + if ($scope.column.exists) { + return $scope.settings.user_acknowledgement && $scope.settings.overwrite_preference; + } else { + return $scope.settings.user_acknowledgement; + } + } }]); diff --git a/seed/static/seed/js/services/columns_service.js b/seed/static/seed/js/services/columns_service.js index 1a7a037186..c015cf48ae 100644 --- a/seed/static/seed/js/services/columns_service.js +++ b/seed/static/seed/js/services/columns_service.js @@ -29,14 +29,14 @@ angular.module('BE.seed.service.columns', []).factory('columns_service', [ new_column_name: column_name, overwrite: overwrite_preference }).then(function (response) { - return response + return response; }).catch(function (error_response) { return { data: { success: false, - message: "Unsuccessful! " + error_response.statusText, + message: 'Unsuccessful: ' + error_response.statusText } - } + }; }); }; diff --git a/seed/static/seed/locales/en_US.json b/seed/static/seed/locales/en_US.json index e2a613dbbb..f9327881d3 100644 --- a/seed/static/seed/locales/en_US.json +++ b/seed/static/seed/locales/en_US.json @@ -101,6 +101,7 @@ "Collapse Tabs": "Collapse Tabs", "Column Name": "Column Name", "Column Order\/Visibility": "Column Order\/Visibility", + "COLUMN_NAME_DUPLICATE_ERROR": "Error: New column name cannot match previous name.", "COLUMN_NAME_EXISTS_WARNING": "Warning: Column name already exists.", "Commercial": "Commercial", "Complete": "Complete", @@ -451,7 +452,7 @@ "Organizations I Belong To": "Organizations I Belong To", "Organizations I Manage": "Organizations I Manage", "Organizations:": "Organizations:", - "OVERWRITE_COLUMN_DATA_QUESTION": "Overwrite data if the column already exists?", + "OVERWRITE_COLUMN_DATA_QUESTION": "Overwrite data?", "Owner": "Owner", "owner": "owner", "Owner Address": "Owner Address", @@ -725,7 +726,7 @@ "username (email)": "username (email)", "users": "users", "Using your current browser will prevent you from accessing features on our website. Use the links below to download a new browser or upgrade your existing browser.": "Using your current browser will prevent you from accessing features on our website. Use the links below to download a new browser or upgrade your existing browser.", - "USING_DEFAULT_UNITS_WARNING": "For columns with unit settings, default units will used for conversions.", + "USING_DEFAULT_UNITS_WARNING": "For columns with unit settings, default units will be used for conversions.", "Version": "Version", "View by Property": "View by Property", "View by Tax Lot": "View by Tax Lot", diff --git a/seed/static/seed/locales/fr_CA.json b/seed/static/seed/locales/fr_CA.json index b45d9028cd..9d1cff6ab3 100644 --- a/seed/static/seed/locales/fr_CA.json +++ b/seed/static/seed/locales/fr_CA.json @@ -101,6 +101,7 @@ "Collapse Tabs": "Réduire les onglets", "Column Name": "Nom de colonne", "Column Order\/Visibility": "L'Ordre Des Colonnes\/Visibilité", + "COLUMN_NAME_DUPLICATE_ERROR": "Erreur: Le nom de la nouvelle colonne ne peut pas correspondre au nom précédent.", "COLUMN_NAME_EXISTS_WARNING": "Attention: le nom de la colonne existe déjà.", "Commercial": "Commercial", "Complete": "Complète", @@ -451,7 +452,7 @@ "Organizations I Belong To": "Organisations auxquelles j'appartiens", "Organizations I Manage": "Organisations que je gère", "Organizations:": "Organisations:", - "OVERWRITE_COLUMN_DATA_QUESTION": "Écraser les données si la colonne existe déjà?", + "OVERWRITE_COLUMN_DATA_QUESTION": "Écraser les données?", "Owner": "Propriétaire", "owner": "propriétaire", "Owner Address": "Adresse du propriétaire", diff --git a/seed/static/seed/partials/rename_column_modal.html b/seed/static/seed/partials/rename_column_modal.html index 2ed60c1311..17a3759d88 100644 --- a/seed/static/seed/partials/rename_column_modal.html +++ b/seed/static/seed/partials/rename_column_modal.html @@ -11,11 +11,8 @@

Desired Name

-
COLUMN_NAME_EXISTS_WARNING
-
-
- OVERWRITE_COLUMN_DATA_QUESTION - +
COLUMN_NAME_EXISTS_WARNING
+
COLUMN_NAME_DUPLICATE_ERROR
@@ -26,9 +23,13 @@
  • LONG_OPERATION_WARNING
  • +
    + + +
    - - Acknowledge + +
    @@ -46,7 +47,7 @@