diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index 136a9c8dcf..2f3cb08aa3 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -4,7 +4,8 @@ from django.utils.text import capfirst from django.utils.functional import curry from django.contrib.admin.views.main import AdminBoundField from django.db.models.fields import BoundField, Field -from django.db.models import BoundRelatedObject, TABULAR, STACKED +from django.db.models.related import BoundRelatedObject +from django.db.models.fields import TABULAR, STACKED from django.db import models from django.conf.settings import ADMIN_MEDIA_PREFIX import re diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 568fbb8f3f..319df83354 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -24,6 +24,8 @@ from django.utils.html import escape import operator from itertools import izip +from django.db.models.query import handle_legacy_orderlist + # The system will display a "Show all" link only if the total result count # is less than or equal to this setting. MAX_SHOW_ALL_ALLOWED = 200 @@ -231,7 +233,7 @@ class ChangeList(object): ordering = lookup_opts.admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name] # Normalize it to new-style ordering. - ordering = models.handle_legacy_orderlist(ordering) + ordering = handle_legacy_orderlist(ordering) if ordering[0].startswith('-'): order_field, order_type = ordering[0][1:], 'desc' diff --git a/django/core/paginator.py b/django/core/paginator.py index 4320d645fc..592e619f05 100644 --- a/django/core/paginator.py +++ b/django/core/paginator.py @@ -1,6 +1,5 @@ from copy import copy from math import ceil -from django.db.models import ModelBase class InvalidPage(Exception): pass diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py index 3b39bcbf6d..fe4e510472 100644 --- a/django/db/models/__init__.py +++ b/django/db/models/__init__.py @@ -1,36 +1,24 @@ from django.conf import settings from django.core import formfields, validators -from django.core.exceptions import ObjectDoesNotExist + from django.db import backend, connection -from django.db.models.fields import * + from django.utils.functional import curry from django.utils.text import capfirst -import copy, datetime, os, re, sys, types from django.db.models.loading import get_installed_models, get_installed_model_modules -from django.db.models.manipulators import ManipulatorDescriptor, ModelAddManipulator, ModelChangeManipulator -from django.db.models.query import Q, orderlist2sql +from django.db.models.query import Q from django.db.models.manager import Manager +from django.db.models.base import Model +from django.db.models.fields import * + +from django.core.exceptions import ObjectDoesNotExist +from django.db.models.exceptions import FieldDoesNotExist, BadKeywordArguments # Admin stages. ADD, CHANGE, BOTH = 1, 2, 3 -# Prefix (in Python path style) to location of models. -MODEL_PREFIX = 'django.models' - -# Methods on models with the following prefix will be removed and -# converted to module-level functions. -MODEL_FUNCTIONS_PREFIX = '_module_' - -# Methods on models with the following prefix will be removed and -# converted to manipulator methods. -MANIPULATOR_FUNCTIONS_PREFIX = '_manipulator_' - - - - - #def get_module(app_label, module_name): # return __import__('%s.%s.%s' % (MODEL_PREFIX, app_label, module_name), '', '', ['']) @@ -38,7 +26,6 @@ MANIPULATOR_FUNCTIONS_PREFIX = '_manipulator_' # return __import__('%s.%s' % (MODEL_PREFIX, app_label), '', '', ['']) - class LazyDate: """ Use in limit_choices_to to compare the field to dates calculated at run time @@ -64,905 +51,10 @@ class LazyDate: # MAIN CLASSES # ################ -class FieldDoesNotExist(Exception): - pass -class BadKeywordArguments(Exception): - pass -class BoundRelatedObject(object): - def __init__(self, related_object, field_mapping, original): - self.relation = related_object - self.field_mappings = field_mapping[related_object.opts.module_name] - def template_name(self): - raise NotImplementedError - def __repr__(self): - return repr(self.__dict__) -class RelatedObject(object): - def __init__(self, parent_opts, model, field): - self.parent_opts = parent_opts - self.model = model - self.opts = model._meta - self.field = field - self.edit_inline = field.rel.edit_inline - self.name = self.opts.module_name - self.var_name = self.opts.object_name.lower() - - def flatten_data(self, follow, obj=None): - new_data = {} - rel_instances = self.get_list(obj) - for i, rel_instance in enumerate(rel_instances): - instance_data = {} - for f in self.opts.fields + self.opts.many_to_many: - # TODO: Fix for recursive manipulators. - fol = follow.get(f.name, None) - if fol: - field_data = f.flatten_data(fol, rel_instance) - for name, value in field_data.items(): - instance_data['%s.%d.%s' % (self.var_name, i, name)] = value - new_data.update(instance_data) - return new_data - - def extract_data(self, data): - """ - Pull out the data meant for inline objects of this class, - i.e. anything starting with our module name. - """ - return data # TODO - - def get_list(self, parent_instance=None): - "Get the list of this type of object from an instance of the parent class." - if parent_instance != None: - func_name = 'get_%s_list' % self.get_method_name_part() - func = getattr(parent_instance, func_name) - list = func() - - count = len(list) + self.field.rel.num_extra_on_change - if self.field.rel.min_num_in_admin: - count = max(count, self.field.rel.min_num_in_admin) - if self.field.rel.max_num_in_admin: - count = min(count, self.field.rel.max_num_in_admin) - - change = count - len(list) - if change > 0: - return list + [None for _ in range(change)] - if change < 0: - return list[:change] - else: # Just right - return list - else: - return [None for _ in range(self.field.rel.num_in_admin)] - - - def editable_fields(self): - "Get the fields in this class that should be edited inline." - return [f for f in self.opts.fields + self.opts.many_to_many if f.editable and f != self.field] - - def get_follow(self, override=None): - if isinstance(override, bool): - if override: - over = {} - else: - return None - else: - if override: - over = override.copy() - elif self.edit_inline: - over = {} - else: - return None - - over[self.field.name] = False - return self.opts.get_follow(over) - - def __repr__(self): - return "" % (self.name, self.field.name) - - def get_manipulator_fields(self, opts, manipulator, change, follow): - # TODO: Remove core fields stuff. - - if manipulator.original_object: - meth_name = 'get_%s_count' % self.get_method_name_part() - count = getattr(manipulator.original_object, meth_name)() - - count += self.field.rel.num_extra_on_change - if self.field.rel.min_num_in_admin: - count = max(count, self.field.rel.min_num_in_admin) - if self.field.rel.max_num_in_admin: - count = min(count, self.field.rel.max_num_in_admin) - else: - count = self.field.rel.num_in_admin - fields = [] - for i in range(count): - for f in self.opts.fields + self.opts.many_to_many: - if follow.get(f.name, False): - prefix = '%s.%d.' % (self.var_name, i) - fields.extend(f.get_manipulator_fields(self.opts, manipulator, change, name_prefix=prefix, rel=True)) - return fields - - def bind(self, field_mapping, original, bound_related_object_class=BoundRelatedObject): - return bound_related_object_class(self, field_mapping, original) - - def get_method_name_part(self): - # This method encapsulates the logic that decides what name to give a - # method that retrieves related many-to-one or many-to-many objects. - # Usually it just uses the lower-cased object_name, but if the related - # object is in another app, the related object's app_label is appended. - # - # Examples: - # - # # Normal case -- a related object in the same app. - # # This method returns "choice". - # Poll.get_choice_list() - # - # # A related object in a different app. - # # This method returns "lcom_bestofaward". - # Place.get_lcom_bestofaward_list() # "lcom_bestofaward" - rel_obj_name = self.field.rel.related_name or self.opts.object_name.lower() - if self.parent_opts.app_label != self.opts.app_label: - rel_obj_name = '%s_%s' % (self.opts.app_label, rel_obj_name) - return rel_obj_name - - -class Options: - def __init__(self, module_name='', verbose_name='', verbose_name_plural='', db_table='', - fields=None, ordering=None, unique_together=None, admin=None, - where_constraints=None, object_name=None, app_label=None, - exceptions=None, permissions=None, get_latest_by=None, - order_with_respect_to=None, module_constants=None): - # Move many-to-many related fields from self.fields into self.many_to_many. - self.fields, self.many_to_many = [], [] - for field in (fields or []): - if field.rel and isinstance(field.rel, ManyToMany): - self.many_to_many.append(field) - else: - self.fields.append(field) - self.module_name, self.verbose_name = module_name, verbose_name - self.verbose_name_plural = verbose_name_plural or verbose_name + 's' - self.db_table = db_table - self.ordering = ordering or [] - self.unique_together = unique_together or [] - self.where_constraints = where_constraints or [] - self.exceptions = exceptions or [] - self.permissions = permissions or [] - self.object_name, self.app_label = object_name, app_label - self.get_latest_by = get_latest_by - if order_with_respect_to: - self.order_with_respect_to = self.get_field(order_with_respect_to) - self.ordering = ('_order',) - else: - self.order_with_respect_to = None - self.module_constants = module_constants or {} - self.admin = admin - - # Calculate one_to_one_field. - self.one_to_one_field = None - for f in self.fields: - if isinstance(f.rel, OneToOne): - self.one_to_one_field = f - break - # Cache the primary-key field. - self.pk = None - for f in self.fields: - if f.primary_key: - self.pk = f - break - # If a primary_key field hasn't been specified, add an - # auto-incrementing primary-key ID field automatically. - if self.pk is None: - self.fields.insert(0, AutoField(name='id', verbose_name='ID', primary_key=True)) - self.pk = self.fields[0] - # Cache whether this has an AutoField. - self.has_auto_field = False - for f in self.fields: - is_auto = isinstance(f, AutoField) - if is_auto and self.has_auto_field: - raise AssertionError, "A model can't have more than one AutoField." - elif is_auto: - self.has_auto_field = True - #HACK - self.limit_choices_to = {} - - - def __repr__(self): - return '' % self.module_name - - # def get_model_module(self): - # return get_module(self.app_label, self.module_name) - - def get_content_type_id(self): - "Returns the content-type ID for this object type." - if not hasattr(self, '_content_type_id'): - import django.models.core - manager = django.models.core.ContentType.objects - self._content_type_id = \ - manager.get_object(python_module_name__exact=self.module_name, - package__label__exact=self.app_label).id - return self._content_type_id - - def get_field(self, name, many_to_many=True): - """ - Returns the requested field by name. Raises FieldDoesNotExist on error. - """ - to_search = many_to_many and (self.fields + self.many_to_many) or self.fields - for f in to_search: - if f.name == name: - return f - raise FieldDoesNotExist, "name=%s" % name - - def get_order_sql(self, table_prefix=''): - "Returns the full 'ORDER BY' clause for this object, according to self.ordering." - if not self.ordering: return '' - pre = table_prefix and (table_prefix + '.') or '' - return 'ORDER BY ' + orderlist2sql(self.ordering, self, pre) - - def get_add_permission(self): - return 'add_%s' % self.object_name.lower() - - def get_change_permission(self): - return 'change_%s' % self.object_name.lower() - - def get_delete_permission(self): - return 'delete_%s' % self.object_name.lower() - - def get_all_related_objects(self): - try: # Try the cache first. - return self._all_related_objects - except AttributeError: - module_list = get_installed_model_modules() - rel_objs = [] - for mod in module_list: - for klass in mod._MODELS: - for f in klass._meta.fields: - if f.rel and self == f.rel.to._meta: - rel_objs.append(RelatedObject(self, klass, f)) - self._all_related_objects = rel_objs - return rel_objs - - def get_followed_related_objects(self, follow=None): - if follow == None: - follow = self.get_follow() - return [f for f in self.get_all_related_objects() if follow.get(f.name, None)] - - def get_data_holders(self, follow=None): - if follow == None: - follow = self.get_follow() - return [f for f in self.fields + self.many_to_many + self.get_all_related_objects() if follow.get(f.name, None)] - - def get_follow(self, override=None): - follow = {} - for f in self.fields + self.many_to_many + self.get_all_related_objects(): - if override and override.has_key(f.name): - child_override = override[f.name] - else: - child_override = None - fol = f.get_follow(child_override) - if fol: - follow[f.name] = fol - return follow - - def get_all_related_many_to_many_objects(self): - module_list = get_installed_model_modules() - rel_objs = [] - for mod in module_list: - for klass in mod._MODELS: - for f in klass._meta.many_to_many: - if f.rel and self == f.rel.to._meta: - rel_objs.append(RelatedObject(self, klass, f)) - return rel_objs - - def get_ordered_objects(self): - "Returns a list of Options objects that are ordered with respect to this object." - if not hasattr(self, '_ordered_objects'): - objects = [] - #HACK - #for klass in get_app(self.app_label)._MODELS: - # opts = klass._meta - # if opts.order_with_respect_to and opts.order_with_respect_to.rel \ - # and self == opts.order_with_respect_to.rel.to._meta: - # objects.append(opts) - self._ordered_objects = objects - return self._ordered_objects - - def has_field_type(self, field_type, follow=None): - """ - Returns True if this object's admin form has at least one of the given - field_type (e.g. FileField). - """ - # TODO: follow - if not hasattr(self, '_field_types'): - self._field_types = {} - if not self._field_types.has_key(field_type): - try: - # First check self.fields. - for f in self.fields: - if isinstance(f, field_type): - raise StopIteration - # Failing that, check related fields. - for related in self.get_followed_related_objects(follow): - for f in related.opts.fields: - if isinstance(f, field_type): - raise StopIteration - except StopIteration: - self._field_types[field_type] = True - else: - self._field_types[field_type] = False - return self._field_types[field_type] - -# Calculate the module_name using a poor-man's pluralization. -get_module_name = lambda class_name: class_name.lower() + 's' - -# Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces". -get_verbose_name = lambda class_name: re.sub('([A-Z])', ' \\1', class_name).lower().strip() - - - - -class ModelBase(type): - "Metaclass for all models" - def __new__(cls, name, bases, attrs): - # If this isn't a subclass of Model, don't do anything special. - if not bases or bases == (object,): - return type.__new__(cls, name, bases, attrs) - - try: - meta_attrs = attrs.pop('META').__dict__ - del meta_attrs['__module__'] - del meta_attrs['__doc__'] - except KeyError: - meta_attrs = {} - - # Gather all attributes that are Field or Manager instances. - fields, managers = [], [] - for obj_name, obj in attrs.items(): - if isinstance(obj, Field): - obj.set_name(obj_name) - fields.append(obj) - del attrs[obj_name] - elif isinstance(obj, Manager): - managers.append((obj_name, obj)) - del attrs[obj_name] - - # Sort the fields and managers in the order that they were created. The - # "creation_counter" is needed because metaclasses don't preserve the - # attribute order. - fields.sort(lambda x, y: x.creation_counter - y.creation_counter) - managers.sort(lambda x, y: x[1].creation_counter - y[1].creation_counter) - - opts = Options( - module_name = meta_attrs.pop('module_name', get_module_name(name)), - # If the verbose_name wasn't given, use the class name, - # converted from InitialCaps to "lowercase with spaces". - verbose_name = meta_attrs.pop('verbose_name', get_verbose_name(name)), - verbose_name_plural = meta_attrs.pop('verbose_name_plural', ''), - db_table = meta_attrs.pop('db_table', ''), - fields = fields, - ordering = meta_attrs.pop('ordering', None), - unique_together = meta_attrs.pop('unique_together', None), - admin = meta_attrs.pop('admin', None), - where_constraints = meta_attrs.pop('where_constraints', None), - object_name = name, - app_label = meta_attrs.pop('app_label', None), - exceptions = meta_attrs.pop('exceptions', None), - permissions = meta_attrs.pop('permissions', None), - get_latest_by = meta_attrs.pop('get_latest_by', None), - order_with_respect_to = meta_attrs.pop('order_with_respect_to', None), - module_constants = meta_attrs.pop('module_constants', None), - ) - - if meta_attrs != {}: - raise TypeError, "'class META' got invalid attribute(s): %s" % ','.join(meta_attrs.keys()) - - # Create the DoesNotExist exception. - attrs['DoesNotExist'] = types.ClassType('DoesNotExist', (ObjectDoesNotExist,), {}) - - # Create the class, because we need it to use in currying. - new_class = type.__new__(cls, name, bases, attrs) - - # Give the class a docstring -- its definition. - if new_class.__doc__ is None: - new_class.__doc__ = "%s.%s(%s)" % (opts.module_name, name, ", ".join([f.name for f in opts.fields])) - - if hasattr(new_class, 'get_absolute_url'): - new_class.get_absolute_url = curry(get_absolute_url, opts, new_class.get_absolute_url) - - # Figure out the app_label by looking one level up. - app_package = sys.modules.get(new_class.__module__) - app_label = app_package.__name__.replace('.models', '') - app_label = app_label[app_label.rfind('.')+1:] - - # Populate the _MODELS member on the module the class is in. - app_package.__dict__.setdefault('_MODELS', []).append(new_class) - - # Cache the app label. - opts.app_label = app_label - - # If the db_table wasn't provided, use the app_label + module_name. - if not opts.db_table: - opts.db_table = "%s_%s" % (app_label, opts.module_name) - new_class._meta = opts - - # Create the default manager, if needed. - # TODO: Use weakref because of possible memory leak / circular reference. - if managers: - for m_name, m in managers: - m._prepare(new_class) - setattr(new_class, m_name, m) - new_class._default_manager = managers[0][1] - else: - if hasattr(new_class, 'objects'): - raise ValueError, "Model %s must specify a custom Manager, because it has a field named 'objects'" % name - m = Manager() - m._prepare(new_class) - new_class.objects = m - new_class._default_manager = m - - new_class._prepare() - - for field in fields: - if field.rel: - other = field.rel.to - if isinstance(other, basestring): - print "string lookup" - else: - related = RelatedObject(other._meta, new_class, field) - field.contribute_to_related_class(other, related) - - - return new_class - - -class Model(object): - __metaclass__ = ModelBase - - AddManipulator = ManipulatorDescriptor('AddManipulator', ModelAddManipulator) - ChangeManipulator = ManipulatorDescriptor('ChangeManipulator', ModelChangeManipulator) - - def __repr__(self): - return '<%s object>' % self.__class__.__name__ - - def __eq__(self, other): - return isinstance(other, self.__class__) and getattr(self, self._meta.pk.attname) == getattr(other, self._meta.pk.attname) - - def __ne__(self, other): - return not self.__eq__(other) - - def __init__(self, *args, **kwargs): - if kwargs: - for f in self._meta.fields: - if isinstance(f.rel, ManyToOne): - try: - # Assume object instance was passed in. - rel_obj = kwargs.pop(f.name) - except KeyError: - try: - # Object instance wasn't passed in -- must be an ID. - val = kwargs.pop(f.attname) - except KeyError: - val = f.get_default() - else: - # Object instance was passed in. - # Special case: You can pass in "None" for related objects if it's allowed. - if rel_obj is None and f.null: - val = None - else: - try: - val = getattr(rel_obj, f.rel.get_related_field().attname) - except AttributeError: - raise TypeError, "Invalid value: %r should be a %s instance, not a %s" % (f.name, f.rel.to, type(rel_obj)) - setattr(self, f.attname, val) - else: - val = kwargs.pop(f.attname, f.get_default()) - setattr(self, f.attname, val) - if kwargs: - raise TypeError, "'%s' is an invalid keyword argument for this function" % kwargs.keys()[0] - for i, arg in enumerate(args): - setattr(self, self._meta.fields[i].attname, arg) - - def _prepare(cls): - # Creates some methods once self._meta has been populated. - for f in cls._meta.fields: - if f.choices: - setattr(cls, 'get_%s_display' % f.name, curry(cls.__get_FIELD_display, field=f)) - if isinstance(f, DateField): - if not f.null: - setattr(cls, 'get_next_by_%s' % f.name, curry(cls.__get_next_or_previous_by_FIELD, field=f, is_next=True)) - setattr(cls, 'get_previous_by_%s' % f.name, curry(cls.__get_next_or_previous_by_FIELD, field=f, is_next=False)) - elif isinstance(f, FileField): - setattr(cls, 'get_%s_filename' % f.name, curry(cls.__get_FIELD_filename, field=f)) - setattr(cls, 'get_%s_url' % f.name, curry(cls.__get_FIELD_url, field=f)) - setattr(cls, 'get_%s_size' % f.name, curry(cls.__get_FIELD_size, field=f)) - setattr(cls, 'save_%s_file' % f.name, curry(cls.__save_FIELD_file, field=f)) - if isinstance(f, ImageField): - # Add get_BLAH_width and get_BLAH_height methods, but only - # if the image field doesn't have width and height cache - # fields. - if not f.width_field: - setattr(cls, 'get_%s_width' % f.name, curry(cls.__get_FIELD_width, field=f)) - if not f.height_field: - setattr(cls, 'get_%s_height' % f.name, curry(cls.__get_FIELD_height, field=f)) - - # If the object has a relationship to itself, as designated by - # RECURSIVE_RELATIONSHIP_CONSTANT, create that relationship formally. - if f.rel and f.rel.to == RECURSIVE_RELATIONSHIP_CONSTANT: - f.rel.to = cls - f.name = f.name or (f.rel.to._meta.object_name.lower() + '_' + f.rel.to._meta.pk.name) - f.verbose_name = f.verbose_name or f.rel.to._meta.verbose_name - f.rel.field_name = f.rel.field_name or f.rel.to._meta.pk.name - - # Add methods for many-to-one related objects. - # EXAMPLES: Choice.get_poll(), Story.get_dateline() - if isinstance(f.rel, ManyToOne): - setattr(cls, 'get_%s' % f.name, curry(cls.__get_foreign_key_object, field_with_rel=f)) - - # Create the default class methods. - for f in cls._meta.many_to_many: - # Add "get_thingie" methods for many-to-many related objects. - # EXAMPLES: Poll.get_site_list(), Story.get_byline_list() - setattr(cls, 'get_%s_list' % f.rel.singular, curry(cls.__get_many_to_many_objects, field_with_rel=f)) - - # Add "set_thingie" methods for many-to-many related objects. - # EXAMPLES: Poll.set_sites(), Story.set_bylines() - setattr(cls, 'set_%s' % f.name, curry(cls.__set_many_to_many_objects, field_with_rel=f)) - - if cls._meta.order_with_respect_to: - cls.get_next_in_order = curry(cls.__get_next_or_previous_in_order, is_next=True) - cls.get_previous_in_order = curry(cls.__get_next_or_previous_in_order, is_next=False) - - _prepare = classmethod(_prepare) - - def save(self): - # Run any pre-save hooks. - if hasattr(self, '_pre_save'): - self._pre_save() - - non_pks = [f for f in self._meta.fields if not f.primary_key] - cursor = connection.cursor() - - # First, try an UPDATE. If that doesn't update anything, do an INSERT. - pk_val = getattr(self, self._meta.pk.attname) - pk_set = bool(pk_val) - record_exists = True - if pk_set: - # Determine whether a record with the primary key already exists. - cursor.execute("SELECT 1 FROM %s WHERE %s=%%s LIMIT 1" % \ - (backend.quote_name(self._meta.db_table), backend.quote_name(self._meta.pk.column)), [pk_val]) - # If it does already exist, do an UPDATE. - if cursor.fetchone(): - db_values = [f.get_db_prep_save(f.pre_save(getattr(self, f.attname), False)) for f in non_pks] - cursor.execute("UPDATE %s SET %s WHERE %s=%%s" % \ - (backend.quote_name(self._meta.db_table), - ','.join(['%s=%%s' % backend.quote_name(f.column) for f in non_pks]), - backend.quote_name(self._meta.pk.attname)), - db_values + [pk_val]) - else: - record_exists = False - if not pk_set or not record_exists: - field_names = [backend.quote_name(f.column) for f in self._meta.fields if not isinstance(f, AutoField)] - db_values = [f.get_db_prep_save(f.pre_save(getattr(self, f.attname), True)) for f in self._meta.fields if not isinstance(f, AutoField)] - # If the PK has been manually set, respect that. - if pk_set: - field_names += [f.column for f in self._meta.fields if isinstance(f, AutoField)] - db_values += [f.get_db_prep_save(f.pre_save(getattr(self, f.column), True)) for f in self._meta.fields if isinstance(f, AutoField)] - placeholders = ['%s'] * len(field_names) - if self._meta.order_with_respect_to: - field_names.append(backend.quote_name('_order')) - # TODO: This assumes the database supports subqueries. - placeholders.append('(SELECT COUNT(*) FROM %s WHERE %s = %%s)' % \ - (backend.quote_name(self._meta.db_table), backend.quote_name(self._meta.order_with_respect_to.column))) - db_values.append(getattr(self, self._meta.order_with_respect_to.attname)) - cursor.execute("INSERT INTO %s (%s) VALUES (%s)" % \ - (backend.quote_name(self._meta.db_table), ','.join(field_names), - ','.join(placeholders)), db_values) - if self._meta.has_auto_field and not pk_set: - setattr(self, self._meta.pk.attname, backend.get_last_insert_id(cursor, self._meta.db_table, self._meta.pk.column)) - connection.commit() - - # Run any post-save hooks. - if hasattr(self, '_post_save'): - self._post_save() - - save.alters_data = True - - def delete(self): - assert getattr(self, self._meta.pk.attname) is not None, "%r can't be deleted because it doesn't have an ID." - - # Run any pre-delete hooks. - if hasattr(self, '_pre_delete'): - self._pre_delete() - - cursor = connection.cursor() - for related in self._meta.get_all_related_objects(): - rel_opts_name = related.get_method_name_part() - if isinstance(related.field.rel, OneToOne): - try: - sub_obj = getattr(self, 'get_%s' % rel_opts_name)() - except ObjectDoesNotExist: - pass - else: - sub_obj.delete() - else: - for sub_obj in getattr(self, 'get_%s_list' % rel_opts_name)(): - sub_obj.delete() - for related in self._meta.get_all_related_many_to_many_objects(): - cursor.execute("DELETE FROM %s WHERE %s=%%s" % \ - (backend.quote_name(related.field.get_m2m_db_table(related.opts)), - backend.quote_name(self._meta.object_name.lower() + '_id')), [getattr(self, self._meta.pk.attname)]) - for f in self._meta.many_to_many: - cursor.execute("DELETE FROM %s WHERE %s=%%s" % \ - (backend.quote_name(f.get_m2m_db_table(self._meta)), - backend.quote_name(self._meta.object_name.lower() + '_id')), - [getattr(self, self._meta.pk.attname)]) - - cursor.execute("DELETE FROM %s WHERE %s=%%s" % \ - (backend.quote_name(self._meta.db_table), backend.quote_name(self._meta.pk.column)), - [getattr(self, self._meta.pk.attname)]) - - connection.commit() - setattr(self, self._meta.pk.attname, None) - for f in self._meta.fields: - if isinstance(f, FileField) and getattr(self, f.attname): - file_name = getattr(self, 'get_%s_filename' % f.name)() - # If the file exists and no other object of this type references it, - # delete it from the filesystem. - if os.path.exists(file_name) and not self._default_manager.get_list(**{'%s__exact' % f.name: getattr(self, f.name)}): - os.remove(file_name) - - # Run any post-delete hooks. - if hasattr(self, '_post_delete'): - self._post_delete() - - delete.alters_data = True - - - def __get_FIELD_display(self, field): - value = getattr(self, field.attname) - return dict(field.choices).get(value, value) - - def __get_next_or_previous_by_FIELD(self, field, is_next, **kwargs): - op = is_next and '>' or '<' - kwargs.setdefault('where', []).append('(%s %s %%s OR (%s = %%s AND %s.%s %s %%s))' % \ - (backend.quote_name(field.column), op, backend.quote_name(field.column), - backend.quote_name(self._meta.db_table), backend.quote_name(self._meta.pk.column), op)) - param = str(getattr(self, field.attname)) - kwargs.setdefault('params', []).extend([param, param, getattr(self, self._meta.pk.attname)]) - kwargs['order_by'] = [(not is_next and '-' or '') + field.name, (not is_next and '-' or '') + self._meta.pk.name] - kwargs['limit'] = 1 - return self.__class__._default_manager.get_object(**kwargs) - - def __get_next_or_previous_in_order(self, is_next): - cachename = "__%s_order_cache" % is_next - if not hasattr(self, cachename): - op = is_next and '>' or '<' - order_field = self.order_with_respect_to - obj = self._default_manager.get_object(order_by=('_order',), - where=['%s %s (SELECT %s FROM %s WHERE %s=%%s)' % \ - (backend.quote_name('_order'), op, backend.quote_name('_order'), - backend.quote_name(opts.db_table), backend.quote_name(opts.pk.column)), - '%s=%%s' % backend.quote_name(order_field.column)], - limit=1, - params=[getattr(self, opts.pk.attname), getattr(self, order_field.attname)]) - setattr(self, cachename, obj) - return getattr(self, cachename) - - def __get_FIELD_filename(self, field): - return os.path.join(settings.MEDIA_ROOT, getattr(self, field.attname)) - - def __get_FIELD_url(self, field): - if getattr(self, field.attname): # value is not blank - import urlparse - return urlparse.urljoin(settings.MEDIA_URL, getattr(self, field.attname)).replace('\\', '/') - return '' - - def __get_FIELD_size(self, field): - return os.path.getsize(self.__get_FIELD_filename(field)) - - def __save_FIELD_file(self, field, filename, raw_contents): - directory = field.get_directory_name() - try: # Create the date-based directory if it doesn't exist. - os.makedirs(os.path.join(settings.MEDIA_ROOT, directory)) - except OSError: # Directory probably already exists. - pass - filename = field.get_filename(filename) - - # If the filename already exists, keep adding an underscore to the name of - # the file until the filename doesn't exist. - while os.path.exists(os.path.join(settings.MEDIA_ROOT, filename)): - try: - dot_index = filename.rindex('.') - except ValueError: # filename has no dot - filename += '_' - else: - filename = filename[:dot_index] + '_' + filename[dot_index:] - - # Write the file to disk. - setattr(self, field.attname, filename) - - full_filename = self.__get_FIELD_filename(field) - fp = open(full_filename, 'wb') - fp.write(raw_contents) - fp.close() - - # Save the width and/or height, if applicable. - if isinstance(field, ImageField) and (field.width_field or field.height_field): - from django.utils.images import get_image_dimensions - width, height = get_image_dimensions(full_filename) - if field.width_field: - setattr(self, field.width_field, width) - if field.height_field: - setattr(self, field.height_field, height) - - # Save the object, because it has changed. - self.save() - - __save_FIELD_file.alters_data = True - - def __get_FIELD_width(self, field): - return self.__get_image_dimensions(field)[0] - - def __get_FIELD_height(self, field): - return self.__get_image_dimensions(field)[1] - - def __get_image_dimensions(self, field): - cachename = "__%s_dimensions_cache" % field.name - if not hasattr(self, cachename): - from django.utils.images import get_image_dimensions - filename = self.__get_FIELD_filename(field)() - setattr(self, cachename, get_image_dimensions(filename)) - return getattr(self, cachename) - - def __get_foreign_key_object(self, field_with_rel): - cache_var = field_with_rel.get_cache_name() - if not hasattr(self, cache_var): - val = getattr(self, field_with_rel.attname) - if val is None: - raise field_with_rel.rel.to.DoesNotExist - other_field = field_with_rel.rel.get_related_field() - if other_field.rel: - params = {'%s__%s__exact' % (field_with_rel.rel.field_name, other_field.rel.field_name): val} - else: - params = {'%s__exact' % field_with_rel.rel.field_name: val} - retrieved_obj = field_with_rel.rel.to._default_manager.get_object(**params) - setattr(self, cache_var, retrieved_obj) - return getattr(self, cache_var) - - def __get_many_to_many_objects(self, field_with_rel): - cache_var = '_%s_cache' % field_with_rel.name - if not hasattr(self, cache_var): - rel_opts = field_with_rel.rel.to._meta - sql = "SELECT %s FROM %s a, %s b WHERE a.%s = b.%s AND b.%s = %%s %s" % \ - (','.join(['a.%s' % backend.quote_name(f.column) for f in rel_opts.fields]), - backend.quote_name(rel_opts.db_table), - backend.quote_name(field_with_rel.get_m2m_db_table(self._meta)), - backend.quote_name(rel_opts.pk.column), - backend.quote_name(rel_opts.object_name.lower() + '_id'), - backend.quote_name(self._meta.object_name.lower() + '_id'), rel_opts.get_order_sql('a')) - cursor = connection.cursor() - cursor.execute(sql, [getattr(self, self._meta.pk.attname)]) - setattr(self, cache_var, [field_with_rel.rel.to(*row) for row in cursor.fetchall()]) - return getattr(self, cache_var) - - def __set_many_to_many_objects(self, id_list, field_with_rel): - current_ids = [obj.id for obj in self.__get_many_to_many_objects(field_with_rel)] - ids_to_add, ids_to_delete = dict([(i, 1) for i in id_list]), [] - for current_id in current_ids: - if current_id in id_list: - del ids_to_add[current_id] - else: - ids_to_delete.append(current_id) - ids_to_add = ids_to_add.keys() - # Now ids_to_add is a list of IDs to add, and ids_to_delete is a list of IDs to delete. - if not ids_to_delete and not ids_to_add: - return False # No change - rel = field_with_rel.rel.to._meta - m2m_table = field_with_rel.get_m2m_db_table(self._meta) - cursor = connection.cursor() - this_id = getattr(self, self._meta.pk.attname) - if ids_to_delete: - sql = "DELETE FROM %s WHERE %s = %%s AND %s IN (%s)" % \ - (backend.quote_name(m2m_table), - backend.quote_name(self._meta.object_name.lower() + '_id'), - backend.quote_name(rel.object_name.lower() + '_id'), ','.join(map(str, ids_to_delete))) - cursor.execute(sql, [this_id]) - if ids_to_add: - sql = "INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ - (backend.quote_name(m2m_table), - backend.quote_name(self._meta.object_name.lower() + '_id'), - backend.quote_name(rel.object_name.lower() + '_id')) - cursor.executemany(sql, [(this_id, i) for i in ids_to_add]) - connection.commit() - try: - delattr(self, '_%s_cache' % field_with_rel.name) # clear cache, if it exists - except AttributeError: - pass - return True - - __set_many_to_many_objects.alters_data = True - - def _get_related(self, method_name, rel_class, rel_field, **kwargs): - kwargs['%s__%s__exact' % (rel_field.name, rel_field.rel.to._meta.pk.name)] = getattr(self, rel_field.rel.get_related_field().attname) - kwargs.update(rel_field.rel.lookup_overrides) - return getattr(rel_class._default_manager, method_name)(**kwargs) - - def _add_related(self, rel_class, rel_field, *args, **kwargs): - init_kwargs = dict(zip([f.attname for f in rel_class._meta.fields if f != rel_field and not isinstance(f, AutoField)], args)) - init_kwargs.update(kwargs) - for f in rel_class._meta.fields: - if isinstance(f, AutoField): - init_kwargs[f.attname] = None - init_kwargs[rel_field.name] = self - obj = rel_class(**init_kwargs) - obj.save() - return obj - - _add_related.alters_data = True - - - # Handles related many-to-many object retrieval. - # Examples: Album.get_song(), Album.get_song_list(), Album.get_song_count() - def _get_related_many_to_many(self, method_name, rel_class, rel_field, **kwargs): - kwargs['%s__%s__exact' % (rel_field.name, self._meta.pk.name)] = getattr(self, self._meta.pk.attname) - return getattr(rel_class._default_manager, method_name)(**kwargs) - - # Handles setting many-to-many related objects. - # Example: Album.set_songs() - def _set_related_many_to_many(self, rel_class, rel_field, id_list): - id_list = map(int, id_list) # normalize to integers - rel = rel_field.rel.to - m2m_table = rel_field.get_m2m_db_table(rel_opts) - this_id = getattr(self, self._meta.pk.attname) - cursor = connection.cursor() - cursor.execute("DELETE FROM %s WHERE %s = %%s" % \ - (backend.quote_name(m2m_table), - backend.quote_name(rel.object_name.lower() + '_id')), [this_id]) - sql = "INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ - (backend.quote_name(m2m_table), - backend.quote_name(rel.object_name.lower() + '_id'), - backend.quote_name(rel_opts.object_name.lower() + '_id')) - cursor.executemany(sql, [(this_id, i) for i in id_list]) - connection.commit() - - - - - -############################################ -# HELPER FUNCTIONS (CURRIED MODEL METHODS) # -############################################ - -# ORDERING METHODS ######################### - -def method_set_order(ordered_obj, self, id_list): - cursor = connection.cursor() - # Example: "UPDATE poll_choices SET _order = %s WHERE poll_id = %s AND id = %s" - sql = "UPDATE %s SET %s = %%s WHERE %s = %%s AND %s = %%s" % \ - (backend.quote_name(ordered_obj.db_table), backend.quote_name('_order'), - backend.quote_name(ordered_obj.order_with_respect_to.column), - backend.quote_name(ordered_obj.pk.column)) - rel_val = getattr(self, ordered_obj.order_with_respect_to.rel.field_name) - cursor.executemany(sql, [(i, rel_val, j) for i, j in enumerate(id_list)]) - connection.commit() - -def method_get_order(ordered_obj, self): - cursor = connection.cursor() - # Example: "SELECT id FROM poll_choices WHERE poll_id = %s ORDER BY _order" - sql = "SELECT %s FROM %s WHERE %s = %%s ORDER BY %s" % \ - (backend.quote_name(ordered_obj.pk.column), - backend.quote_name(ordered_obj.db_table), - backend.quote_name(ordered_obj.order_with_respect_to.column), - backend.quote_name('_order')) - rel_val = getattr(self, ordered_obj.order_with_respect_to.rel.field_name) - cursor.execute(sql, [rel_val]) - return [r[0] for r in cursor.fetchall()] - -############################################## -# HELPER FUNCTIONS (CURRIED MODEL FUNCTIONS) # -############################################## - -def get_absolute_url(opts, func, self): - return settings.ABSOLUTE_URL_OVERRIDES.get('%s.%s' % (opts.app_label, opts.module_name), func)(self) diff --git a/django/db/models/base.py b/django/db/models/base.py new file mode 100644 index 0000000000..dc3d657126 --- /dev/null +++ b/django/db/models/base.py @@ -0,0 +1,589 @@ +from django.db.models.manipulators import ManipulatorDescriptor, ModelAddManipulator, ModelChangeManipulator +from django.db.models.fields import Field, DateField, FileField, ImageField, AutoField +from django.db.models.fields import OneToOne, ManyToOne, ManyToMany, RECURSIVE_RELATIONSHIP_CONSTANT +from django.db.models.related import RelatedObject +from django.db.models.manager import Manager +from django.db.models.query import orderlist2sql +from django.db.models.options import Options +from django.db import connection, backend + +from django.core.exceptions import ObjectDoesNotExist +from django.utils.functional import curry + +import re +import types +import sys + + +# Calculate the module_name using a poor-man's pluralization. +get_module_name = lambda class_name: class_name.lower() + 's' + +# Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces". +get_verbose_name = lambda class_name: re.sub('([A-Z])', ' \\1', class_name).lower().strip() + + +class ModelBase(type): + "Metaclass for all models" + def __new__(cls, name, bases, attrs): + # If this isn't a subclass of Model, don't do anything special. + if not bases or bases == (object,): + return type.__new__(cls, name, bases, attrs) + + try: + meta_attrs = attrs.pop('META').__dict__ + del meta_attrs['__module__'] + del meta_attrs['__doc__'] + except KeyError: + meta_attrs = {} + + # Gather all attributes that are Field or Manager instances. + fields, managers = [], [] + for obj_name, obj in attrs.items(): + if isinstance(obj, Field): + obj.set_name(obj_name) + fields.append(obj) + del attrs[obj_name] + elif isinstance(obj, Manager): + managers.append((obj_name, obj)) + del attrs[obj_name] + + # Sort the fields and managers in the order that they were created. The + # "creation_counter" is needed because metaclasses don't preserve the + # attribute order. + fields.sort(lambda x, y: x.creation_counter - y.creation_counter) + managers.sort(lambda x, y: x[1].creation_counter - y[1].creation_counter) + + opts = Options( + module_name = meta_attrs.pop('module_name', get_module_name(name)), + # If the verbose_name wasn't given, use the class name, + # converted from InitialCaps to "lowercase with spaces". + verbose_name = meta_attrs.pop('verbose_name', get_verbose_name(name)), + verbose_name_plural = meta_attrs.pop('verbose_name_plural', ''), + db_table = meta_attrs.pop('db_table', ''), + fields = fields, + ordering = meta_attrs.pop('ordering', None), + unique_together = meta_attrs.pop('unique_together', None), + admin = meta_attrs.pop('admin', None), + where_constraints = meta_attrs.pop('where_constraints', None), + object_name = name, + app_label = meta_attrs.pop('app_label', None), + exceptions = meta_attrs.pop('exceptions', None), + permissions = meta_attrs.pop('permissions', None), + get_latest_by = meta_attrs.pop('get_latest_by', None), + order_with_respect_to = meta_attrs.pop('order_with_respect_to', None), + module_constants = meta_attrs.pop('module_constants', None), + ) + + if meta_attrs != {}: + raise TypeError, "'class META' got invalid attribute(s): %s" % ','.join(meta_attrs.keys()) + + # Create the DoesNotExist exception. + attrs['DoesNotExist'] = types.ClassType('DoesNotExist', (ObjectDoesNotExist,), {}) + + # Create the class, because we need it to use in currying. + new_class = type.__new__(cls, name, bases, attrs) + + # Give the class a docstring -- its definition. + if new_class.__doc__ is None: + new_class.__doc__ = "%s.%s(%s)" % (opts.module_name, name, ", ".join([f.name for f in opts.fields])) + + if hasattr(new_class, 'get_absolute_url'): + new_class.get_absolute_url = curry(get_absolute_url, opts, new_class.get_absolute_url) + + # Figure out the app_label by looking one level up. + app_package = sys.modules.get(new_class.__module__) + app_label = app_package.__name__.replace('.models', '') + app_label = app_label[app_label.rfind('.')+1:] + + # Populate the _MODELS member on the module the class is in. + app_package.__dict__.setdefault('_MODELS', []).append(new_class) + + # Cache the app label. + opts.app_label = app_label + + # If the db_table wasn't provided, use the app_label + module_name. + if not opts.db_table: + opts.db_table = "%s_%s" % (app_label, opts.module_name) + new_class._meta = opts + + # Create the default manager, if needed. + # TODO: Use weakref because of possible memory leak / circular reference. + if managers: + for m_name, m in managers: + m._prepare(new_class) + setattr(new_class, m_name, m) + new_class._default_manager = managers[0][1] + else: + if hasattr(new_class, 'objects'): + raise ValueError, "Model %s must specify a custom Manager, because it has a field named 'objects'" % name + m = Manager() + m._prepare(new_class) + new_class.objects = m + new_class._default_manager = m + + new_class._prepare() + + for field in fields: + if field.rel: + other = field.rel.to + if isinstance(other, basestring): + print "string lookup" + else: + related = RelatedObject(other._meta, new_class, field) + field.contribute_to_related_class(other, related) + + + return new_class + + +class Model(object): + __metaclass__ = ModelBase + + AddManipulator = ManipulatorDescriptor('AddManipulator', ModelAddManipulator) + ChangeManipulator = ManipulatorDescriptor('ChangeManipulator', ModelChangeManipulator) + + def __repr__(self): + return '<%s object>' % self.__class__.__name__ + + def __eq__(self, other): + return isinstance(other, self.__class__) and getattr(self, self._meta.pk.attname) == getattr(other, self._meta.pk.attname) + + def __ne__(self, other): + return not self.__eq__(other) + + def __init__(self, *args, **kwargs): + if kwargs: + for f in self._meta.fields: + if isinstance(f.rel, ManyToOne): + try: + # Assume object instance was passed in. + rel_obj = kwargs.pop(f.name) + except KeyError: + try: + # Object instance wasn't passed in -- must be an ID. + val = kwargs.pop(f.attname) + except KeyError: + val = f.get_default() + else: + # Object instance was passed in. + # Special case: You can pass in "None" for related objects if it's allowed. + if rel_obj is None and f.null: + val = None + else: + try: + val = getattr(rel_obj, f.rel.get_related_field().attname) + except AttributeError: + raise TypeError, "Invalid value: %r should be a %s instance, not a %s" % (f.name, f.rel.to, type(rel_obj)) + setattr(self, f.attname, val) + else: + val = kwargs.pop(f.attname, f.get_default()) + setattr(self, f.attname, val) + if kwargs: + raise TypeError, "'%s' is an invalid keyword argument for this function" % kwargs.keys()[0] + for i, arg in enumerate(args): + setattr(self, self._meta.fields[i].attname, arg) + + def _prepare(cls): + # Creates some methods once self._meta has been populated. + for f in cls._meta.fields: + if f.choices: + setattr(cls, 'get_%s_display' % f.name, curry(cls.__get_FIELD_display, field=f)) + if isinstance(f, DateField): + if not f.null: + setattr(cls, 'get_next_by_%s' % f.name, curry(cls.__get_next_or_previous_by_FIELD, field=f, is_next=True)) + setattr(cls, 'get_previous_by_%s' % f.name, curry(cls.__get_next_or_previous_by_FIELD, field=f, is_next=False)) + elif isinstance(f, FileField): + setattr(cls, 'get_%s_filename' % f.name, curry(cls.__get_FIELD_filename, field=f)) + setattr(cls, 'get_%s_url' % f.name, curry(cls.__get_FIELD_url, field=f)) + setattr(cls, 'get_%s_size' % f.name, curry(cls.__get_FIELD_size, field=f)) + setattr(cls, 'save_%s_file' % f.name, curry(cls.__save_FIELD_file, field=f)) + if isinstance(f, ImageField): + # Add get_BLAH_width and get_BLAH_height methods, but only + # if the image field doesn't have width and height cache + # fields. + if not f.width_field: + setattr(cls, 'get_%s_width' % f.name, curry(cls.__get_FIELD_width, field=f)) + if not f.height_field: + setattr(cls, 'get_%s_height' % f.name, curry(cls.__get_FIELD_height, field=f)) + + # If the object has a relationship to itself, as designated by + # RECURSIVE_RELATIONSHIP_CONSTANT, create that relationship formally. + if f.rel and f.rel.to == RECURSIVE_RELATIONSHIP_CONSTANT: + f.rel.to = cls + f.name = f.name or (f.rel.to._meta.object_name.lower() + '_' + f.rel.to._meta.pk.name) + f.verbose_name = f.verbose_name or f.rel.to._meta.verbose_name + f.rel.field_name = f.rel.field_name or f.rel.to._meta.pk.name + + # Add methods for many-to-one related objects. + # EXAMPLES: Choice.get_poll(), Story.get_dateline() + if isinstance(f.rel, ManyToOne): + setattr(cls, 'get_%s' % f.name, curry(cls.__get_foreign_key_object, field_with_rel=f)) + + # Create the default class methods. + for f in cls._meta.many_to_many: + # Add "get_thingie" methods for many-to-many related objects. + # EXAMPLES: Poll.get_site_list(), Story.get_byline_list() + setattr(cls, 'get_%s_list' % f.rel.singular, curry(cls.__get_many_to_many_objects, field_with_rel=f)) + + # Add "set_thingie" methods for many-to-many related objects. + # EXAMPLES: Poll.set_sites(), Story.set_bylines() + setattr(cls, 'set_%s' % f.name, curry(cls.__set_many_to_many_objects, field_with_rel=f)) + + if cls._meta.order_with_respect_to: + cls.get_next_in_order = curry(cls.__get_next_or_previous_in_order, is_next=True) + cls.get_previous_in_order = curry(cls.__get_next_or_previous_in_order, is_next=False) + + _prepare = classmethod(_prepare) + + def save(self): + # Run any pre-save hooks. + if hasattr(self, '_pre_save'): + self._pre_save() + + non_pks = [f for f in self._meta.fields if not f.primary_key] + cursor = connection.cursor() + + # First, try an UPDATE. If that doesn't update anything, do an INSERT. + pk_val = getattr(self, self._meta.pk.attname) + pk_set = bool(pk_val) + record_exists = True + if pk_set: + # Determine whether a record with the primary key already exists. + cursor.execute("SELECT 1 FROM %s WHERE %s=%%s LIMIT 1" % \ + (backend.quote_name(self._meta.db_table), backend.quote_name(self._meta.pk.column)), [pk_val]) + # If it does already exist, do an UPDATE. + if cursor.fetchone(): + db_values = [f.get_db_prep_save(f.pre_save(getattr(self, f.attname), False)) for f in non_pks] + cursor.execute("UPDATE %s SET %s WHERE %s=%%s" % \ + (backend.quote_name(self._meta.db_table), + ','.join(['%s=%%s' % backend.quote_name(f.column) for f in non_pks]), + backend.quote_name(self._meta.pk.attname)), + db_values + [pk_val]) + else: + record_exists = False + if not pk_set or not record_exists: + field_names = [backend.quote_name(f.column) for f in self._meta.fields if not isinstance(f, AutoField)] + db_values = [f.get_db_prep_save(f.pre_save(getattr(self, f.attname), True)) for f in self._meta.fields if not isinstance(f, AutoField)] + # If the PK has been manually set, respect that. + if pk_set: + field_names += [f.column for f in self._meta.fields if isinstance(f, AutoField)] + db_values += [f.get_db_prep_save(f.pre_save(getattr(self, f.column), True)) for f in self._meta.fields if isinstance(f, AutoField)] + placeholders = ['%s'] * len(field_names) + if self._meta.order_with_respect_to: + field_names.append(backend.quote_name('_order')) + # TODO: This assumes the database supports subqueries. + placeholders.append('(SELECT COUNT(*) FROM %s WHERE %s = %%s)' % \ + (backend.quote_name(self._meta.db_table), backend.quote_name(self._meta.order_with_respect_to.column))) + db_values.append(getattr(self, self._meta.order_with_respect_to.attname)) + cursor.execute("INSERT INTO %s (%s) VALUES (%s)" % \ + (backend.quote_name(self._meta.db_table), ','.join(field_names), + ','.join(placeholders)), db_values) + if self._meta.has_auto_field and not pk_set: + setattr(self, self._meta.pk.attname, backend.get_last_insert_id(cursor, self._meta.db_table, self._meta.pk.column)) + connection.commit() + + # Run any post-save hooks. + if hasattr(self, '_post_save'): + self._post_save() + + save.alters_data = True + + def delete(self): + assert getattr(self, self._meta.pk.attname) is not None, "%r can't be deleted because it doesn't have an ID." + + # Run any pre-delete hooks. + if hasattr(self, '_pre_delete'): + self._pre_delete() + + cursor = connection.cursor() + for related in self._meta.get_all_related_objects(): + rel_opts_name = related.get_method_name_part() + if isinstance(related.field.rel, OneToOne): + try: + sub_obj = getattr(self, 'get_%s' % rel_opts_name)() + except ObjectDoesNotExist: + pass + else: + sub_obj.delete() + else: + for sub_obj in getattr(self, 'get_%s_list' % rel_opts_name)(): + sub_obj.delete() + for related in self._meta.get_all_related_many_to_many_objects(): + cursor.execute("DELETE FROM %s WHERE %s=%%s" % \ + (backend.quote_name(related.field.get_m2m_db_table(related.opts)), + backend.quote_name(self._meta.object_name.lower() + '_id')), [getattr(self, self._meta.pk.attname)]) + for f in self._meta.many_to_many: + cursor.execute("DELETE FROM %s WHERE %s=%%s" % \ + (backend.quote_name(f.get_m2m_db_table(self._meta)), + backend.quote_name(self._meta.object_name.lower() + '_id')), + [getattr(self, self._meta.pk.attname)]) + + cursor.execute("DELETE FROM %s WHERE %s=%%s" % \ + (backend.quote_name(self._meta.db_table), backend.quote_name(self._meta.pk.column)), + [getattr(self, self._meta.pk.attname)]) + + connection.commit() + setattr(self, self._meta.pk.attname, None) + for f in self._meta.fields: + if isinstance(f, FileField) and getattr(self, f.attname): + file_name = getattr(self, 'get_%s_filename' % f.name)() + # If the file exists and no other object of this type references it, + # delete it from the filesystem. + if os.path.exists(file_name) and not self._default_manager.get_list(**{'%s__exact' % f.name: getattr(self, f.name)}): + os.remove(file_name) + + # Run any post-delete hooks. + if hasattr(self, '_post_delete'): + self._post_delete() + + delete.alters_data = True + + + def __get_FIELD_display(self, field): + value = getattr(self, field.attname) + return dict(field.choices).get(value, value) + + def __get_next_or_previous_by_FIELD(self, field, is_next, **kwargs): + op = is_next and '>' or '<' + kwargs.setdefault('where', []).append('(%s %s %%s OR (%s = %%s AND %s.%s %s %%s))' % \ + (backend.quote_name(field.column), op, backend.quote_name(field.column), + backend.quote_name(self._meta.db_table), backend.quote_name(self._meta.pk.column), op)) + param = str(getattr(self, field.attname)) + kwargs.setdefault('params', []).extend([param, param, getattr(self, self._meta.pk.attname)]) + kwargs['order_by'] = [(not is_next and '-' or '') + field.name, (not is_next and '-' or '') + self._meta.pk.name] + kwargs['limit'] = 1 + return self.__class__._default_manager.get_object(**kwargs) + + def __get_next_or_previous_in_order(self, is_next): + cachename = "__%s_order_cache" % is_next + if not hasattr(self, cachename): + op = is_next and '>' or '<' + order_field = self.order_with_respect_to + obj = self._default_manager.get_object(order_by=('_order',), + where=['%s %s (SELECT %s FROM %s WHERE %s=%%s)' % \ + (backend.quote_name('_order'), op, backend.quote_name('_order'), + backend.quote_name(opts.db_table), backend.quote_name(opts.pk.column)), + '%s=%%s' % backend.quote_name(order_field.column)], + limit=1, + params=[getattr(self, opts.pk.attname), getattr(self, order_field.attname)]) + setattr(self, cachename, obj) + return getattr(self, cachename) + + def __get_FIELD_filename(self, field): + return os.path.join(settings.MEDIA_ROOT, getattr(self, field.attname)) + + def __get_FIELD_url(self, field): + if getattr(self, field.attname): # value is not blank + import urlparse + return urlparse.urljoin(settings.MEDIA_URL, getattr(self, field.attname)).replace('\\', '/') + return '' + + def __get_FIELD_size(self, field): + return os.path.getsize(self.__get_FIELD_filename(field)) + + def __save_FIELD_file(self, field, filename, raw_contents): + directory = field.get_directory_name() + try: # Create the date-based directory if it doesn't exist. + os.makedirs(os.path.join(settings.MEDIA_ROOT, directory)) + except OSError: # Directory probably already exists. + pass + filename = field.get_filename(filename) + + # If the filename already exists, keep adding an underscore to the name of + # the file until the filename doesn't exist. + while os.path.exists(os.path.join(settings.MEDIA_ROOT, filename)): + try: + dot_index = filename.rindex('.') + except ValueError: # filename has no dot + filename += '_' + else: + filename = filename[:dot_index] + '_' + filename[dot_index:] + + # Write the file to disk. + setattr(self, field.attname, filename) + + full_filename = self.__get_FIELD_filename(field) + fp = open(full_filename, 'wb') + fp.write(raw_contents) + fp.close() + + # Save the width and/or height, if applicable. + if isinstance(field, ImageField) and (field.width_field or field.height_field): + from django.utils.images import get_image_dimensions + width, height = get_image_dimensions(full_filename) + if field.width_field: + setattr(self, field.width_field, width) + if field.height_field: + setattr(self, field.height_field, height) + + # Save the object, because it has changed. + self.save() + + __save_FIELD_file.alters_data = True + + def __get_FIELD_width(self, field): + return self.__get_image_dimensions(field)[0] + + def __get_FIELD_height(self, field): + return self.__get_image_dimensions(field)[1] + + def __get_image_dimensions(self, field): + cachename = "__%s_dimensions_cache" % field.name + if not hasattr(self, cachename): + from django.utils.images import get_image_dimensions + filename = self.__get_FIELD_filename(field)() + setattr(self, cachename, get_image_dimensions(filename)) + return getattr(self, cachename) + + def __get_foreign_key_object(self, field_with_rel): + cache_var = field_with_rel.get_cache_name() + if not hasattr(self, cache_var): + val = getattr(self, field_with_rel.attname) + if val is None: + raise field_with_rel.rel.to.DoesNotExist + other_field = field_with_rel.rel.get_related_field() + if other_field.rel: + params = {'%s__%s__exact' % (field_with_rel.rel.field_name, other_field.rel.field_name): val} + else: + params = {'%s__exact' % field_with_rel.rel.field_name: val} + retrieved_obj = field_with_rel.rel.to._default_manager.get_object(**params) + setattr(self, cache_var, retrieved_obj) + return getattr(self, cache_var) + + def __get_many_to_many_objects(self, field_with_rel): + cache_var = '_%s_cache' % field_with_rel.name + if not hasattr(self, cache_var): + rel_opts = field_with_rel.rel.to._meta + sql = "SELECT %s FROM %s a, %s b WHERE a.%s = b.%s AND b.%s = %%s %s" % \ + (','.join(['a.%s' % backend.quote_name(f.column) for f in rel_opts.fields]), + backend.quote_name(rel_opts.db_table), + backend.quote_name(field_with_rel.get_m2m_db_table(self._meta)), + backend.quote_name(rel_opts.pk.column), + backend.quote_name(rel_opts.object_name.lower() + '_id'), + backend.quote_name(self._meta.object_name.lower() + '_id'), rel_opts.get_order_sql('a')) + cursor = connection.cursor() + cursor.execute(sql, [getattr(self, self._meta.pk.attname)]) + setattr(self, cache_var, [field_with_rel.rel.to(*row) for row in cursor.fetchall()]) + return getattr(self, cache_var) + + def __set_many_to_many_objects(self, id_list, field_with_rel): + current_ids = [obj.id for obj in self.__get_many_to_many_objects(field_with_rel)] + ids_to_add, ids_to_delete = dict([(i, 1) for i in id_list]), [] + for current_id in current_ids: + if current_id in id_list: + del ids_to_add[current_id] + else: + ids_to_delete.append(current_id) + ids_to_add = ids_to_add.keys() + # Now ids_to_add is a list of IDs to add, and ids_to_delete is a list of IDs to delete. + if not ids_to_delete and not ids_to_add: + return False # No change + rel = field_with_rel.rel.to._meta + m2m_table = field_with_rel.get_m2m_db_table(self._meta) + cursor = connection.cursor() + this_id = getattr(self, self._meta.pk.attname) + if ids_to_delete: + sql = "DELETE FROM %s WHERE %s = %%s AND %s IN (%s)" % \ + (backend.quote_name(m2m_table), + backend.quote_name(self._meta.object_name.lower() + '_id'), + backend.quote_name(rel.object_name.lower() + '_id'), ','.join(map(str, ids_to_delete))) + cursor.execute(sql, [this_id]) + if ids_to_add: + sql = "INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ + (backend.quote_name(m2m_table), + backend.quote_name(self._meta.object_name.lower() + '_id'), + backend.quote_name(rel.object_name.lower() + '_id')) + cursor.executemany(sql, [(this_id, i) for i in ids_to_add]) + connection.commit() + try: + delattr(self, '_%s_cache' % field_with_rel.name) # clear cache, if it exists + except AttributeError: + pass + return True + + __set_many_to_many_objects.alters_data = True + + def _get_related(self, method_name, rel_class, rel_field, **kwargs): + kwargs['%s__%s__exact' % (rel_field.name, rel_field.rel.to._meta.pk.name)] = getattr(self, rel_field.rel.get_related_field().attname) + kwargs.update(rel_field.rel.lookup_overrides) + return getattr(rel_class._default_manager, method_name)(**kwargs) + + def _add_related(self, rel_class, rel_field, *args, **kwargs): + init_kwargs = dict(zip([f.attname for f in rel_class._meta.fields if f != rel_field and not isinstance(f, AutoField)], args)) + init_kwargs.update(kwargs) + for f in rel_class._meta.fields: + if isinstance(f, AutoField): + init_kwargs[f.attname] = None + init_kwargs[rel_field.name] = self + obj = rel_class(**init_kwargs) + obj.save() + return obj + + _add_related.alters_data = True + + + # Handles related many-to-many object retrieval. + # Examples: Album.get_song(), Album.get_song_list(), Album.get_song_count() + def _get_related_many_to_many(self, method_name, rel_class, rel_field, **kwargs): + kwargs['%s__%s__exact' % (rel_field.name, self._meta.pk.name)] = getattr(self, self._meta.pk.attname) + return getattr(rel_class._default_manager, method_name)(**kwargs) + + # Handles setting many-to-many related objects. + # Example: Album.set_songs() + def _set_related_many_to_many(self, rel_class, rel_field, id_list): + id_list = map(int, id_list) # normalize to integers + rel = rel_field.rel.to + m2m_table = rel_field.get_m2m_db_table(rel_opts) + this_id = getattr(self, self._meta.pk.attname) + cursor = connection.cursor() + cursor.execute("DELETE FROM %s WHERE %s = %%s" % \ + (backend.quote_name(m2m_table), + backend.quote_name(rel.object_name.lower() + '_id')), [this_id]) + sql = "INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ + (backend.quote_name(m2m_table), + backend.quote_name(rel.object_name.lower() + '_id'), + backend.quote_name(rel_opts.object_name.lower() + '_id')) + cursor.executemany(sql, [(this_id, i) for i in id_list]) + connection.commit() + + + + + +############################################ +# HELPER FUNCTIONS (CURRIED MODEL METHODS) # +############################################ + +# ORDERING METHODS ######################### + +def method_set_order(ordered_obj, self, id_list): + cursor = connection.cursor() + # Example: "UPDATE poll_choices SET _order = %s WHERE poll_id = %s AND id = %s" + sql = "UPDATE %s SET %s = %%s WHERE %s = %%s AND %s = %%s" % \ + (backend.quote_name(ordered_obj.db_table), backend.quote_name('_order'), + backend.quote_name(ordered_obj.order_with_respect_to.column), + backend.quote_name(ordered_obj.pk.column)) + rel_val = getattr(self, ordered_obj.order_with_respect_to.rel.field_name) + cursor.executemany(sql, [(i, rel_val, j) for i, j in enumerate(id_list)]) + connection.commit() + +def method_get_order(ordered_obj, self): + cursor = connection.cursor() + # Example: "SELECT id FROM poll_choices WHERE poll_id = %s ORDER BY _order" + sql = "SELECT %s FROM %s WHERE %s = %%s ORDER BY %s" % \ + (backend.quote_name(ordered_obj.pk.column), + backend.quote_name(ordered_obj.db_table), + backend.quote_name(ordered_obj.order_with_respect_to.column), + backend.quote_name('_order')) + rel_val = getattr(self, ordered_obj.order_with_respect_to.rel.field_name) + cursor.execute(sql, [rel_val]) + return [r[0] for r in cursor.fetchall()] + +############################################## +# HELPER FUNCTIONS (CURRIED MODEL FUNCTIONS) # +############################################## + +def get_absolute_url(opts, func, self): + return settings.ABSOLUTE_URL_OVERRIDES.get('%s.%s' % (opts.app_label, opts.module_name), func)(self) + + diff --git a/django/db/models/exceptions.py b/django/db/models/exceptions.py new file mode 100644 index 0000000000..e16276f51b --- /dev/null +++ b/django/db/models/exceptions.py @@ -0,0 +1,5 @@ +class FieldDoesNotExist(Exception): + pass + +class BadKeywordArguments(Exception): + pass \ No newline at end of file diff --git a/django/db/models/options.py b/django/db/models/options.py new file mode 100644 index 0000000000..9b6358b184 --- /dev/null +++ b/django/db/models/options.py @@ -0,0 +1,191 @@ +from django.db.models.related import RelatedObject +from django.db.models.fields import OneToOne, ManyToMany +from django.db.models.fields import AutoField +from django.db.models.loading import get_installed_model_modules +from django.db.models.query import orderlist2sql +from django.db.models.exceptions import FieldDoesNotExist + +class Options: + def __init__(self, module_name='', verbose_name='', verbose_name_plural='', db_table='', + fields=None, ordering=None, unique_together=None, admin=None, + where_constraints=None, object_name=None, app_label=None, + exceptions=None, permissions=None, get_latest_by=None, + order_with_respect_to=None, module_constants=None): + # Move many-to-many related fields from self.fields into self.many_to_many. + self.fields, self.many_to_many = [], [] + for field in (fields or []): + if field.rel and isinstance(field.rel, ManyToMany): + self.many_to_many.append(field) + else: + self.fields.append(field) + self.module_name, self.verbose_name = module_name, verbose_name + self.verbose_name_plural = verbose_name_plural or verbose_name + 's' + self.db_table = db_table + self.ordering = ordering or [] + self.unique_together = unique_together or [] + self.where_constraints = where_constraints or [] + self.exceptions = exceptions or [] + self.permissions = permissions or [] + self.object_name, self.app_label = object_name, app_label + self.get_latest_by = get_latest_by + if order_with_respect_to: + self.order_with_respect_to = self.get_field(order_with_respect_to) + self.ordering = ('_order',) + else: + self.order_with_respect_to = None + self.module_constants = module_constants or {} + self.admin = admin + + # Calculate one_to_one_field. + self.one_to_one_field = None + for f in self.fields: + if isinstance(f.rel, OneToOne): + self.one_to_one_field = f + break + # Cache the primary-key field. + self.pk = None + for f in self.fields: + if f.primary_key: + self.pk = f + break + # If a primary_key field hasn't been specified, add an + # auto-incrementing primary-key ID field automatically. + if self.pk is None: + self.fields.insert(0, AutoField(name='id', verbose_name='ID', primary_key=True)) + self.pk = self.fields[0] + # Cache whether this has an AutoField. + self.has_auto_field = False + for f in self.fields: + is_auto = isinstance(f, AutoField) + if is_auto and self.has_auto_field: + raise AssertionError, "A model can't have more than one AutoField." + elif is_auto: + self.has_auto_field = True + #HACK + self.limit_choices_to = {} + + + def __repr__(self): + return '' % self.module_name + + # def get_model_module(self): + # return get_module(self.app_label, self.module_name) + + def get_content_type_id(self): + "Returns the content-type ID for this object type." + if not hasattr(self, '_content_type_id'): + import django.models.core + manager = django.models.core.ContentType.objects + self._content_type_id = \ + manager.get_object(python_module_name__exact=self.module_name, + package__label__exact=self.app_label).id + return self._content_type_id + + def get_field(self, name, many_to_many=True): + """ + Returns the requested field by name. Raises FieldDoesNotExist on error. + """ + to_search = many_to_many and (self.fields + self.many_to_many) or self.fields + for f in to_search: + if f.name == name: + return f + raise FieldDoesNotExist, "name=%s" % name + + def get_order_sql(self, table_prefix=''): + "Returns the full 'ORDER BY' clause for this object, according to self.ordering." + if not self.ordering: return '' + pre = table_prefix and (table_prefix + '.') or '' + return 'ORDER BY ' + orderlist2sql(self.ordering, self, pre) + + def get_add_permission(self): + return 'add_%s' % self.object_name.lower() + + def get_change_permission(self): + return 'change_%s' % self.object_name.lower() + + def get_delete_permission(self): + return 'delete_%s' % self.object_name.lower() + + def get_all_related_objects(self): + try: # Try the cache first. + return self._all_related_objects + except AttributeError: + module_list = get_installed_model_modules() + rel_objs = [] + for mod in module_list: + for klass in mod._MODELS: + for f in klass._meta.fields: + if f.rel and self == f.rel.to._meta: + rel_objs.append(RelatedObject(self, klass, f)) + self._all_related_objects = rel_objs + return rel_objs + + def get_followed_related_objects(self, follow=None): + if follow == None: + follow = self.get_follow() + return [f for f in self.get_all_related_objects() if follow.get(f.name, None)] + + def get_data_holders(self, follow=None): + if follow == None: + follow = self.get_follow() + return [f for f in self.fields + self.many_to_many + self.get_all_related_objects() if follow.get(f.name, None)] + + def get_follow(self, override=None): + follow = {} + for f in self.fields + self.many_to_many + self.get_all_related_objects(): + if override and override.has_key(f.name): + child_override = override[f.name] + else: + child_override = None + fol = f.get_follow(child_override) + if fol: + follow[f.name] = fol + return follow + + def get_all_related_many_to_many_objects(self): + module_list = get_installed_model_modules() + rel_objs = [] + for mod in module_list: + for klass in mod._MODELS: + for f in klass._meta.many_to_many: + if f.rel and self == f.rel.to._meta: + rel_objs.append(RelatedObject(self, klass, f)) + return rel_objs + + def get_ordered_objects(self): + "Returns a list of Options objects that are ordered with respect to this object." + if not hasattr(self, '_ordered_objects'): + objects = [] + #HACK + #for klass in get_app(self.app_label)._MODELS: + # opts = klass._meta + # if opts.order_with_respect_to and opts.order_with_respect_to.rel \ + # and self == opts.order_with_respect_to.rel.to._meta: + # objects.append(opts) + self._ordered_objects = objects + return self._ordered_objects + + def has_field_type(self, field_type, follow=None): + """ + Returns True if this object's admin form has at least one of the given + field_type (e.g. FileField). + """ + # TODO: follow + if not hasattr(self, '_field_types'): + self._field_types = {} + if not self._field_types.has_key(field_type): + try: + # First check self.fields. + for f in self.fields: + if isinstance(f, field_type): + raise StopIteration + # Failing that, check related fields. + for related in self.get_followed_related_objects(follow): + for f in related.opts.fields: + if isinstance(f, field_type): + raise StopIteration + except StopIteration: + self._field_types[field_type] = True + else: + self._field_types[field_type] = False + return self._field_types[field_type] \ No newline at end of file diff --git a/django/db/models/query.py b/django/db/models/query.py index 1028ad4a0e..d991285ea2 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1,4 +1,5 @@ from django.db import backend, connection +from django.db.models.exceptions import * LOOKUP_SEPARATOR = '__' #################### diff --git a/django/db/models/related.py b/django/db/models/related.py new file mode 100644 index 0000000000..3f6d6c2dcd --- /dev/null +++ b/django/db/models/related.py @@ -0,0 +1,135 @@ +class BoundRelatedObject(object): + def __init__(self, related_object, field_mapping, original): + self.relation = related_object + self.field_mappings = field_mapping[related_object.opts.module_name] + + def template_name(self): + raise NotImplementedError + + def __repr__(self): + return repr(self.__dict__) + +class RelatedObject(object): + def __init__(self, parent_opts, model, field): + self.parent_opts = parent_opts + self.model = model + self.opts = model._meta + self.field = field + self.edit_inline = field.rel.edit_inline + self.name = self.opts.module_name + self.var_name = self.opts.object_name.lower() + + def flatten_data(self, follow, obj=None): + new_data = {} + rel_instances = self.get_list(obj) + for i, rel_instance in enumerate(rel_instances): + instance_data = {} + for f in self.opts.fields + self.opts.many_to_many: + # TODO: Fix for recursive manipulators. + fol = follow.get(f.name, None) + if fol: + field_data = f.flatten_data(fol, rel_instance) + for name, value in field_data.items(): + instance_data['%s.%d.%s' % (self.var_name, i, name)] = value + new_data.update(instance_data) + return new_data + + def extract_data(self, data): + """ + Pull out the data meant for inline objects of this class, + i.e. anything starting with our module name. + """ + return data # TODO + + def get_list(self, parent_instance=None): + "Get the list of this type of object from an instance of the parent class." + if parent_instance != None: + func_name = 'get_%s_list' % self.get_method_name_part() + func = getattr(parent_instance, func_name) + list = func() + + count = len(list) + self.field.rel.num_extra_on_change + if self.field.rel.min_num_in_admin: + count = max(count, self.field.rel.min_num_in_admin) + if self.field.rel.max_num_in_admin: + count = min(count, self.field.rel.max_num_in_admin) + + change = count - len(list) + if change > 0: + return list + [None for _ in range(change)] + if change < 0: + return list[:change] + else: # Just right + return list + else: + return [None for _ in range(self.field.rel.num_in_admin)] + + + def editable_fields(self): + "Get the fields in this class that should be edited inline." + return [f for f in self.opts.fields + self.opts.many_to_many if f.editable and f != self.field] + + def get_follow(self, override=None): + if isinstance(override, bool): + if override: + over = {} + else: + return None + else: + if override: + over = override.copy() + elif self.edit_inline: + over = {} + else: + return None + + over[self.field.name] = False + return self.opts.get_follow(over) + + def __repr__(self): + return "" % (self.name, self.field.name) + + def get_manipulator_fields(self, opts, manipulator, change, follow): + # TODO: Remove core fields stuff. + + if manipulator.original_object: + meth_name = 'get_%s_count' % self.get_method_name_part() + count = getattr(manipulator.original_object, meth_name)() + + count += self.field.rel.num_extra_on_change + if self.field.rel.min_num_in_admin: + count = max(count, self.field.rel.min_num_in_admin) + if self.field.rel.max_num_in_admin: + count = min(count, self.field.rel.max_num_in_admin) + else: + count = self.field.rel.num_in_admin + fields = [] + for i in range(count): + for f in self.opts.fields + self.opts.many_to_many: + if follow.get(f.name, False): + prefix = '%s.%d.' % (self.var_name, i) + fields.extend(f.get_manipulator_fields(self.opts, manipulator, change, name_prefix=prefix, rel=True)) + return fields + + def bind(self, field_mapping, original, bound_related_object_class=BoundRelatedObject): + return bound_related_object_class(self, field_mapping, original) + + def get_method_name_part(self): + # This method encapsulates the logic that decides what name to give a + # method that retrieves related many-to-one or many-to-many objects. + # Usually it just uses the lower-cased object_name, but if the related + # object is in another app, the related object's app_label is appended. + # + # Examples: + # + # # Normal case -- a related object in the same app. + # # This method returns "choice". + # Poll.get_choice_list() + # + # # A related object in a different app. + # # This method returns "lcom_bestofaward". + # Place.get_lcom_bestofaward_list() # "lcom_bestofaward" + rel_obj_name = self.field.rel.related_name or self.opts.object_name.lower() + if self.parent_opts.app_label != self.opts.app_label: + rel_obj_name = '%s_%s' % (self.opts.app_label, rel_obj_name) + return rel_obj_name