diff --git a/.gitignore b/.gitignore index 3ae5d07..b134049 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,20 @@ *.pyc settings_local.py + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg diff --git a/report_utils/mixins.py b/report_utils/mixins.py index bf03dc1..facd1f6 100644 --- a/report_utils/mixins.py +++ b/report_utils/mixins.py @@ -23,6 +23,8 @@ import datetime from report_utils.model_introspection import ( + _get_field_by_name, + _get_remote_field, get_relation_fields_from_model, get_properties_from_model, get_direct_fields_from_model, @@ -35,6 +37,7 @@ "path path_verbose field field_verbose aggregate total group choices field_type", ) + def generate_filename(title, ends_with): title = title.split('.')[0] title.replace(' ', '_') @@ -43,6 +46,7 @@ def generate_filename(title, ends_with): title += ends_with return title + class DataExportMixin(object): def build_sheet(self, data, ws, sheet_name='report', header=None, widths=None): first_row = 1 @@ -223,7 +227,7 @@ def can_change_or_view(model): path += '__' # Legacy format to append a __ here. new_model = get_model_from_path_string(model_class, path) - model_field = new_model._meta.get_field_by_name(field)[0] + model_field = new_model._meta.get_field(field) choices = model_field.choices new_display_fields.append(DisplayField( path, '', field, '', '', None, None, choices, '' @@ -541,7 +545,7 @@ def get_fields(self, model_class, field_name='', path='', path_verbose=''): app_label = model_class._meta.app_label if field_name != '': - field = model_class._meta.get_field_by_name(field_name) + field = _get_field_by_name(model_class, field_name) if path_verbose: path_verbose += "::" # TODO: need actual model name to generate choice list (not pluralized field name) @@ -555,9 +559,9 @@ def get_fields(self, model_class, field_name='', path='', path_verbose=''): path += '__' if field[2]: # Direct field try: - new_model = field[0].related.parent_model + new_model = _get_remote_field(field[0]).parent_model except AttributeError: - new_model = field[0].related.model + new_model = _get_remote_field(field[0]).model path_verbose = new_model.__name__.lower() else: # Indirect related field try: @@ -584,13 +588,13 @@ def get_fields(self, model_class, field_name='', path='', path_verbose=''): def get_related_fields(self, model_class, field_name, path="", path_verbose=""): """ Get fields for a given model """ if field_name: - field = model_class._meta.get_field_by_name(field_name) + field = _get_field_by_name(model_class, field_name) if field[2]: # Direct field try: - new_model = field[0].related.parent_model() + new_model = _get_remote_field(field[0]).parent_model() except AttributeError: - new_model = field[0].related.model + new_model = _get_remote_field(field[0]).model else: # Indirect related field if hasattr(field[0], 'related_model'): # Django>=1.8 diff --git a/report_utils/model_introspection.py b/report_utils/model_introspection.py index f4f826f..60304fe 100644 --- a/report_utils/model_introspection.py +++ b/report_utils/model_introspection.py @@ -1,12 +1,15 @@ """ Functioned to introspect a model """ +from itertools import chain from django.contrib.contenttypes.models import ContentType from django.db.models.fields import FieldDoesNotExist from django.conf import settings import inspect + def isprop(v): return isinstance(v, property) + def get_properties_from_model(model_class): """ Show properties from a model """ properties = [] @@ -19,17 +22,56 @@ def get_properties_from_model(model_class): return sorted(properties, key=lambda k: k['label']) +def _get_all_field_names(model): + """ + 100% compatible version of the old API of model._meta.get_all_field_names() + From: https://docs.djangoproject.com/en/1.9/ref/models/meta/#migrating-from-the-old-api + """ + return list(set(chain.from_iterable( + (field.name, field.attname) if hasattr(field, 'attname') else (field.name,) + for field in model._meta.get_fields() + # For complete backwards compatibility, you may want to exclude + # GenericForeignKey from the results. + if not (field.many_to_one and field.related_model is None) + ))) + + +def _get_field_by_name(model_class, field_name): + """ + Compatible with old API of model_class._meta.get_field_by_name(field_name) + """ + field = model_class._meta.get_field(field_name) + return ( + field, + field.model, + not field.auto_created or field.concrete, + field.many_to_many + ) + + +def _get_remote_field(field): + """ + Compatible with Django 1.8~1.10 ('related' was renamed to 'remote_field') + """ + if hasattr(field, 'remote_field'): + return field.remote_field + elif hasattr(field, 'related'): + return field.related + else: + return None + + def get_relation_fields_from_model(model_class): """ Get related fields (m2m, FK, and reverse FK) """ relation_fields = [] - all_fields_names = model_class._meta.get_all_field_names() + all_fields_names = _get_all_field_names(model_class) for field_name in all_fields_names: - field = model_class._meta.get_field_by_name(field_name) + field = _get_field_by_name(model_class, field_name) # get_all_field_names will return the same field # both with and without _id. Ignore the duplicate. if field_name[-3:] == '_id' and field_name[:-3] in all_fields_names: continue - if field[3] or not field[2] or hasattr(field[0], 'related'): + if field[3] or not field[2] or _get_remote_field(field[0]): field[0].field_name = field_name relation_fields += [field[0]] return relation_fields @@ -38,10 +80,10 @@ def get_relation_fields_from_model(model_class): def get_direct_fields_from_model(model_class): """ Direct, not m2m, not FK """ direct_fields = [] - all_fields_names = model_class._meta.get_all_field_names() + all_fields_names = _get_all_field_names(model_class) for field_name in all_fields_names: - field = model_class._meta.get_field_by_name(field_name) - if field[2] and not field[3] and not hasattr(field[0], 'related'): + field = _get_field_by_name(model_class, field_name) + if field[2] and not field[3] and not _get_remote_field(field[0]): direct_fields += [field[0]] return direct_fields @@ -68,15 +110,15 @@ def get_model_from_path_string(root_model, path): for path_section in path.split('__'): if path_section: try: - field = root_model._meta.get_field_by_name(path_section) + field = _get_field_by_name(root_model, path_section) except FieldDoesNotExist: return root_model if field[2]: - if hasattr(field[0], 'related'): + if _get_remote_field(field[0]): try: - root_model = field[0].related.parent_model() + root_model = _get_remote_field(field[0]).parent_model() except AttributeError: - root_model = field[0].related.model + root_model = _get_remote_field(field[0]).model else: if hasattr(field[0], 'related_model'): root_model = field[0].related_model diff --git a/report_utils/utils.py b/report_utils/utils.py index 2d54be0..214d988 100644 --- a/report_utils/utils.py +++ b/report_utils/utils.py @@ -1,3 +1 @@ from .mixins import DataExportMixin - -