def propagate_cols(self, col_names, target_df_name, source_df_name,
                       down=True):
        """
        Put the data for "col_name" from source_df into target_df
        Used to get "azimuth" from sample table into measurements table
        (for example).
        Note: if getting data from the sample table, don't include "sample"
        in the col_names list.  It is included automatically.
        """
        # make sure target table is read in
        if target_df_name not in self.tables:
            self.add_magic_table(target_df_name)
        if target_df_name not in self.tables:
            print("-W- Couldn't read in {} table".format(target_df_name))
            return
        # make sure source table is read in
        if source_df_name not in self.tables:
            self.add_magic_table(source_df_name)
            print("-W- Couldn't read in {} table".format(source_df_name))
            return
        # make sure col_names are all available in source table
        source_df = self.tables[source_df_name].df
        if not set(col_names).issubset(source_df.columns):
            for col in col_names[:]:
                if col not in source_df.columns:
                    print("-W- Column '{}' isn't in {} table, skipping it".format(col, source_df_name))
                    col_names.remove(col)
        if not col_names:
            print("-W- Invalid or missing column names, could not propagate columns")
            return
        #
        if down:
            add_name = source_df_name[:-1]
            if 'measurements' in self.tables.keys():
                self.propagate_location_to_measurements()
            elif 'specimens' in self.tables.keys():
                self.propagate_location_to_specimens()
            else:
                self.propagate_name_down('location', 'sites')
        else:
            add_name = target_df_name[:-1]

        # get dataframes for merge
        target_df = self.tables[target_df_name].df
        source_df = self.tables[source_df_name].df
        backup_source_df = source_df.copy()
        # finesse source_df to make sure it has all the right columns
        # and no unnecessary duplicates
        if source_df_name[:-1] not in source_df.columns:
            source_df[source_df_name[:-1]] = source_df.index
        source_df = source_df.drop_duplicates(inplace=False, subset=col_names + [source_df_name[:-1]])
        source_df = source_df.groupby(source_df.index, sort=False).fillna(method='ffill')
        source_df = source_df.groupby(source_df.index, sort=False).fillna(method='bfill')
        # if the groupby/fillna operation fails due to pandas bug, do the same by hand:
        if not len(source_df):
            new = []
            grouped = backup_source_df.groupby(backup_source_df.index)
            for label, group in grouped:
                new_group = group.fillna(method="ffill")
                new_group = new_group.fillna(method="bfill")
                new.append(new_group)
            source_df = pd.concat(new, sort=True)

        # if the groupby/fillna still doesn't work, we are out of luck
        if not len(source_df):
            return target_df
        # propagate down
        if down:
            # do merge
            target_df[add_name] = target_df[add_name].astype(str)
            target_df = target_df.merge(source_df[col_names], how='left',
                                        left_on=add_name, right_index=True,
                                        suffixes=["_target", "_source"])
        # propagate up
        else:
            # do merge
            col_names.append(add_name)
            source_df[add_name] = source_df[add_name].astype(str)
            target_df = target_df.merge(source_df[col_names],
                                        how='left', left_index=True,
                                        right_on=add_name,
                                        suffixes=['_target', '_source'])
            target_df.index = target_df[add_name]
            target_df.drop([add_name + "_source", add_name + "_target"], axis=1, inplace=True)

        # ignore any duplicate rows
        target_df.drop_duplicates(inplace=True)
        # mess with target_df to remove un-needed merge columns
        for col in col_names:
            # if there has been a previous merge, consolidate and delete data
            if col + "_target" in target_df.columns:
                # prioritize values from target df
                new_arr = np.where(target_df[col + "_target"],
                                   target_df[col + "_target"],
                                   target_df[col + "_source"])
                target_df.rename(columns={col + "_target": col}, inplace=True)
                target_df[col] = new_arr
            if col + "_source" in target_df.columns:
                # delete extra merge column
                del target_df[col + "_source"]
        #

        # drop any duplicate rows
        target_df.drop_duplicates(inplace=True)
        self.tables[target_df_name].df = target_df
        return target_df